
前言:在之前,我們已經(jīng)完成了項目的基本準(zhǔn)備,那么就可以開始后臺開發(fā)了,突然又想到一個問題,就是準(zhǔn)備的時候只是設(shè)計了前臺的RESTful APIs,但是后臺管理我們同樣也是需要API的,那么就在這一篇里面一起實現(xiàn)了吧...
- 前序文章鏈接:SpringBoot技術(shù)棧搭建個人博客【項目準(zhǔn)備】:http://www.itdecent.cn/p/0293368fe750
一些設(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è)置DEFAULT為CURRENT_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í)資料