前言:在之前,我們已經完成了項目的基本准備,那么就可以開始后台開發了,突然又想到一個問題,就是准備的時候只是設計了前台的RESTful APIs,但是后台管理我們同樣也是需要API的,那么就在這一篇里面一起實現了吧...
- 前序文章鏈接:SpringBoot技術棧搭建個人博客【項目准備】:https://www.jianshu.com/p/0293368fe750
一些設計上的調整
在查了一些資料和吸收了一些評論給出良好的建議之后,我覺得有必要對一些設計進行一些調整:
- 1)數據庫:命名應該更加規范,比如表示分類最好用category而不是sort,表示評論最好用comment而不是message;
- 2)RESful APIs:在准備着手開始寫后台的時候就已經發現,本來想的是凡是以
/api
開頭的都是暴露出來給前端用的,凡是以/admin
開頭的都是給后台使用的地址,但是意外的沒有設計后天的API也把一些刪除命令暴露給了前端,這就不好了重新設計設計; - 3)命名規范的問題:因為使用MyBatis逆向工程自動生成的時候,配置了一個
useActualColumnNames
使用表真正名稱的東西,所以整得來生成POJO類基礎字段有下划線,看着着實有點不爽,把它給干掉干掉...;
數據庫調整
把字段規范了一下,並且刪除了分類下是否有效的字段(感覺這種不經常變換的字段留着也沒啥用干脆干掉..),所以調整為了下面這個樣子(調整字段已標紅):
然后重新使用生成器自動生成對應的文件,注意記得修改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:
前台開放RESful APIs:
這些API只是用來和前端交互的接口,另外一些關於日志啊之類的東西就直接在后台寫就行了,OK,這樣就爽多了,可以開始着手寫代碼了;
基本配置
隨着配置內容的增多,我逐漸的想要放棄.yml的配置文件,主要的一點是這東西不好對內容進行分類(下圖是簡單配置了一些基本文件后的.yml和.properties文件的對比)..
最后還是用回.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/
,但是在項目的根目錄下並沒有發現這樣的目錄,反而是在當前盤符的根目錄..不是很懂這個規則..總之是成功了的..
打開是密密麻麻一堆跟控制台一樣的【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個字段夠了,但后來發現這是很蠢萌的...所以不得不調整數據庫的字段長度,好在只需要單方面調整數據庫的字段長度就好了:
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設計好了之后,后台的任務變得非常明確,開發起來也就思路很清晰了,只是自己還是缺少一些必要的經驗,如對一些通用方法的抽象/層與層之間數據交互的典型設計之類的東西,特別是一些安全方面的東西,網上的資料也比較少一些,也是自己需要學習的地方;
歡迎轉載,轉載請注明出處!
簡書ID:@我沒有三顆心臟
github:wmyskxz
歡迎關注公眾微信號:wmyskxz_javaweb
分享自己的Java Web學習之路以及各種Java學習資料