配置亂碼原因和對應解決法「springboot配置tomcat配置亂碼」

一些設計上的調整

在查了一些資料和吸收了一些評論給出良好的建議之後,我覺得有必要對一些設計進行一些調整:

  • 1)資料庫:命名應該更加規範,比如表示分類最好用category而不是sort,表示評論最好用comment而不是message;
  • 2)RESful APIs:在準備著手開始寫後台的時候就已經發現,本來想的是凡是以/api開頭的都是暴露出來給前端用的,凡是以/admin開頭的都是給後台使用的地址,但是意外的沒有設計後天的API也把一些刪除命令暴露給了前端,這就不好了重新設計設計;
  • 3)命名規範的問題:因為使用MyBatis逆向工程自動生成的時候,配置了一個useActualColumnNames使用表真正名稱的東西,所以整得來生成POJO類基礎欄位有下劃線,看著著實有點不爽,把它給幹掉幹掉…;

資料庫調整

把欄位規範了一下,並且刪除了分類下是否有效的欄位(感覺這種不經常變換的欄位留著也沒啥用乾脆幹掉..),所以調整為了下面這個樣子(調整欄位已標紅):

基於SpringBoot的個人博客搭建(二):後端開發

然後重新使用生成器自動生成對應的文件,注意記得修改generatorConfig.xml文件中對應的資料庫名稱;

創建和修改時間的欄位設置

通過查資料發現其實我們可以通過直接設置資料庫來自動更新我們的modified_by欄位,並且可以像設置初始值那樣給create_by和modified_by兩個欄位以當前時間戳設置默認值,這裡具體以tbl_article_info這張表為例:

CREATE TABLE `tbl_article_info` (
 `id` bigint(40) NOT NULL AUTO_INCREMENT COMMENT '主鍵',
 `title` varchar(50) NOT NULL DEFAULT '' COMMENT '文章標題',
 `summary` varchar(300) NOT NULL DEFAULT '' COMMENT '文章簡介,默認100個漢字以內',
 `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 '創建時間',
 `modified_by` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '修改日期',
 PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

我們通過設置DEFAULT為CURRENT_TIMESTAMP,然後給modified_by欄位多添加了一句ON UPDATE CURRENT_TIMESTAMP,這樣它就會在更新的時候將該欄位的值設置為更新時間,這樣我們就不用在後台關心這兩個值了,也少寫了一些代碼(其實是寫代碼的時候發現可以這樣偷懶..hhh…);

RESTful APIs重新設計

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

後台Restful APIs:

基於SpringBoot的個人博客搭建(二):後端開發

前台開放RESful APIs:

基於SpringBoot的個人博客搭建(二):後端開發

這些API只是用來和前端交互的介面,另外一些關於日誌啊之類的東西就直接在後台寫就行了,OK,這樣就爽多了,可以開始著手寫代碼了;

基本配置

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

基於SpringBoot的個人博客搭建(二):後端開發

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

編碼設置

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

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

但是這樣設置之後,在後面的使用當中還是會發生提交表單時中文亂碼的問題,在網上搜索了一下找到了解決方法,新建一個【config】包創建下面這樣一個配置類:

@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);
 }
}

資料庫及連接池配置

決定這一次試試Druid的監控功能,所以給一下資料庫的配置:

## ——————————資料庫訪問配置——————————
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
# 下面為連接池的補充設置,應用到上面所有數據源中
# 初始化大小,最小,最大
spring.datasource.druid.initial-size=5
spring.datasource.druid.min-idle=5
spring.datasource.druid.max-active=20
# 配置獲取連接等待超時的時間
spring.datasource.druid.max-wait=60000
# 配置間隔多久才進行一次檢測,檢測需要關閉的空閑連接,單位是毫秒
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
# 配置監控統計攔截的filters,去掉後監控界面sql無法統計,'wall'用於防火牆
spring.datasource.druid.filters=stat,wall,log4j

日誌配置

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

第一步:創建logback-spring.xml

當項目跑起來的時候,我們不可能還去看控制台的輸出信息吧,所以我們需要把日誌寫到文件裡面,在網上找到一個例子(鏈接:
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中你只要按照規則組織文件名,就能夠使得配置文件能夠被正確載入,並且官方推薦優先使用帶有-spring的文件名作為日誌的配置(如上面使用的logback-spring.xml,而不是logback.xml),滿足這樣的命名規範並且保證文件在src/main/resources下就好了;

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

我們定義的目錄位置為/log/wmyskxz/,但是在項目的根目錄下並沒有發現這樣的目錄,反而是在當前盤符的根目錄..不是很懂這個規則..總之是成功了的..

基於SpringBoot的個人博客搭建(二):後端開發

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

實際測試了一下,上線之後肯定需要調整輸出級別的,不然日誌文件就會特別大…

攔截器配置

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

第一步:創建User實體類

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

public class User {
 private String username;
 private String password;
 /* getter and setter */
}

第二步:創建攔截器並繼承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 {
 // 對用戶賬號進行驗證,是否正確
 if (user.getUsername().equals(username) && user.getPassword().equals(password)) {
 flag = true;
 } else {
 flag = false;
 }
 }
 return flag;
 }
}

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

第三步:在配置類中複寫addInterceptors方法

剛才我們在設置編碼的時候自己創建了一個繼承自WebMvcConfigurerAdapter的設置類,我們需要複寫其中的addInterceptors方法來為我們的攔截器添加配置:

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

第四步:配置登錄頁面

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

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

訪問日誌記錄

上面我們設置了訪問限制的攔截器,對後台訪問進行了限制,這是攔截器的好處,我們同樣也使用攔截器對於訪問數量進行一個統計

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

對照著資料庫的設計,我們需要保存的信息都從request對象中去獲取,然後保存到資料庫中即可,代碼也很簡單:

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方法中中無法強轉handler),不然不方便測試…

BrowserUtil是找的網上的一段代碼,直接黏貼複製放【util】包下就可以了:

/**
 * 用於從Request請求中獲取到客戶端的獲取操作系統,瀏覽器及瀏覽器版本信息
 *
 * @author:wmyskxz
 * @create:2018-06-21-上午 8:40
 */
public class BrowserUtil {
 /**
 * 獲取操作系統,瀏覽器及瀏覽器版本信息
 *
 * @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;
 }
}

第二步:設置攔截地址

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

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

設置默認錯誤頁面

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


Service 層開發

這是糾結最久應該怎麼寫的,一開始我還準備老老實實地利用MyBatis逆向工程生成的一堆東西去給每一個實體創建一個Service的,這樣其實就只是對Dao層進行了一層不必要的封裝而已,然後通過分析其實主要的業務也就分成幾個:文章/評論/分類/日誌瀏覽量這四個部分而已,所以創建這四個Service就好了;

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

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

通用介面開發

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

第一步:創建通用介面

我們把一些常見的,通用的方法統一使用泛型封裝在一個通用介面之中:

/**
 * 通用介面
 *
 * @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);
}

第二步:實現通用介面類

/**
 * 通用Service
 *
 * @param <T>
 */
public abstract class BaseService<T> implements IService<T> {
 @Autowired
 protected Mapper<T> mapper;
 public Mapper<T> getMapper() {
 return mapper;
 }
 /**
 * 說明:根據主鍵欄位進行查詢,方法參數必須包含完整的主鍵屬性,查詢條件使用等號
 *
 * @param key
 * @return
 */
 @Override
 public T selectByKey(Object key) {
 return mapper.selectByPrimaryKey(key);
 }
 /**
 * 說明:保存一個實體,null的屬性也會保存,不會使用資料庫默認值
 *
 * @param entity
 * @return
 */
 @Override
 public int save(T entity) {
 return mapper.insert(entity);
 }
 /**
 * 說明:根據主鍵欄位進行刪除,方法參數必須包含完整的主鍵屬性
 *
 * @param key
 * @return
 */
 @Override
 public int delete(Object key) {
 return mapper.deleteByPrimaryKey(key);
 }
 /**
 * 說明:根據主鍵更新實體全部欄位,null值會被更新
 *
 * @param entity
 * @return
 */
 @Override
 public int updateAll(T entity) {
 return mapper.updateByPrimaryKey(entity);
 }
 /**
 * 根據主鍵更新屬性不為null的值
 *
 * @param entity
 * @return
 */
 @Override
 public int updateNotNull(T entity) {
 return mapper.updateByPrimaryKeySelective(entity);
 }
 /**
 * 說明:根據Example條件進行查詢
 * 重點:這個查詢支持通過Example類指定查詢列,通過selectProperties方法指定查詢列
 *
 * @param example
 * @return
 */
 @Override
 public List<T> selectByExample(Object example) {
 return mapper.selectByExample(example);
 }
}

至此呢,我們的通用介面就開發完成了

第三步:使用通用介面

編寫好我們的通用介面之後,使用就變得很方便了,只需要繼承相應的通用介面或者通用介面實現類,然後進行簡單的封裝就行了,下面以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 {
}

對應到SortInfo的RESTful API設計,這樣簡單的繼承就能夠很好的支持,但是我們還是使用最原始的方式來創建吧

Service介面申明

查了一些資料,問了一下實習公司的前輩老師,並且根據我們之前設計好的RESTful APIs,我們很有必要搞一個dto層用於前後端之間的數據交互,這一層主要是對資料庫的數據進行一個封裝整合,也方便前後端的數據交互,所以我們首先就需要分析在dto層中應該存在哪些數據:

DTO層開發

對應我們的業務邏輯和RESTful APIs,我大概弄了下面幾個Dto:

① ArticleDto:

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

/**
 * 文章信息類
 * 說明:關聯了tbl_article_info/tbl_article_content/tbl_article_category/tbl_category_info/
 * tbl_article_picture五張表的基礎欄位
 *
 * @author:wmyskxz
 * @create:2018-06-19-下午 14:13
 */
public class ArticleDto {
 // tbl_article_info基礎欄位
 private Long id;
 private String title;
 private String summary;
 private Boolean isTop;
 private Integer traffic;
 // tbl_article_content基礎欄位
 private Long articleContentId;
 private String content;
 // tbl_category_info基礎欄位
 private Long categoryId;
 private String categoryName;
 private Byte categoryNumber;
 // tbl_article_category基礎欄位
 private Long articleCategoryId;
 // tbl_article_picture基礎欄位
 private Long articlePictureId;
 private String pictureUrl;
 /* getter and setter */
}

②ArticleCommentDto:

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

/**
 * 文章評論信息
 * 說明:關聯了tbl_comment和tbl_article_comment兩張表的信息
 *
 * @author:wmyskxz
 * @create:2018-06-19-下午 14:09
 */
public class ArticleCommentDto {
 // tbl_comment基礎欄位
 private Long id; // 評論id
 private String content; // 評論內容
 private String name; // 用戶自定義的顯示名稱
 private String email;
 private String ip;
 // tbl_article_comment基礎欄位
 private Long articleCommentId; // tbl_article_comment主鍵
 private Long articleId; // 文章ID
 /* getter and setter */
}

③ArticleCategoryDto:

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

/**
 * 文章分類傳輸對象
 * 說明:關聯了tbl_article_category和tbl_category_info兩張表的數據
 *
 * @author:wmyskxz
 * @create:2018-06-20-上午 8:45
 */
public class ArticleCategoryDto {
 // tbl_article_category表基礎欄位
 private Long id; // tbl_article_category表主鍵
 private Long categoryId; // 分類信息ID
 private Long articleId; // 文章ID
 // tbl_category_info表基礎欄位
 private String name; // 分類信息顯示名稱
 private Byte number; // 該分類下對應的文章數量
 /* getter and setter */
}

④ArticleWithPictureDto:

該Dto封裝了文章用於顯示的基本信息,對應所有的獲取文章集合的RESful APIs

/**
 * 帶題圖信息的文章基礎信息分裝類
 *
 * @author:wmyskxz
 * @create:2018-06-19-下午 14:53
 */
public class ArticleWithPictureDto {
 // tbl_article_info基礎欄位
 private Long id;
 private String title;
 private String summary;
 private Boolean isTop;
 private Integer traffic;
 // tbl_article_picture基礎欄位
 private Long articlePictureId;
 private String pictureUrl;
 /* getter and setter */
}

Service介面開發

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

文章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);
}

系統Service

/**
 * 日誌/訪問統計等系統相關Service
 */
public interface SysService {
 void addLog(SysLog sysLog);
 void addView(SysView sysView);
 int getLogCount();
 int getViewCount();
 List<SysLog> listAllLog();
 List<SysView> listAllView();
}

Controller 層開發

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

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

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


記錄坑

1)MyBatis中Text類型的坑

按照《阿里手冊》(簡稱)上所規範的那樣,我把文章的content單獨弄成了一張表並且將這個「可能很長」的欄位的類型設置成了text類型,但是MyBatis逆向工程自動生成的時候,卻把這個text類型的欄位單獨給列了出去,即在生成的xml中多出了一個<resultMap>,標識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系統服務Service,但是卻報nullpointer的錯,發現是沒有自動注入上,SysService為空..這是為什麼呢?排除掉註解沒有識別或者沒有給Service添加上註解的可能性之後,我發現好像是攔截器攔截的時候Service並沒有創建成功造成的,參考這篇文章:
https://blog.csdn.net/slgxmh/article/details/51860278,成功解決問題:

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

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

3)資料庫sys_log表中operate_by欄位的坑

當時設計表的時候,就只是單純的想要保存一下用戶使用的瀏覽器是什麼,其實當時並不知道應該怎麼獲取獲取到的東西又是什麼,只是覺得保存瀏覽器20個欄位夠了,但後來發現這是很蠢萌的…所以不得不調整資料庫的欄位長度,好在只需要單方面調整資料庫的欄位長度就好了:

基於SpringBoot的個人博客搭建(二):後端開發

4)保存文章的方式的坑

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

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

/**
 * Markdown轉Html工具類
 *
 * @author:wmyskxz
 * @create:2018-06-21-上午 10:09
 */
public class Markdown2HtmlUtil {
 /**
 * 將markdown源碼轉換成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源碼轉成html代碼再返回給前台就好了:

/**
 * 通過文章的ID獲取對應的文章信息
 *
 * @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;
}

樣式之類的交給前台就好了,搞定…


簡單總結

關於統計啊日誌類的Controller還沒有開發,RESful API也沒有設計,這裡就先發布文章了,因為好像時間有點緊,後台的頁面暫時可能開發不完,準備直接開始前台頁面顯示的開發(主要是自己對前端不熟悉還要學習..),這裡對後台進行一個簡單的總結:

其實發現當資料庫設計好了,RESful APIs設計好了之後,後台的任務變得非常明確,開發起來也就思路很清晰了,只是自己還是缺少一些必要的經驗,如對一些通用方法的抽象/層與層之間數據交互的典型設計之類的東西,特別是一些安全方面的東西,網上的資料也比較少一些,也是自己需要學習的地方;

原創文章,作者:投稿專員,如若轉載,請註明出處:https://www.506064.com/zh-tw/n/281289.html

(0)
打賞 微信掃一掃 微信掃一掃 支付寶掃一掃 支付寶掃一掃
投稿專員的頭像投稿專員
上一篇 2024-12-21 13:17
下一篇 2024-12-21 13:17

相關推薦

發表回復

登錄後才能評論