SpringBoot技術(shù)棧搭建個人博客【后臺開發(fā)】

前言:在之前,我們已經(jīng)完成了項目的基本準(zhǔn)備,那么就可以開始后臺開發(fā)了,突然又想到一個問題,就是準(zhǔn)備的時候只是設(shè)計了前臺的RESTful APIs,但是后臺管理我們同樣也是需要API的,那么就在這一篇里面一起實現(xiàn)了吧...

一些設(shè)計上的調(diào)整

在查了一些資料和吸收了一些評論給出良好的建議之后,我覺得有必要對一些設(shè)計進(jìn)行一些調(diào)整:

  • 1)數(shù)據(jù)庫:命名應(yīng)該更加規(guī)范,比如表示分類最好用category而不是sort,表示評論最好用comment而不是message;
  • 2)RESful APIs:在準(zhǔn)備著手開始寫后臺的時候就已經(jīng)發(fā)現(xiàn),本來想的是凡是以/api開頭的都是暴露出來給前端用的,凡是以/admin開頭的都是給后臺使用的地址,但是意外的沒有設(shè)計后天的API也把一些刪除命令暴露給了前端,這就不好了重新設(shè)計設(shè)計;
  • 3)命名規(guī)范的問題:因為使用MyBatis逆向工程自動生成的時候,配置了一個useActualColumnNames使用表真正名稱的東西,所以整得來生成POJO類基礎(chǔ)字段有下劃線,看著著實有點不爽,把它給干掉干掉...;

數(shù)據(jù)庫調(diào)整

把字段規(guī)范了一下,并且刪除了分類下是否有效的字段(感覺這種不經(jīng)常變換的字段留著也沒啥用干脆干掉..),所以調(diào)整為了下面這個樣子(調(diào)整字段已標(biāo)紅):

然后重新使用生成器自動生成對應(yīng)的文件,注意記得修改generatorConfig.xml文件中對應(yīng)的數(shù)據(jù)庫名稱;

創(chuàng)建和修改時間的字段設(shè)置

通過查資料發(fā)現(xiàn)其實我們可以通過直接設(shè)置數(shù)據(jù)庫來自動更新我們的modified_by字段,并且可以像設(shè)置初始值那樣給create_by和modified_by兩個字段以當(dāng)前時間戳設(shè)置默認(rèn)值,這里具體以tbl_article_info這張表為例:

CREATE TABLE `tbl_article_info` (
  `id` bigint(40) NOT NULL AUTO_INCREMENT COMMENT '主鍵',
  `title` varchar(50) NOT NULL DEFAULT '' COMMENT '文章標(biāo)題',
  `summary` varchar(300) NOT NULL DEFAULT '' COMMENT '文章簡介,默認(rèn)100個漢字以內(nèi)',
  `is_top` tinyint(1) NOT NULL DEFAULT '0' COMMENT '文章是否置頂,0為否,1為是',
  `traffic` int(10) NOT NULL DEFAULT '0' COMMENT '文章訪問量',
  `create_by` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '創(chuàng)建時間',
  `modified_by` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '修改日期',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

我們通過設(shè)置DEFAULTCURRENT_TIMESTAMP,然后給modified_by字段多添加了一句ON UPDATE CURRENT_TIMESTAMP,這樣它就會在更新的時候?qū)⒃撟侄蔚闹翟O(shè)置為更新時間,這樣我們就不用在后臺關(guān)心這兩個值了,也少寫了一些代碼(其實是寫代碼的時候發(fā)現(xiàn)可以這樣偷懶..hhh...);

RESTful APIs重新設(shè)計

我們需要把一些不能夠暴露給前臺的API收回,然后再設(shè)計一下后臺的API,搗鼓了一下,最后大概是這個樣子了:

后臺Restful APIs:

前臺開放RESful APIs:

這些API只是用來和前端交互的接口,另外一些關(guān)于日志啊之類的東西就直接在后臺寫就行了,OK,這樣就爽多了,可以開始著手寫代碼了;

基本配置

隨著配置內(nèi)容的增多,我逐漸的想要放棄.yml的配置文件,主要的一點是這東西不好對內(nèi)容進(jìn)行分類(下圖是簡單配置了一些基本文件后的.yml和.properties文件的對比)..

最后還是用回.properties文件吧,不分類還是有點難受

編碼設(shè)置

我們首先需要解決的是中文亂碼的問題,對應(yīng)GET請求,我們可以通過修改Tomcat的配置文件【server.xml】來把它默認(rèn)的編碼格式改為UTF-8,而對于POST請求,我們需要統(tǒng)一配置一個攔截器一樣的東西把請求的編碼統(tǒng)一改成UTF-8:

## ——————————編碼設(shè)置——————————
spring.http.encoding.charset=UTF-8
spring.http.encoding.force=true
spring.http.encoding.enabled=true
server.tomcat.uri-encoding=UTF-8

但是這樣設(shè)置之后,在后面的使用當(dāng)中還是會發(fā)生提交表單時中文亂碼的問題,在網(wǎng)上搜索了一下找到了解決方法,新建一個【config】包創(chuàng)建下面這樣一個配置類:

@Configuration
public class MyWebMvcConfigurerAdapter extends WebMvcConfigurerAdapter {
    @Bean
    public HttpMessageConverter<String> responseBodyConverter() {
        StringHttpMessageConverter converter = new StringHttpMessageConverter(Charset.forName("UTF-8"));
        return converter;
    }

    @Override
    public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
        super.configureMessageConverters(converters);
        converters.add(responseBodyConverter());
    }

    @Override
    public void configureContentNegotiation(ContentNegotiationConfigurer configurer) {
        configurer.favorPathExtension(false);
    }
}

數(shù)據(jù)庫及連接池配置

決定這一次試試Druid的監(jiān)控功能,所以給一下數(shù)據(jù)庫的配置:

## ——————————數(shù)據(jù)庫訪問配置——————————
spring.datasource.type=com.alibaba.druid.pool.DruidDataSource
spring.datasource.driver-class-name = com.mysql.jdbc.Driver
spring.datasource.url = jdbc:mysql://127.0.0.1:3306/blog?characterEncoding=UTF-8
spring.datasource.username = root
spring.datasource.password = 123456

# 下面為連接池的補充設(shè)置,應(yīng)用到上面所有數(shù)據(jù)源中
# 初始化大小,最小,最大
spring.datasource.druid.initial-size=5
spring.datasource.druid.min-idle=5
spring.datasource.druid.max-active=20
# 配置獲取連接等待超時的時間
spring.datasource.druid.max-wait=60000
# 配置間隔多久才進(jìn)行一次檢測,檢測需要關(guān)閉的空閑連接,單位是毫秒
spring.datasource.druid.time-between-eviction-runs-millis=60000
# 配置一個連接在池中最小生存的時間,單位是毫秒
spring.datasource.druid.min-evictable-idle-time-millis=300000
spring.datasource.druid.validation-query=SELECT 1 FROM DUAL
spring.datasource.druid.test-while-idle=true
spring.datasource.druid.test-on-borrow=false
spring.datasource.druid.test-on-return=false
# 打開PSCache,并且指定每個連接上PSCache的大小
spring.datasource.druid.pool-prepared-statements=true
spring.datasource.druid.max-pool-prepared-statement-per-connection-size=20
# 配置監(jiān)控統(tǒng)計攔截的filters,去掉后監(jiān)控界面sql無法統(tǒng)計,'wall'用于防火墻
spring.datasource.druid.filters=stat,wall,log4j

日志配置

在SpringBoot中其實已經(jīng)使用了Logback來作為默認(rèn)的日志框架,這是log4j作者推出的新一代日志框架,它效率更高、能夠適應(yīng)諸多的運行環(huán)境,同時天然支持SLF4J,在SpringBoot中我們無需再添加額外的依賴就能使用,這是因為在spring-boot-starter-web包中已經(jīng)有了該依賴了,所以我們只需要進(jìn)行配置使用就好了

第一步:創(chuàng)建logback-spring.xml

當(dāng)項目跑起來的時候,我們不可能還去看控制臺的輸出信息吧,所以我們需要把日志寫到文件里面,在網(wǎng)上找到一個例子(鏈接:http://tengj.top/2017/04/05/springboot7/

<?xml version="1.0" encoding="UTF-8"?>
<configuration scan="true" scanPeriod="60 seconds" debug="false">
    <contextName>logback</contextName>
    <!--自己定義一個log.path用于說明日志的輸出目錄-->
    <property name="log.path" value="/log/wmyskxz/"/>
    <!--輸出到控制臺-->
    <appender name="console" class="ch.qos.logback.core.ConsoleAppender">
        <!-- <filter class="ch.qos.logback.classic.filter.ThresholdFilter">
             <level>ERROR</level>
         </filter>-->
        <encoder>
            <pattern>%d{HH:mm:ss.SSS} %contextName [%thread] %-5level %logger{36} - %msg%n</pattern>
        </encoder>
    </appender>

    <!--輸出到文件-->
    <appender name="file" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <fileNamePattern>${log.path}/logback.%d{yyyy-MM-dd}.log</fileNamePattern>
        </rollingPolicy>
        <encoder>
            <pattern>%d{HH:mm:ss.SSS} %contextName [%thread] %-5level %logger{36} - %msg%n</pattern>
        </encoder>
    </appender>

    <root level="info">
        <appender-ref ref="console"/>
        <appender-ref ref="file"/>
    </root>

    <!-- logback為java中的包 -->
    <logger name="cn.wmyskxz.blog.controller"/>
</configuration>

在Spring Boot中你只要按照規(guī)則組織文件名,就能夠使得配置文件能夠被正確加載,并且官方推薦優(yōu)先使用帶有-spring的文件名作為日志的配置(如上面使用的logback-spring.xml,而不是logback.xml),滿足這樣的命名規(guī)范并且保證文件在src/main/resources下就好了;

第二步:重啟項目檢查是否成功

我們定義的目錄位置為/log/wmyskxz/,但是在項目的根目錄下并沒有發(fā)現(xiàn)這樣的目錄,反而是在當(dāng)前盤符的根目錄..不是很懂這個規(guī)則..總之是成功了的..

打開是密密麻麻一堆跟控制臺一樣的【info】級別的信息,因為這個系統(tǒng)本身就比較簡單,所以就沒有必要去搞什么文本切割之類的東西了,ok..日志算是配置完成;

實際測試了一下,上線之后肯定需要調(diào)整輸出級別的,不然日志文件就會特別大...

攔截器配置

我們需要對地址進(jìn)行攔截,對所有的/admin開頭的地址請求進(jìn)行攔截,因為這是后臺管理的默認(rèn)訪問地址開頭,這是必須進(jìn)行驗證之后才能訪問的地址,正如上面的RESTful APIs,這里包含了一些增加/刪除/更改/編輯一類的操作,而統(tǒng)統(tǒng)這些操作都是不能夠開放給用戶的操作,所以我們需要對這些地址進(jìn)行攔截:

第一步:創(chuàng)建User實體類

做驗證還是需要添加session,不然不好弄,所以我們還是得創(chuàng)建一個常規(guī)的實體:

public class User {
    private String username;
    private String password;

    /* getter and setter */
}

第二步:創(chuàng)建攔截器并繼承HandlerInterceptor接口

在【interceptor】包下新建一個【BackInterceptor】類并繼承HandlerInterceptor接口:

public class BackInterceptor implements HandlerInterceptor {

    private static String username = "wmyskxz";
    private static String password = "123456";

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        boolean flag = true;
        User user = (User) request.getSession().getAttribute("user");
        if (null == user) {
            flag = false;
        } else {
            // 對用戶賬號進(jìn)行驗證,是否正確
            if (user.getUsername().equals(username) && user.getPassword().equals(password)) {
                flag = true;
            } else {
                flag = false;
            }
        }
        return flag;
    }
}

在攔截器中,我們從session中取出了user,并判斷是否符合要求,這里我們直接寫死了(并沒有更改密碼的需求,但需要加密),而且我們并沒有做任何的跳轉(zhuǎn)操作,原因很簡單,根本就不需要跳轉(zhuǎn),因為訪問后臺的用戶只有我一個人,所以只需要我知道正確的登錄地址就可以了...

第三步:在配置類中復(fù)寫addInterceptors方法

剛才我們在設(shè)置編碼的時候自己創(chuàng)建了一個繼承自WebMvcConfigurerAdapter的設(shè)置類,我們需要復(fù)寫其中的addInterceptors方法來為我們的攔截器添加配置:

@Override
public void addInterceptors(InterceptorRegistry registry) {
    // addPathPatterns 用于添加攔截規(guī)則
    // excludePathPatterns 用戶排除攔截
    registry.addInterceptor(new BackInterceptor()).addPathPatterns("/admin/**").excludePathPatterns("/toLogin");
    super.addInterceptors(registry);
}
  • 說明:這個方法也很簡單,通過在addPathPatterns中添加攔截規(guī)則(這里設(shè)置攔截/admin開頭的所有地址),并通過excludePathPatterns來排除攔截的地址(這里為/toLogin,即登錄地址,到時候我可以弄得復(fù)雜隱蔽一點兒)

第四步:配置登錄頁面

以前我們在寫Spring MVC的時候,如果需要訪問一個頁面,必須要在Controller中添加一個方法跳轉(zhuǎn)到相應(yīng)的頁面才可以,但是在SpringBoot中增加了更加方便快捷的方法:

/**
 * 以前要訪問一個頁面需要先創(chuàng)建個Controller控制類,在寫方法跳轉(zhuǎn)到頁面
 * 在這里配置后就不需要那么麻煩了,直接訪問http://localhost:8080/toLogin就跳轉(zhuǎn)到login.html頁面了
 *
 * @param registry
 */
@Override
public void addViewControllers(ViewControllerRegistry registry) {
    registry.addViewController("/admin/login").setViewName("login.html");
    super.addViewControllers(registry);
}
  • 注意:login.html記得要放在【templates】下才會生效哦...(我試過使用login綁定視圖名不成功,只能寫全了...)

訪問日志記錄

上面我們設(shè)置了訪問限制的攔截器,對后臺訪問進(jìn)行了限制,這是攔截器的好處,我們同樣也使用攔截器對于訪問數(shù)量進(jìn)行一個統(tǒng)計

第一步:編寫前臺訪問攔截器

對照著數(shù)據(jù)庫的設(shè)計,我們需要保存的信息都從request對象中去獲取,然后保存到數(shù)據(jù)庫中即可,代碼也很簡單:

public class ForeInterceptor implements HandlerInterceptor {

    @Autowired
    SysService sysService;

    private SysLog sysLog = new SysLog();
    private SysView sysView = new SysView();

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {

        // 訪問者的IP
        String ip = request.getRemoteAddr();
        // 訪問地址
        String url = request.getRequestURL().toString();
        //得到用戶的瀏覽器名
        String userbrowser = BrowserUtil.getOsAndBrowserInfo(request);

        // 給SysLog增加字段
        sysLog.setIp(StringUtils.isEmpty(ip) ? "0.0.0.0" : ip);
        sysLog.setOperateBy(StringUtils.isEmpty(userbrowser) ? "獲取瀏覽器名失敗" : userbrowser);
        sysLog.setOperateUrl(StringUtils.isEmpty(url) ? "獲取URL失敗" : url);

        // 增加訪問量
        sysView.setIp(StringUtils.isEmpty(ip) ? "0.0.0.0" : ip);
        sysService.addView(sysView);

        return true;
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        HandlerMethod handlerMethod = (HandlerMethod) handler;
        Method method = handlerMethod.getMethod();

        // 保存日志信息
        sysLog.setRemark(method.getName());
        sysService.addLog(sysLog);
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {

    }
}
  • 注意:但是需要注意的是測試的時候別把攔截器開了(主要是postHandle方法中中無法強轉(zhuǎn)handler),不然不方便測試...

BrowserUtil是找的網(wǎng)上的一段代碼,直接黏貼復(fù)制放【util】包下就可以了:

/**
 * 用于從Request請求中獲取到客戶端的獲取操作系統(tǒng),瀏覽器及瀏覽器版本信息
 *
 * @author:wmyskxz
 * @create:2018-06-21-上午 8:40
 */
public class BrowserUtil {
    /**
     * 獲取操作系統(tǒng),瀏覽器及瀏覽器版本信息
     *
     * @param request
     * @return
     */
    public static String getOsAndBrowserInfo(HttpServletRequest request) {
        String browserDetails = request.getHeader("User-Agent");
        String userAgent = browserDetails;
        String user = userAgent.toLowerCase();

        String os = "";
        String browser = "";

        //=================OS Info=======================
        if (userAgent.toLowerCase().indexOf("windows") >= 0) {
            os = "Windows";
        } else if (userAgent.toLowerCase().indexOf("mac") >= 0) {
            os = "Mac";
        } else if (userAgent.toLowerCase().indexOf("x11") >= 0) {
            os = "Unix";
        } else if (userAgent.toLowerCase().indexOf("android") >= 0) {
            os = "Android";
        } else if (userAgent.toLowerCase().indexOf("iphone") >= 0) {
            os = "IPhone";
        } else {
            os = "UnKnown, More-Info: " + userAgent;
        }
        //===============Browser===========================
        if (user.contains("edge")) {
            browser = (userAgent.substring(userAgent.indexOf("Edge")).split(" ")[0]).replace("/", "-");
        } else if (user.contains("msie")) {
            String substring = userAgent.substring(userAgent.indexOf("MSIE")).split(";")[0];
            browser = substring.split(" ")[0].replace("MSIE", "IE") + "-" + substring.split(" ")[1];
        } else if (user.contains("safari") && user.contains("version")) {
            browser = (userAgent.substring(userAgent.indexOf("Safari")).split(" ")[0]).split("/")[0]
                    + "-" + (userAgent.substring(userAgent.indexOf("Version")).split(" ")[0]).split("/")[1];
        } else if (user.contains("opr") || user.contains("opera")) {
            if (user.contains("opera")) {
                browser = (userAgent.substring(userAgent.indexOf("Opera")).split(" ")[0]).split("/")[0]
                        + "-" + (userAgent.substring(userAgent.indexOf("Version")).split(" ")[0]).split("/")[1];
            } else if (user.contains("opr")) {
                browser = ((userAgent.substring(userAgent.indexOf("OPR")).split(" ")[0]).replace("/", "-"))
                        .replace("OPR", "Opera");
            }

        } else if (user.contains("chrome")) {
            browser = (userAgent.substring(userAgent.indexOf("Chrome")).split(" ")[0]).replace("/", "-");
        } else if ((user.indexOf("mozilla/7.0") > -1) || (user.indexOf("netscape6") != -1) ||
                (user.indexOf("mozilla/4.7") != -1) || (user.indexOf("mozilla/4.78") != -1) ||
                (user.indexOf("mozilla/4.08") != -1) || (user.indexOf("mozilla/3") != -1)) {
            browser = "Netscape-?";

        } else if (user.contains("firefox")) {
            browser = (userAgent.substring(userAgent.indexOf("Firefox")).split(" ")[0]).replace("/", "-");
        } else if (user.contains("rv")) {
            String IEVersion = (userAgent.substring(userAgent.indexOf("rv")).split(" ")[0]).replace("rv:", "-");
            browser = "IE" + IEVersion.substring(0, IEVersion.length() - 1);
        } else {
            browser = "UnKnown, More-Info: " + userAgent;
        }

        return os + "-" + browser;
    }
}

第二步:設(shè)置攔截地址

還是在剛才的配置類中,新增這么一條:

@Override
public void addInterceptors(InterceptorRegistry registry) {
    // addPathPatterns 用于添加攔截規(guī)則
    // excludePathPatterns 用戶排除攔截
    registry.addInterceptor(new BackInterceptor()).addPathPatterns("/admin/**").excludePathPatterns("/toLogin");
    registry.addInterceptor(getForeInterceptor()).addPathPatterns("/**").excludePathPatterns("/toLogin","/admin/**");
    super.addInterceptors(registry);
}

設(shè)置默認(rèn)錯誤頁面

在SpringBoot中,默認(rèn)的錯誤頁面比較丑(如下),所以我們可以自己改得稍微好看一點兒,具體的教程在這里:http://tengj.top/2018/05/16/springboot13/ ,我就搞前臺的時候再去弄了...


Service 層開發(fā)

這是糾結(jié)最久應(yīng)該怎么寫的,一開始我還準(zhǔn)備老老實實地利用MyBatis逆向工程生成的一堆東西去給每一個實體創(chuàng)建一個Service的,這樣其實就只是對Dao層進(jìn)行了一層不必要的封裝而已,然后通過分析其實主要的業(yè)務(wù)也就分成幾個:文章/評論/分類/日志瀏覽量這四個部分而已,所以創(chuàng)建這四個Service就好了;

比較神奇的事情是在網(wǎng)上找到一種通用Mapper的最佳實踐方法,整個人都驚了,“wtf?還可以這樣寫哦?”,資料如下:http://tengj.top/2017/12/20/springboot11/

emmmm..我們通過MyBatis的逆向工程,已經(jīng)很大程度上簡化了我們的開發(fā),因為在Dao層我們已經(jīng)免去了自己寫SQL語句,自己寫實體,自己寫XML映射文件的麻煩,但在Service層我們?nèi)匀粺o可避免的要寫一些類似功能的代碼,有沒有什么方法能把這些比較通用的方法給提取出來呢? 答案就在上面的鏈接中,oh,簡直太酷了...我決定在這里介紹一下...

通用接口開發(fā)

在Spring4中,由于支持了泛型注解,再結(jié)合通用Mapper,我們的想法得到了一個最佳的實踐方法,下面我們來講解一下:

第一步:創(chuàng)建通用接口

我們把一些常見的,通用的方法統(tǒng)一使用泛型封裝在一個通用接口之中:

/**
 * 通用接口
 *
 * @author: wmyskxz
 * @create: 2018年6月15日10:27:04
 */
public interface IService<T> {

    T selectByKey(Object key);

    int save(T entity);

    int delete(Object key);

    int updateAll(T entity);

    int updateNotNull(T entity);

    List<T> selectByExample(Object example);

}

第二步:實現(xiàn)通用接口類

/**
 * 通用Service
 *
 * @param <T>
 */
public abstract class BaseService<T> implements IService<T> {

    @Autowired
    protected Mapper<T> mapper;

    public Mapper<T> getMapper() {
        return mapper;
    }

    /**
     * 說明:根據(jù)主鍵字段進(jìn)行查詢,方法參數(shù)必須包含完整的主鍵屬性,查詢條件使用等號
     *
     * @param key
     * @return
     */
    @Override
    public T selectByKey(Object key) {
        return mapper.selectByPrimaryKey(key);
    }

    /**
     * 說明:保存一個實體,null的屬性也會保存,不會使用數(shù)據(jù)庫默認(rèn)值
     *
     * @param entity
     * @return
     */
    @Override
    public int save(T entity) {
        return mapper.insert(entity);
    }

    /**
     * 說明:根據(jù)主鍵字段進(jìn)行刪除,方法參數(shù)必須包含完整的主鍵屬性
     *
     * @param key
     * @return
     */
    @Override
    public int delete(Object key) {
        return mapper.deleteByPrimaryKey(key);
    }

    /**
     * 說明:根據(jù)主鍵更新實體全部字段,null值會被更新
     *
     * @param entity
     * @return
     */
    @Override
    public int updateAll(T entity) {
        return mapper.updateByPrimaryKey(entity);
    }

    /**
     * 根據(jù)主鍵更新屬性不為null的值
     *
     * @param entity
     * @return
     */
    @Override
    public int updateNotNull(T entity) {
        return mapper.updateByPrimaryKeySelective(entity);
    }

    /**
     * 說明:根據(jù)Example條件進(jìn)行查詢
     * 重點:這個查詢支持通過Example類指定查詢列,通過selectProperties方法指定查詢列
     *
     * @param example
     * @return
     */
    @Override
    public List<T> selectByExample(Object example) {
        return mapper.selectByExample(example);
    }
}

至此呢,我們的通用接口就開發(fā)完成了

第三步:使用通用接口

編寫好我們的通用接口之后,使用就變得很方便了,只需要繼承相應(yīng)的通用接口或者通用接口實現(xiàn)類,然后進(jìn)行簡單的封裝就行了,下面以SortInfo為例:

public interface SortInfoService extends IService<SortInfo> {
}
========================分割線========================
/**
 * 分類信息Service
 *
 * @author:wmyskxz
 * @create:2018-06-15-上午 11:14
 */
@Service
public class SortInfoServiceImpl extends BaseService<SortInfo> implements SortInfoService {
}

對應(yīng)到SortInfo的RESTful API設(shè)計,這樣簡單的繼承就能夠很好的支持,但是我們還是使用最原始的方式來創(chuàng)建吧...

Service接口申明

查了一些資料,問了一下實習(xí)公司的前輩老師,并且根據(jù)我們之前設(shè)計好的RESTful APIs,我們很有必要搞一個dto層用于前后端之間的數(shù)據(jù)交互,這一層主要是對數(shù)據(jù)庫的數(shù)據(jù)進(jìn)行一個封裝整合,也方便前后端的數(shù)據(jù)交互,所以我們首先就需要分析在dto層中應(yīng)該存在哪些數(shù)據(jù):

DTO層開發(fā)

對應(yīng)我們的業(yè)務(wù)邏輯和RESTful APIs,我大概弄了下面幾個Dto:

① ArticleDto:

該Dto封裝了文章的詳細(xì)信息,對應(yīng)RESTful API中的/api/article/{id}——通過文章ID獲取文章信息

/**
 * 文章信息類
 * 說明:關(guān)聯(lián)了tbl_article_info/tbl_article_content/tbl_article_category/tbl_category_info/
 * tbl_article_picture五張表的基礎(chǔ)字段
 *
 * @author:wmyskxz
 * @create:2018-06-19-下午 14:13
 */
public class ArticleDto {

    // tbl_article_info基礎(chǔ)字段
    private Long id;
    private String title;
    private String summary;
    private Boolean isTop;
    private Integer traffic;

    // tbl_article_content基礎(chǔ)字段
    private Long articleContentId;
    private String content;

    // tbl_category_info基礎(chǔ)字段
    private Long categoryId;
    private String categoryName;
    private Byte categoryNumber;

    // tbl_article_category基礎(chǔ)字段
    private Long articleCategoryId;

    // tbl_article_picture基礎(chǔ)字段
    private Long articlePictureId;
    private String pictureUrl;

    /* getter and setter */
}

②ArticleCommentDto:

該Dto封裝的事文章的評論信息,對應(yīng)/api/comment/article/{id}——通過文章ID獲取某一篇文章的全部評論信息

/**
 * 文章評論信息
 * 說明:關(guān)聯(lián)了tbl_comment和tbl_article_comment兩張表的信息
 *
 * @author:wmyskxz
 * @create:2018-06-19-下午 14:09
 */
public class ArticleCommentDto {
    // tbl_comment基礎(chǔ)字段
    private Long id;                // 評論id
    private String content;         // 評論內(nèi)容
    private String name;            // 用戶自定義的顯示名稱
    private String email;
    private String ip;

    // tbl_article_comment基礎(chǔ)字段
    private Long articleCommentId;  // tbl_article_comment主鍵
    private Long articleId;         // 文章ID

    /* getter and setter */
}

③ArticleCategoryDto:

該Dto是封裝了文章的一些分類信息,對應(yīng)/admin/category/{id}——獲取某一篇文章的分類信息

/**
 * 文章分類傳輸對象
 * 說明:關(guān)聯(lián)了tbl_article_category和tbl_category_info兩張表的數(shù)據(jù)
 *
 * @author:wmyskxz
 * @create:2018-06-20-上午 8:45
 */
public class ArticleCategoryDto {

    //  tbl_article_category表基礎(chǔ)字段
    private Long id;            // tbl_article_category表主鍵
    private Long categoryId;    // 分類信息ID
    private Long articleId;     // 文章ID

    // tbl_category_info表基礎(chǔ)字段
    private String name;        // 分類信息顯示名稱
    private Byte number;        // 該分類下對應(yīng)的文章數(shù)量

    /* getter and setter */
}

④ArticleWithPictureDto:

該Dto封裝了文章用于顯示的基本信息,對應(yīng)所有的獲取文章集合的RESful APIs

/**
 * 帶題圖信息的文章基礎(chǔ)信息分裝類
 *
 * @author:wmyskxz
 * @create:2018-06-19-下午 14:53
 */
public class ArticleWithPictureDto {
    // tbl_article_info基礎(chǔ)字段
    private Long id;
    private String title;
    private String summary;
    private Boolean isTop;
    private Integer traffic;

    // tbl_article_picture基礎(chǔ)字段
    private Long articlePictureId;
    private String pictureUrl;

    /* getter and setter */
}

Service接口開發(fā)

Service層其實就是對我們業(yè)務(wù)的一個封裝,所以有了RESTful APIs文檔,我們可以很輕易的寫出對應(yīng)的業(yè)務(wù)模塊:

文章Service

/**
 * 文章Service
 * 說明:ArticleInfo里面封裝了picture/content/category等信息
 */
public interface ArticleService {

    void addArticle(ArticleDto articleDto);

    void deleteArticleById(Long id);

    void updateArticle(ArticleDto articleDto);

    void updateArticleCategory(Long articleId, Long categoryId);

    ArticleDto getOneById(Long id);

    ArticlePicture getPictureByArticleId(Long id);

    List<ArticleWithPictureDto> listAll();

    List<ArticleWithPictureDto> listByCategoryId(Long id);

    List<ArticleWithPictureDto> listLastest();
}

分類Service

/**
 * 分類Service
 */
public interface CategoryService {
    void addCategory(CategoryInfo categoryInfo);

    void deleteCategoryById(Long id);

    void updateCategory(CategoryInfo categoryInfo);

    void updateArticleCategory(ArticleCategory articleCategory);

    CategoryInfo getOneById(Long id);

    List<CategoryInfo> listAllCategory();

    ArticleCategoryDto getCategoryByArticleId(Long id);
}

留言Service

/**
 * 留言的Service
 */
public interface CommentService {
    void addComment(Comment comment);

    void addArticleComment(ArticleCommentDto articleCommentDto);

    void deleteCommentById(Long id);

    void deleteArticleCommentById(Long id);

    List<Comment> listAllComment();

    List<ArticleCommentDto> listAllArticleCommentById(Long id);
}

系統(tǒng)Service

/**
 * 日志/訪問統(tǒng)計等系統(tǒng)相關(guān)Service
 */
public interface SysService {
    void addLog(SysLog sysLog);

    void addView(SysView sysView);

    int getLogCount();

    int getViewCount();

    List<SysLog> listAllLog();

    List<SysView> listAllView();
}

Controller 層開發(fā)

Controller層簡單理解的話,就是用來獲取數(shù)據(jù)的,所以只要Service層開發(fā)好了Controller層就很容易,就不多說了,只是我們可以把一些公用的東西放到一個BaseController中,比如引入Service:

/**
 * 基礎(chǔ)控制器
 *
 * @author:wmyskxz
 * @create:2018-06-19-上午 11:25
 */
public class BaseController {
    @Autowired
    ArticleService articleService;
    @Autowired
    CommentService commentService;
    @Autowired
    CategoryService categoryService;
}

然后前后臺的控制器只需要繼承該類就行了,這樣的方式非常值得借鑒的,只是因為這個系統(tǒng)比較簡單,所以這個BaseController,我看過一些源碼,可以在里面弄一個通用的用于返回數(shù)據(jù)的方法,比如分頁數(shù)據(jù)/錯誤信息之類的;


記錄坑

1)MyBatis中Text類型的坑

按照《阿里手冊》(簡稱)上所規(guī)范的那樣,我把文章的content單獨弄成了一張表并且將這個“可能很長”的字段的類型設(shè)置成了text類型,但是MyBatis逆向工程自動生成的時候,卻把這個text類型的字段單獨給列了出去,即在生成的xml中多出了一個<resultMap>,標(biāo)識id為ResultMapWithBLOBs,MyBatis這樣做可能的原因還是怕這個字段太長影響前面的字段查詢吧,但是操作這樣的LONGVARCHAR類型的字段MyBatis好像并沒有集成很好,所以想要很好的操作還是需要給它弄成VARCHAR類型才行;

在generatorConfig.xml中配置生成字段的時候加上這樣一句話就好了:

<table domainObjectName="ArticleContent" tableName="tbl_article_content">  
    <columnOverride column="content" javaType="java.lang.String" jdbcType="VARCHAR" />  
</table>  

2)攔截器中Service注入為null的坑

在編寫前臺攔截器的時候,我使用@Autowired注解自動注入了SysService系統(tǒng)服務(wù)Service,但是卻報nullpointer的錯,發(fā)現(xiàn)是沒有自動注入上,SysService為空..這是為什么呢?排除掉注解沒有識別或者沒有給Service添加上注解的可能性之后,我發(fā)現(xiàn)好像是攔截器攔截的時候Service并沒有創(chuàng)建成功造成的,參考這篇文章:https://blog.csdn.net/slgxmh/article/details/51860278,成功解決問題:

@Bean
public HandlerInterceptor getForeInterceptor() {
    return new ForeInterceptor();
}

@Override
public void addInterceptors(InterceptorRegistry registry) {
    // addPathPatterns 用于添加攔截規(guī)則
    // excludePathPatterns 用戶排除攔截
    registry.addInterceptor(new BackInterceptor()).addPathPatterns("/admin/**").excludePathPatterns("/toLogin");
   registry.addInterceptor(getForeInterceptor()).addPathPatterns("/**").excludePathPatterns("/toLogin", "/admin/**");
    super.addInterceptors(registry);
}

其實就是添加上@Bean注解讓ForeInterceptor提前加載;

3)數(shù)據(jù)庫sys_log表中operate_by字段的坑

當(dāng)時設(shè)計表的時候,就只是單純的想要保存一下用戶使用的瀏覽器是什么,其實當(dāng)時并不知道應(yīng)該怎么獲取獲取到的東西又是什么,只是覺得保存瀏覽器20個字段夠了,但后來發(fā)現(xiàn)這是很蠢萌的...所以不得不調(diào)整數(shù)據(jù)庫的字段長度,好在只需要單方面調(diào)整數(shù)據(jù)庫的字段長度就好了:

4)保存文章的方式的坑

因為我想要在數(shù)據(jù)庫中保存的是md源碼,而返回前臺前端希望的是直接拿到html代碼,這樣就能很方便的輸出了,所以這要怎么做呢?找到一篇參考文章:https://my.oschina.net/u/566591/blog/1535380

我們不要搞那么復(fù)雜的封裝,只要簡單弄一個工具類就可以了,在【util】包下新建一個【Markdown2HtmlUtil】:

/**
 * Markdown轉(zhuǎn)Html工具類
 *
 * @author:wmyskxz
 * @create:2018-06-21-上午 10:09
 */
public class Markdown2HtmlUtil {
    /**
     * 將markdown源碼轉(zhuǎn)換成html返回
     *
     * @param markdown md源碼
     * @return html代碼
     */
    public static String markdown2html(String markdown) {
        MutableDataSet options = new MutableDataSet();
        options.setFrom(ParserEmulationProfile.MARKDOWN);
        options.set(Parser.EXTENSIONS, Arrays.asList(new Extension[]{TablesExtension.create()}));
        Parser parser = Parser.builder(options).build();
        HtmlRenderer renderer = HtmlRenderer.builder(options).build();

        Node document = parser.parse(markdown);
        return renderer.render(document);
    }
}

使用也很簡單,只需要在獲取一篇文章的時候把ArticleDto里面的md源碼轉(zhuǎn)成html代碼再返回給前臺就好了:

/**
 * 通過文章的ID獲取對應(yīng)的文章信息
 *
 * @param id
 * @return 自己封裝好的文章信息類
 */
@ApiOperation("通過文章ID獲取文章信息")
@GetMapping("article/{id}")
public ArticleDto getArticleById(@PathVariable Long id) {
    ArticleDto articleDto = articleService.getOneById(id);
    articleDto.setContent(Markdown2HtmlUtil.markdown2html(articleDto.getContent()));
    return articleDto;
}

樣式之類的交給前臺就好了,搞定...


簡單總結(jié)

關(guān)于統(tǒng)計啊日志類的Controller還沒有開發(fā),RESful API也沒有設(shè)計,這里就先發(fā)布文章了,因為好像時間有點緊,后臺的頁面暫時可能開發(fā)不完,準(zhǔn)備直接開始前臺頁面顯示的開發(fā)(主要是自己對前端不熟悉還要學(xué)習(xí)..),這里對后臺進(jìn)行一個簡單的總結(jié):

其實發(fā)現(xiàn)當(dāng)數(shù)據(jù)庫設(shè)計好了,RESful APIs設(shè)計好了之后,后臺的任務(wù)變得非常明確,開發(fā)起來也就思路很清晰了,只是自己還是缺少一些必要的經(jīng)驗,如對一些通用方法的抽象/層與層之間數(shù)據(jù)交互的典型設(shè)計之類的東西,特別是一些安全方面的東西,網(wǎng)上的資料也比較少一些,也是自己需要學(xué)習(xí)的地方;

歡迎轉(zhuǎn)載,轉(zhuǎn)載請注明出處!
簡書ID:@我沒有三顆心臟
github:wmyskxz
歡迎關(guān)注公眾微信號:wmyskxz_javaweb
分享自己的Java Web學(xué)習(xí)之路以及各種Java學(xué)習(xí)資料

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 我曾經(jīng)跟別人說,這座城市是我的福地。 初入職場時近一個月封閉式培訓(xùn)是在這里。接連兩次重要的資格培訓(xùn)是在這里,并順利...
    江湖皆是好去處閱讀 579評論 0 0
  • 這是我連續(xù)寫作的第66篇文章,推薦一本好書。 讀一本46萬字的書,大概要花費多長的時間?一天?半天?當(dāng)然速度也取決...
    京珂大師姐閱讀 406評論 0 7

友情鏈接更多精彩內(nèi)容