一、概述
經過HelloWorld示例(Spring Boot 快速入門(上)HelloWorld示例)( Spring Boot 快速入門 詳解 HelloWorld示例詳解)兩篇的學習和練習,相信你已經知道了Spring Boot是如此的簡單,但又有不少疑惑,那么多注解如何記住,他的生態怎么樣,緩存、NoSQL、定時器、郵件發送等細節功能如何處理。
如果你覺得一篇一篇看文章學習太耗時間,你看這篇就夠啦,如果你覺得這篇太長,可以跳過本章看其他章節。
本章是一個文章發布管理系統綜合示例,主要以操作數據庫、集成權限為主功能來實現Spring Boot周邊核心功能。主要包括了
本章實現的功能
1、實現Thymeleaf模板
2、實現Rest Api
3、實現基於Shiro的權限登錄
4、實現基於Mybatis的增刪改查
5、實現基於ehcache的緩存
6、實現日志輸出
7、實現全局配置
同時本章也向讀者提供如何設計一個系統的思路。
通常我們編寫一個小系統需要
1、需求分析:這里簡單描述要演練什么樣的系統
2、系統設計:包括了數據庫和程序分層設計
3、編碼:這里指編碼
4、測試:這里只包括單元測試
5、上線:這里指運行代碼
二、需求分析
本章以開發一個文章發布管理系統為假想的需求,涉及到的功能
1.有一個管理員可以登錄系統發布文章
2.可以發布文章、編輯文字、刪除文章
3.有一個文章列表
這是個典型的基於數據庫驅動的信息系統,使用spring boot+mysql即可開發。
三、系統設計
本章需要演練的內容,實際上是一個小型的信息管理系統(文章發布管理系統),有權限、有增刪改查、有緩存、有日志、有數據庫等。已經完全具備一個信息系統應有的功能。
針對此類演練的示例,我們也應該從標准的項目實戰思維來演練,而不能上來就開始新建項目、貼代碼等操作。
3.1 分層架構

經典的三層、展示層、服務層、數據庫訪問層。
所以在項目結構中我們可以設計成
3.2 數據庫設計
本次只是為了演示相關技術,所以采用單表設計,就設計一個t_article表,用戶名與密碼采用固定的。數據庫設計盡量符合相關標准(本文中已小寫下滑線來命名字段)
數據庫 article
1)表 t_article設計

2)創建Table語句
SET NAMES utf8mb4; SET FOREIGN_KEY_CHECKS = 0; -- ---------------------------- -- Table structure for t_article -- ---------------------------- DROP TABLE IF EXISTS `t_article`; CREATE TABLE `t_article` ( `id` int(11) NOT NULL AUTO_INCREMENT, `title` varchar(255) NOT NULL, `content` varchar(255) NOT NULL, `post_time` datetime NOT NULL, `post_status` int(11) NOT NULL, `create_by` datetime NOT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; SET FOREIGN_KEY_CHECKS = 1;
四、編碼與測試
程序的編碼應在整個設計中占到20%的工作量,有人說,那么那些復雜的業務、算法難道不花時間,復雜業務模型、復雜算法應該在系統設計階段去作為關鍵技術去攻克。不要等到編碼了,才去慢慢做。
編碼與測試我們可以經歷一些標准的路徑。
1、創建項目,建立適合的項目目錄
2、整合mybatis建立數據庫訪問層並測試
3、編寫service服務層
4、編寫應用層
5、整合thymeleaf編寫前端
6、給系統加入Shiro權限認證
7、給系統加入logging日志
8、給系統加入緩存
9、給系統加入完整的測試代碼
4.1 項目結構(復習使用IDEA創建項目)
4.1.1 使用IDEA創建項目
使用IDEA(本教程之后都使用IDEA來創建)創建名為 springstudy的項目
1)File>New>Project,如下圖選擇Spring Initializr 然后點擊 【Next】下一步
2)填寫GroupId(包名)、Artifact(項目名) ,本項目中 GroupId=com.fishpro Artiface=springstudy,這個步驟跟HelloWorld實例是一樣的

3)選擇依賴,我們選擇Web
注:也可以使用HelloWorld示例項目,Copy一份,來做。
4.1.2 初始化項目結構
在springstudy包名下增加包名
1)controller mvc控制層
2)dao mybatis的數據庫訪問層
3)domain 實體類對應數據庫字段
4)service 服務層
impl 服務實現

4.1.3 application.yml
個人習慣使用yml格式配置文件(縮進)
直接修改application.properties改為 application.yml
4.1.4 指定程序端口為8991
在application.yml中輸入
server: port: 8991
4.2 增加Mybatis支持,編寫數據庫訪問代碼
4.2.1 編輯Pom.xml 增加依賴
本章使用mybatis和阿里巴巴的driud連接池來鏈接操作數據庫
在pom.xml中增加依賴如下,注意有4個依賴引入,分別是mysql鏈接支持、jdbc支持、druid的alibaba連接池支持、mybatis支持。
<!--jdbc數據庫支持-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<!--druid -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.0.28</version>
</dependency>
<!--mybatis -->
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.4.4</version>
</dependency>
如果依賴未自動導入,點擊右下方 Import Changes 即可。
4.2.2 com.alibaba.druid連接池配置
(中文文檔 https://github.com/alibaba/druid/wiki/%E5%B8%B8%E8%A7%81%E9%97%AE%E9%A2%98)
本章只是演練(配置、使用),不說明具體功能說明及配置含義。
1)在resouces\application.yml 配置Druid的應用程序配置
spring:
datasource:
type: com.alibaba.druid.pool.DruidDataSource
driverClassName: com.mysql.jdbc.Driver
url: jdbc:mysql://localhost:3306/demo_article?useSSL=false&useUnicode=true&characterEncoding=utf8
#mysql用戶名
username: root
#mysql密碼
password: 123
#初始化線程池數量
initialSize: 1
#空閑連接池的大小
minIdle: 3
#最大激活量
maxActive: 20
# 配置獲取連接等待超時的時間
maxWait: 60000
# 配置間隔多久才進行一次檢測,檢測需要關閉的空閑連接,單位是毫秒
timeBetweenEvictionRunsMillis: 60000
# 配置一個連接在池中最小生存的時間,單位是毫秒
minEvictableIdleTimeMillis: 30000
validationQuery: select 'x'
testWhileIdle: true
testOnBorrow: false
testOnReturn: false
# 打開PSCache,並且指定每個連接上PSCache的大小
poolPreparedStatements: true
maxPoolPreparedStatementPerConnectionSize: 20
# 配置監控統計攔截的filters,去掉后監控界面sql無法統計,'wall'用於防火牆
filters: stat,wall,slf4j
# 通過connectProperties屬性來打開mergeSql功能;慢SQL記錄
connectionProperties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=5000
# 合並多個DruidDataSource的監控數據
#useGlobalDataSourceStat: true
問題:com.mysql.jdbc.Driver 不能加載問題,因確認 mysql-connector-java 的依賴引入。
4.2.3 配置mybatis
在application.yml中增加
mybatis:
configuration:
#true來開啟駝峰功能。
map-underscore-to-camel-case: true
#正則掃描mapper映射的位置
mapper-locations: mybatis/**/*Mapper.xml
#正則掃描實體類package的
typeAliasesPackage: com.fishpro.springstudy.**.domain
4.2.4 編寫實體類domain.ArticleDO.java
1)在 com.fishpro.sprintstudy.domain包下新建java類 ArticleDO.java
2)編寫代碼如下(后期可以采用自動生成的方法)
package com.fishpro.springstudy.domain;
import java.util.Date;
public class ArticleDO {
private Integer id;
private String title;
private String content;
private Date postTime;
private Integer postStatus;
private Date createBy;
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public String getContent() {
return content;
}
public void setContent(String content) {
this.content = content;
}
public Date getPostTime() {
return postTime;
}
public void setPostTime(Date postTime) {
this.postTime = postTime;
}
public Integer getPostStatus() {
return postStatus;
}
public void setPostStatus(Integer postStatus) {
this.postStatus = postStatus;
}
public Date getCreateBy() {
return createBy;
}
public void setCreateBy(Date createBy) {
this.createBy = createBy;
}
}
4.2.5 編寫mybatis的mapper的xml
根據配置文件中的配置
#正則掃描mapper映射的位置
mapper-locations: mybatis/**/*Mapper.xml
我們在resources/下創建mybatis文件夾,並創建文件ArticleMapper.xml 包括了
1)獲取單個實體
2)獲取分頁列表
3)插入
4)更新
5)刪除
5)批量刪除
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.fishpro.springstudy.dao.ArticleDao">
<select id="get" resultType="com.fishpro.springstudy.domain.ArticleDO">
select `id`,`title`,`content`,`post_time`,`post_status`,`create_by` from t_article where id = #{value}
</select>
<select id="list" resultType="com.fishpro.springstudy.domain.ArticleDO">
select `id`,`title`,`content`,`post_time`,`post_status`,`create_by` from t_article
<where>
<if test="id != null and id != '-1' " > and id = #{id} </if>
<if test="title != null and title != '' " > and title = #{title} </if>
<if test="content != null and content != '' " > and content = #{content} </if>
<if test="postTime != null and postTime != '' " > and post_time = #{postTime} </if>
<if test="postStatus != null and postStatus != '-1' " > and post_status = #{postStatus} </if>
<if test="createBy != null and createBy != '' " > and create_by = #{createBy} </if>
</where>
<choose>
<when test="sort != null and sort.trim() != ''">
order by ${sort} ${order}
</when>
<otherwise>
order by id desc
</otherwise>
</choose>
<if test="offset != null and limit != null">
limit #{offset}, #{limit}
</if>
</select>
<select id="count" resultType="int">
select count(*) from t_article
<where>
<if test="id != null and id != '-1' " > and id = #{id} </if>
<if test="title != null and title != '' " > and title = #{title} </if>
<if test="content != null and content != '' " > and content = #{content} </if>
<if test="postTime != null and postTime != '' " > and post_time = #{postTime} </if>
<if test="postStatus != null and postStatus != '-1' " > and post_status = #{postStatus} </if>
<if test="createBy != null and createBy != '' " > and create_by = #{createBy} </if>
</where>
</select>
<insert id="save" parameterType="com.fishpro.springstudy.domain.ArticleDO" useGeneratedKeys="true" keyProperty="id">
insert into t_article
(
`title`,
`content`,
`post_time`,
`post_status`,
`create_by`
)
values
(
#{title},
#{content},
#{postTime},
#{postStatus},
#{createBy}
)
</insert>
<update id="update" parameterType="com.fishpro.springstudy.domain.ArticleDO">
update t_article
<set>
<if test="title != null">`title` = #{title}, </if>
<if test="content != null">`content` = #{content}, </if>
<if test="postTime != null">`post_time` = #{postTime}, </if>
<if test="postStatus != null">`post_status` = #{postStatus}, </if>
<if test="createBy != null">`create_by` = #{createBy}</if>
</set>
where id = #{id}
</update>
<delete id="remove">
delete from t_article where id = #{value}
</delete>
<delete id="batchRemove">
delete from t_article where id in
<foreach item="id" collection="array" open="(" separator="," close=")">
#{id}
</foreach>
</delete>
</mapper>
4.2.6 編寫dao
Dao是通過Mybats自動與Mapper對應的
package com.fishpro.springstudy.dao;
import com.fishpro.springstudy.domain.ArticleDO;
import java.util.List;
import java.util.Map;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface ArticleDao {
ArticleDO get(Integer id);
List<ArticleDO> list(Map<String,Object> map);
int count(Map<String,Object> map);
int save(ArticleDO article);
int update(ArticleDO article);
int remove(Integer id);
int batchRemove(Integer[] ids);
}
注意:自此我們已經完成了實體類到具體數據庫的映射操作,下面4.4.7編寫一個controller類方法,直接測試。
4.2.7 編寫一個RestController測試dao
雖然,原則上,我們需要建立service層,才能編寫controller,現在我們不妨先測試下我們編寫的Dao是否正確。
ArticleController.cs
package com.fishpro.springstudy.controller;
import com.fishpro.springstudy.dao.ArticleDao;
import com.fishpro.springstudy.domain.ArticleDO;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.Date;
@RequestMapping("/article")
@RestController
public class ArtcileController {
@Autowired
private ArticleDao articleDao;
@RequestMapping("/test")
public String test(){
ArticleDO articleDO=new ArticleDO();
articleDO.setTitle("testing");
articleDO.setContent("content");
articleDO.setCreateBy(new Date());
articleDO.setPostStatus(0);
articleDO.setPostTime(new Date());
int i= articleDao.save(articleDO);
if(i>0)
return "ok";
else
return "fail";
}
}
在瀏覽器輸入 http://localhost:8991/article/test
如下圖:是瀏覽器的截圖和數據庫插入的數據展示。

4.3 編寫Service服務層代碼
服務層代碼通常分接口,和接口的實現,具體放在service和service.impl下;
在 com.fishpro.springstudy.service 下建立接口文件 ArticleService.java
在 com.fishpro.springstudy.service.impl下建立接口實現文件 ArticleServiceImpl.java
主要代碼如下
ArticleService.java
public interface ArticleService {
ArticleDO get(Integer id);
List<ArticleDO> list(Map<String, Object> map);
int count(Map<String, Object> map);
int save(ArticleDO article);
int update(ArticleDO article);
int remove(Integer id);
int batchRemove(Integer[] ids);
}
ArticleServiceImpl.java
@Service
public class ArticleServiceImpl implements ArticleService {
@Autowired
private ArticleDao articleDao;
@Override
public ArticleDO get(Integer id){
return articleDao.get(id);
}
@Override
public List<ArticleDO> list(Map<String, Object> map){
return articleDao.list(map);
}
@Override
public int count(Map<String, Object> map){
return articleDao.count(map);
}
@Override
public int save(ArticleDO article){
return articleDao.save(article);
}
@Override
public int update(ArticleDO article){
return articleDao.update(article);
}
@Override
public int remove(Integer id){
return articleDao.remove(id);
}
@Override
public int batchRemove(Integer[] ids){
return articleDao.batchRemove(ids);
}
}
注意實現接口文件 ArticleServiceImpl.java 類中實現了注解 @Service
4.4 編寫應用層代碼
4.4.1 Rest Api簡單實踐
實際上在4.1最后,我們已經建立了一個Rest Api接口來測試。建立Rest Api在Spring Boot中非常簡單
1)在controller包名下建立以Controller結尾的java文件,例如 ArticleController.cs
2)在類名上加入注解 @RestController 表示該類是Rest Api
在類名上加入 @RequestMapping 注解,表示該類的路由例如 @RequestMapping("/article")
3)編寫public方法 例如 public String test(),在public方法上添加 @RequestMapping("/test") 表示該方法的路由是 test
例如4.1中
@RequestMapping("/test")
public String test(){
return "test";
}
4)Get還是Post等方法
Post 在方法上加入@PostMapping("/postTest") 和 @ResponseBody (表示返回JSON格式)注解
@PostMapping("/postTest")
@ResponseBody
public ArticleDO postTest(){
ArticleDO model=articleDao.get(1);
return model;
}
在Postman(谷歌下載)http://localhost:8991/article/postTest 如下圖:

Get 在方法上加入 @GetMapping
5)參數的注解
HttpServletRequest
通常我們web方法的參數是HttpServletRequest,我們也可以在方法參數中設置HttpServletRequest參數,如下
@GetMapping("/paramTest")
public String paramTest(HttpServletRequest request){
if(request.getParameter("d")!=null)
return request.getParameter("d").toString();
else
return "not find param name d";
}
1)當我們輸入 http://localhost:8991/article/paramTest 顯示 “not find param name d”
2)當我輸入http://localhost:8991/article/paramTest?d=i%20am%20d 顯示 i am d
@RequestParam 替換 HttpServletRequest 的 request.getParameter方法
@GetMapping("/paramNameTest")
public String paramNameTest(HttpServletRequest request,@RequestParam("name") String name){
if(!"".equals(name))
return name;
else
return "not find param name ";
}
@PathVariable 參數在路由中顯示
@GetMapping("/paramPathTest/{name}")
public String paramPathTest(HttpServletRequest request,@PathVariable("name") String name){
if(request.getParameter("d")!=null)
return request.getParameter("d").toString();
else
return "not find param name d";
}
@RequestBody 參數為Json
@PostMapping("/jsonPostJsonTest")
@ResponseBody
public ArticleDO jsonPostJsonTest(@RequestBody ArticleDO articleDO){
return articleDO;
}
4.4.2 編寫文章的新增、編輯、刪除、獲取列表等Controller層代碼
為了統一管理返回狀態,我們定義個返回的基礎信息包括返回的代碼、信息等信息 如下,表示統一使用Json作為返回信息
{"code":1,"msg":"返回信息","data":Object}
對應的返回類
com.fishpro.springstudy.domain.Rsp.java
public class Rsp extends HashMap<String ,Object> {
private static final long serialVersionUID = 1L;
public Rsp() {
put("code", 0);
put("msg", "操作成功");
}
public static Rsp error() {
return error(1, "操作失敗");
}
public static Rsp error(String msg) {
return error(500, msg);
}
public static Rsp error(int code, String msg) {
Rsp r = new Rsp();
if(msg==null)
{
msg="發生錯誤";
}
r.put("code", code);
r.put("msg", msg);
return r;
}
public static Rsp ok(String msg) {
Rsp r = new Rsp();
r.put("msg", msg);
return r;
}
public static Rsp ok(Map<String, Object> map) {
Rsp r = new Rsp();
r.putAll(map);
return r;
}
public static Rsp ok() {
return new Rsp();
}
@Override
public Rsp put(String key, Object value) {
super.put(key, value);
return this;
}
}
在ArticleController.java里面,我們編寫 相關的方法,全部的java代碼如下:
注意:這里我們不研究分頁的方法(后面講)。
/**
* 文章首頁 存放列表頁面
* */
@GetMapping()
String Article(){
return "article/index";
}
/**
* 獲取文章列表數據 不考慮分頁
* */
@ResponseBody
@GetMapping("/list")
public List<ArticleDO> list(@RequestParam Map<String, Object> params){
//查詢列表數據
List<ArticleDO> articleList = articleService.list(params);
return articleList;
}
/**
* 文章添加頁面的路由
* */
@GetMapping("/add")
String add(){
return "article/add";
}
/**
* 文章編輯頁面的路由
* */
@GetMapping("/edit/{id}")
String edit(@PathVariable("id") Integer id,Model model){
ArticleDO article = articleService.get(id);
model.addAttribute("article", article);
return "article/edit";
}
/**
* Post方法,保存數據 這里不考慮權限
*/
@ResponseBody
@PostMapping("/save")
public Rsp save(ArticleDO article){
if(articleService.save(article)>0){
return Rsp.ok();
}
return Rsp.error();
}
/**
* Post方法,修改數據 這里不考慮權限
*/
@ResponseBody
@RequestMapping("/update")
public Rsp update( ArticleDO article){
articleService.update(article);
return Rsp.ok();
}
/**
* Post方法,刪除數據 這里不考慮權限
*/
@PostMapping( "/remove")
@ResponseBody
public Rsp remove( Integer id){
if(articleService.remove(id)>0){
return Rsp.ok();
}
return Rsp.error();
}
/**
* Post方法,批量刪除數據 這里不考慮權限
*/
@PostMapping( "/batchRemove")
@ResponseBody
public Rsp remove(@RequestParam("ids[]") Integer[] ids){
articleService.batchRemove(ids);
return Rsp.ok();
}
說明:
Article方法 對應 /article/index地址 對應html文件為 resources/templates/article/index.html
add方法對應 /article/add 對應html文件為 resources/templates/article/add.html
edit方法對應 /article/edit 對應html文件為 resources/templates/article/edit.html
4.5 使用Thymeleaf編寫前端頁面
Thymeleaf是一套Java開發的獨立的模板引擎,可以很好與Spring Boot整合,起到事半功倍的效果。
使用Thymeleaf前,我們需要知道
/resources/static 是存放靜態文件 包括image css js等
/resources/templates 是存放模板文件
4.5.1 Pom.xml中添加依賴
<!-- 模板引擎 Thymeleaf 依賴 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<!--thymeleaf 兼容非嚴格的html5-->
<dependency>
<groupId>net.sourceforge.nekohtml</groupId>
<artifactId>nekohtml</artifactId>
</dependency>
4.5.2 配置Thymeleaf
編輯 application.yml
spring:
thymeleaf:
mode: LEGACYHTML5
cache: false
prefix: classpath:/templates/
4.5.3 使用Thymeleaf
thymeleaf可以直接使用html后綴,在resources/templates下增加,在本章示例中
resources/templates/article/index.html
resources/templates/article/add.html
resources/templates/article/edit.html
為了快速的開發實例,我們使用前端框架H+作為練習使用。
前端使用jquery、bootstrap.css、bootstrap-table.js
4.5.4 文章列表頁面
列表頁面主要采用bootstrap-table.js插件。
bootstrap-table.js
因為數據少,我們之間采用客戶端分頁的模式 sidePagination : "client", // 設置在哪里進行分頁,可選值為"client" 或者 "server"
1)建立模板頁面: resources/templates/article/index.html
2 建立路由:在ArticleController中增加前端頁面路由/article/index
3) 建立數據路由:ArticleController中編寫bootstrap-table的ajax請求方法 list
4)運行:在瀏覽器中驗證

注意:本頁面沒有用到thymeleaf的模板語句。
4.5.5 添加文章功能
注意,我們使用了layui的彈窗組件。
1)建立模板頁面: resources/templates/article/add.html
2 建立路由:在ArticleController中增加前端頁面路由/article/
3) 建立數據路由:ArticleController中編寫bootstrap-table的ajax請求方法 save
4)運行:編寫頁面的ajax方法,在瀏覽器中驗證

保存新增數據代碼
function save() {
$.ajax({
cache : true,
type : "POST",
url : "/article/save",
data : $('#signupForm').serialize(),// 你的formid
async : false,
error : function(request) {
parent.layer.alert("Connection error");
},
success : function(data) {
if (data.code == 0) {
parent.layer.msg("操作成功");
parent.reLoad();
var index = parent.layer.getFrameIndex(window.name); // 獲取窗口索引
parent.layer.close(index);
} else {
parent.layer.alert(data.msg)
}
}
});
}
注意:本頁面沒有用到thymeleaf的模板語句。
4.5.6 修改文章功能
1)建立模板頁面: resources/templates/article/edit.html
2 建立路由:在ArticleController中增加前端頁面路由/article/edit,並配置模板頁面,如下代碼,其中thymeleaf標簽規則為
a.th開頭
b.等於號后面是 “${ }” 標簽,在${ } 大括號內存放后台的model數據和數據的邏輯。如${article.title}表示后台的article對象中的title值
c.關於thymeleaf這里不做細化,后面單獨實踐。
<form class="form-horizontal m-t" id="signupForm">
<input id="id" name="id" th:value="${article.id}" type="hidden">
<div class="form-group">
<label class="col-sm-3 control-label">:</label>
<div class="col-sm-8">
<input id="title" name="title" th:value="${article.title}" class="form-control" type="text">
</div>
</div>
<div class="form-group">
<label class="col-sm-3 control-label">:</label>
<div class="col-sm-8">
<input id="content" name="content" th:value="${article.content}" class="form-control" type="text">
</div>
</div>
<div class="form-group">
<label class="col-sm-3 control-label">:</label>
<div class="col-sm-8">
<input id="postStatus" name="postStatus" th:value="${article.postStatus}" class="form-control" type="text">
</div>
</div>
<div class="form-group">
<div class="col-sm-8 col-sm-offset-3">
<button type="submit" class="btn btn-primary">提交</button>
</div>
</div>
</form>
3) 建立數據路由:ArticleController中編寫bootstrap-table的ajax請求方法 update
4)運行:編寫頁面的ajax方法,在瀏覽器中驗證
function update() {
$.ajax({
cache : true,
type : "POST",
url : "/article/update",
data : $('#signupForm').serialize(),// 你的formid
async : false,
error : function(request) {
parent.layer.alert("Connection error");
},
success : function(data) {
if (data.code == 0) {
parent.layer.msg("操作成功");
parent.reLoad();
var index = parent.layer.getFrameIndex(window.name); // 獲取窗口索引
parent.layer.close(index);
} else {
parent.layer.alert(data.msg)
}
}
});
}
4.5.7 刪除文章功能
因為刪除不需要單獨編寫界面,流程與新增、編輯都不一樣,刪除直接在列表頁面進行觸發。
1) 建立數據路由:ArticleController中編寫bootstrap-table的ajax請求方法 remove
2)運行:編寫頁面的ajax方法,在瀏覽器中驗證
function remove(id) {
layer.confirm('確定要刪除選中的記錄?', {
btn : [ '確定', '取消' ]
}, function() {
$.ajax({
url : prefix+"/remove",
type : "post",
data : {
'id' : id
},
success : function(r) {
if (r.code==0) {
layer.msg(r.msg);
reLoad();
}else{
layer.msg(r.msg);
}
}
});
})
}
總結:編寫代碼工作實際上是枯燥無味的,實際上面的,三層結構代碼是可以全部自動生成的,沒有必要手動來編寫,只不過,在這里,拿出來講解說明部分原理。
4.6 使用Shiro加入權限認證
如何對4.5的功能加入權限認證,這樣,其他人就不能隨便使用這些具有危險操作的功能。
在Spring Boot中已經支持了很多權限認證套件,比如Shiro 比如Spring Boot Security,本章實踐使用Shiro,他簡單而強大,非常適合中后端開發者使用。
Shiro對於使用者來說,雖然簡單易於使用,但是里面的各種流程,我到現在還是不求甚解。
4.6.1 Shiro簡單說明
有必要簡單了解下這個認證框架,采用官方的圖片說明

1) Authentication:身份認證/登錄,驗證用戶是不是擁有相應的身份;
2)Authorization:授權,即權限驗證,驗證某個已認證的用戶是否擁有某個權限;即判斷用戶是否能做事情,常見的如:驗證某個用戶是否擁有某個角色。或者細粒度的驗證某個用戶對某個資源是否具有某個權限;
3)Session Manager:會話管理,即用戶登錄后就是一次會話,在沒有退出之前,它的所有信息都在會話中;會話可以是普通JavaSE環境的,也可以是如Web環境的;
4)Cryptography:加密,保護數據的安全性,如密碼加密存儲到數據庫,而不是明文存儲;
這里需要說明的是 Authentication 和 Authorization 看起來是差不多,實 Authentication 是身份證認證,你去公園,進大門就要驗票,就是這個。Authorization 是授權,就是你去里面玩,你到了某個景點,還要驗證下你是否被授權訪問,就是這個Authorization
其他幾個說明
5)Web Support:Web支持,可以非常容易的集成到Web環境;
6)Caching:緩存,比如用戶登錄后,其用戶信息、擁有的角色/權限不必每次去查,這樣可以提高效率;
7)Concurrency:shiro支持多線程應用的並發驗證,即如在一個線程中開啟另一個線程,能把權限自動傳播過去;
8)Testing:提供測試支持;
9)Run As:允許一個用戶假裝為另一個用戶(如果他們允許)的身份進行訪問;
10)Remember Me:記住我,這個是非常常見的功能,即一次登錄后,下次再來的話不用登錄了。
那么Shiro是如何實現一個認證,又是如何實現一個授權的呢?
這里涉及到幾個概念

1)Subject:當前用戶,Subject 可以是一個人,但也可以是第三方服務、守護進程帳戶、時鍾守護任務或者其它–當前和軟件交互的任何事件。
解讀:你去公園,Subject就是你(人)
2)SecurityManager:管理所有Subject,SecurityManager 是 Shiro 架構的核心,配合內部安全組件共同組成安全傘。
解讀:SecurityManager就是公園的門票管理系統(包括了閘機、后台服務等)
3)Realms:用於進行權限信息的驗證,我們自己實現。Realm 本質上是一個特定的安全 DAO:它封裝與數據源連接的細節,得到Shiro 所需的相關的數據。在配置 Shiro 的時候,你必須指定至少一個Realm 來實現認證(authentication)和/或授權(authorization)。
解讀:就是你拿的票,你可以買一個大的門票,也可以買包含特殊項目的門票。不同的門票對應不同的授權。

下面我實際操作如何整合Shiro
4.6.2 Pom中加入Shiro依賴
如下代碼:注意這里加入了ehcache、shiro、shiro for spring、shiro ehcache、shiro thymeleaf(與thymeleaf完美結合)
<!-- ehchache -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<dependency>
<groupId>net.sf.ehcache</groupId>
<artifactId>ehcache</artifactId>
</dependency>
<!--shiro -->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-core</artifactId>
<version>1.3.2</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.3.2</version>
</dependency>
<!-- shiro ehcache -->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-ehcache</artifactId>
<version>1.3.2</version>
</dependency>
<dependency>
<groupId>com.github.theborakompanioni</groupId>
<artifactId>thymeleaf-extras-shiro</artifactId>
<version>1.2.1</version>
</dependency>
ehcache配置
ehcache 需要在resources下新建config文件夾,並新建ehcache.xml文配置文件
<?xml version="1.0" encoding="UTF-8"?>
<ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="http://ehcache.org/ehcache.xsd"
updateCheck="false">
<diskStore path="java.io.tmpdir/Tmp_EhCache" />
<defaultCache eternal="false" maxElementsInMemory="1000"
overflowToDisk="false" diskPersistent="false" timeToIdleSeconds="0"
timeToLiveSeconds="600" memoryStoreEvictionPolicy="LRU" />
<cache name="role" eternal="false" maxElementsInMemory="10000"
overflowToDisk="false" diskPersistent="false" timeToIdleSeconds="0"
timeToLiveSeconds="0" memoryStoreEvictionPolicy="LFU" />
</ehcache>
4.6.3 在Spring Boot中編寫Shiro配置
根據4.6.1簡要說明,如下圖,我們需要使用Shiro就必須要先創建Shiro SecurityManager,
而創建SecurityManager,的過程就是包括設置Realm。
在Realm中,我們繼承兩個接口,一個是認證、一個是授權。

1) 增加包名 springstudy.config
2)在springstudy.config增加shiro包名,並增加UserRealm.java 表示Shiro權限認證中的用戶票據(門票)。代碼如下,我們假設了用戶admin密碼1234569,擁有一些權限。
/**
* 授權 假設
* system:article:index 列表
* system:article:add 增加權限
* system:article:edit 修改權限
* system:article:remove 刪除權限
* system:article:batchRemove 批量刪除權限
* */
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
Long userId= ShiroUtils.getUserId();
Set<String> permissions=new HashSet<>();
permissions.add("system:article:index");
permissions.add("system:article:add");
permissions.add("system:article:edit");
permissions.add("system:article:remove");
permissions.add("system:article:batchRemove");
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
info.setStringPermissions(permissions);
return info;
}
/**
* 認證 給出一個假設的admin用戶
* */
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
String username=(String)authenticationToken.getPrincipal();
Map<String ,Object> map=new HashMap<>(16);
map.put("username",username);
String password =new String((char[]) authenticationToken.getCredentials());
if(!"admin".equals(username) || !"1234569".equals(password)){
throw new IncorrectCredentialsException("賬號或密碼不正確");
}
UserDO user=new UserDO();
user.setId(1L);
user.setUsername(username);
user.setPassword(password);
SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(user, password, getName());
return info;
}
3)在shiro包名下,新建一個ShiroUtils.java的類,作為公用的類
@Autowired
private static SessionDAO sessionDAO;
public static Subject getSubjct() {
return SecurityUtils.getSubject();
}
public static UserDO getUser() {
Object object = getSubjct().getPrincipal();
UserDO userDO=new UserDO();
return (UserDO)object;
}
public static Long getUserId() {
return getUser().getId();
}
public static void logout() {
getSubjct().logout();
}
public static List<Principal> getPrinciples() {
List<Principal> principals = null;
Collection<Session> sessions = sessionDAO.getActiveSessions();
return principals;
}
4)在shiro包名下新建BDSessionListener.java,實現 SessionListener接口
private final AtomicInteger sessionCount = new AtomicInteger(0);
@Override
public void onStart(Session session) {
sessionCount.incrementAndGet();
}
@Override
public void onStop(Session session) {
sessionCount.decrementAndGet();
}
@Override
public void onExpiration(Session session) {
sessionCount.decrementAndGet();
}
public int getSessionCount() {
return sessionCount.get();
}
5)在1)中的包名 config下增加類ShiroConfig.java
詳細代碼見 源碼下載
/**
* shiroFilterFactoryBean 實現過濾器過濾
* setFilterChainDefinitionMap 表示設置可以訪問或禁止訪問目錄
* @param securityManager 安全管理器
* */
@Bean
ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager) {
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
shiroFilterFactoryBean.setSecurityManager(securityManager);
//設置登錄頁面
shiroFilterFactoryBean.setLoginUrl("/login");
//登錄后的頁面
shiroFilterFactoryBean.setSuccessUrl("/article/index");
//未認證頁面提示
shiroFilterFactoryBean.setUnauthorizedUrl("/403");
//設置無需加載權限的頁面過濾器
LinkedHashMap<String, String> filterChainDefinitionMap = new LinkedHashMap<>();
filterChainDefinitionMap.put("/fonts/**", "anon");
filterChainDefinitionMap.put("/css/**", "anon");
filterChainDefinitionMap.put("/js/**", "anon");
filterChainDefinitionMap.put("/index", "anon");
//authc 有權限
//filterChainDefinitionMap.put("/**", "authc");
filterChainDefinitionMap.put("/**", "authc");
//設置過濾器
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
return shiroFilterFactoryBean;
}
6)運行 http://localhost:8991//index
可以看到,跳轉到http://localhost:8991/login

4.6.4 增加用戶登錄模塊
在4.6.3中,在shiro過濾器中,我們默認login是可以訪問的,其他都不能訪問,用戶必須經過shiro進行認真后,才能登錄訪問其他頁面。
1)在resources/templates 下新建 login.html
2)實現html5代碼
3) 新增LoginController.java(在controller包名下)
@GetMapping("/login")
public String login(){
return "/login";
}
/**
* 登錄按鈕對應的 服務端api
* @param username 用戶名
* @param password 用戶密碼
* @return Rsp 返回成功或失敗 Json格式
* */
@ResponseBody
@PostMapping("/login")
public Rsp ajaxLogin(@RequestParam String username, @RequestParam String password){
UsernamePasswordToken token = new UsernamePasswordToken(username,password);
Subject subject = SecurityUtils.getSubject();
try{
subject.login(token);
return Rsp.ok();
}catch (AuthenticationException e){
return Rsp.error("用戶名或密碼錯誤");
}
}
在瀏覽器 輸入 http://localhost:8991/login

登錄后可以進入文章列表頁面
4.7 加入測試模塊
按照標准流程,我們是要加入單頁測試。一般單元測試是在每個功能做完后,就把單元測試用例寫完。這樣就不會忘記,也不需要重復去做某個功能。但是這里寫的實戰教程,就單獨拿出來說下。
本章使用自帶的 spring-boot-test-starter 框架進行單元測試
4.7.1 Spring Boot Test 簡介
spring-boot-test-starter 中主要使用了以下幾個注解完成測試功能
@BeforeClass 在所有測試方法前執行一次,一般在其中寫上整體初始化的代碼
@AfterClass 在所有測試方法后執行一次,一般在其中寫上銷毀和釋放資源的代碼
@Before 在每個測試方法前執行,一般用來初始化方法(比如我們在測試別的方法時,類中與其他測試方法共享的值已經被改變,為了保證測試結果的有效性,我們會在@Before注解的方法中重置數據)
@After 在每個測試方法后執行,在方法執行完成后要做的事情
@Test(timeout = 1000) 測試方法執行超過1000毫秒后算超時,測試將失敗
@Test(expected = Exception.class) 測試方法期望得到的異常類,如果方法執行沒有拋出指定的異常,則測試失敗
@Ignore(“not ready yet”) 執行測試時將忽略掉此方法,如果用於修飾類,則忽略整個類
@Test 編寫一般測試用例
@RunWith 在JUnit中有很多個Runner,他們負責調用你的測試代碼,每一個Runner都有各自的特殊功能,你要根據需要選擇不同的Runner來運行你的測試代碼。
4.7.2 MockMVC
測試Web應用程序,通常使用 MockMVC 測試Controller
使用MockMVC的關鍵是
在獨立項目中使用
MockMvcBuilders.standaloneSetup
在web項目中使用
MockMvcBuilders.webAppContextSetup
4.7.3 Pom中加入依賴
這個已經有了
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
4.7.4 編寫基於Controller的單元測試
@RunWith(SpringRunner.class)
@SpringBootTest(classes = SpringstudyApplication.class)
@AutoConfigureMockMvc
public class ArticleControllerTests {
private URL base;
//定義mockmvc
private MockMvc mvc;
//注入WebApplicationContext
@Autowired
private WebApplicationContext webApplicationContext;
/**
* 在測試之前 初始化mockmvc
* */
@Before
public void testBefore() throws Exception{
String url = "http://localhost:8991";
this.base = new URL(url);
//mvc = MockMvcBuilders.standaloneSetup(new ArticleController()).build();
mvc = MockMvcBuilders.webAppContextSetup(webApplicationContext).build();
}
@After
public void testAfter(){
System.out.println("測試后");
}
/**
* 使用一個測試
* */
@Test
public void saveTest() throws Exception{
MultiValueMap<String, String> map = new LinkedMultiValueMap<>();
map.add("title", "是時候認真學習SpringBoot了");
map.add("content", "是時候認真學習SpringBoot了");
mvc.perform(MockMvcRequestBuilders.post("/article/save").accept(MediaType.ALL)
.params(map))
.andExpect(MockMvcResultMatchers.status().isOk())
.andDo(MockMvcResultHandlers.print());
}
}
問題:因為沒有使用過MockMvc,總是測試失敗,其實對於陌生的功能點,最好找個簡明的知識點學習下。
4.8 加入Web全局攔截器WebMvcConfigurer
通常我們在程序中需要全局處理包括
1)時間格式化問題
2)跨域請求問題
3)路由適配大小寫問題
等等,這些問題,不可能在每個頁面每個功能的時候一一去做處理,這樣工作繁瑣,並且容易忘記處理。這里需要加入全局配置。
在Spring Boot 2.0 (Spring 5.0)中已經取消了 WebMvcConfigurerAdapter
@Configuration
public class WebConfigurer implements WebMvcConfigurer {
/**
* 注入路徑匹配規則 忽略URL大小寫
* */
@Override
public void configurePathMatch(PathMatchConfigurer configurer) {
org.springframework.util.AntPathMatcher matcher=new org.springframework.util.AntPathMatcher();
matcher.setCachePatterns(false);
configurer.setPathMatcher(matcher);
}
/**
* 支持跨域提交
* */
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowCredentials(true)
.allowedHeaders("*")
.allowedOrigins("*")
.allowedMethods("*");
}
}
4.9 加入日志(slf4j+logback)功能
日志功能,無論是哪個插件,基本都是相似的,其日志層級包括了
TARCE , DEBUG , INFO , WARN , ERROR , FATAL , OFF
其市場上主要的插件包括
1)slf4j
2)log4j
3)logback
4)log4j2
本章使用slf4j+logback,slf4j是內置的日志記錄組件,logback則主要用來保存記錄
4.9.1 在Pom.xml 引入依賴
默認已經包括了slf4j,據說springboot的log就是slf4j提供的。
4.9.1 配置日志框架
引入依賴成功后,就可以使用log了,不過想要漂亮的使用log,我們還需要知道一些配置比如我們會有一些疑問
1)日志保存在哪里
2)日志是每天一份還是一直保存到一份里面
3)能不能像增加注解一樣指定哪些類或方法使用日志
具體配置如下:
#slf4j日志配置 logback配置見 resources/logback-spring.xml
logging:
level:
root: error
com.fishpor.springstudy: info
logback配置則使用xml,具體路徑是 resources/logback-spring.xml ,沒有此文件則新建文件,加入如下代碼
<?xml version="1.0" encoding="UTF-8"?>
<configuration scan="true" scanPeriod="60 seconds" debug="false">
<contextName>logback</contextName>
<!--輸出到控制台-->
<appender name="console" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<!--按天生成日志-->
<appender name="logFile" class="ch.qos.logback.core.rolling.RollingFileAppender">
<Prudent>true</Prudent>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<FileNamePattern>
applog/%d{yyyy-MM-dd}/%d{yyyy-MM-dd}.log
</FileNamePattern>
</rollingPolicy>
<layout class="ch.qos.logback.classic.PatternLayout">
<Pattern>
%d{yyyy-MM-dd HH:mm:ss} -%msg%n
</Pattern>
</layout>
</appender>
<logger name="com.glsafesports.pine" additivity="false">
<appender-ref ref="console"/>
<appender-ref ref="logFile" />
</logger>
<root level="error">
<appender-ref ref="console"/>
<appender-ref ref="logFile" />
</root>
</configuration>
4.9.2 在代碼中應用
在ArticleController中加入測試方法
/**
* 測試 log
* */
@GetMapping("/log")
@ResponseBody
public String log(){
logger.info("info:");
logger.error("info:");
logger.warn("info:");
return "log";
}
4.9.3 運行效果

4.10 加入緩存功能
緩存也是我們系統中常用的功能,這里我們使用比較簡單的 ehcache。
另外時下更多的使用 redis 來作為緩存,這個后面單獨實戰。
4.10.1 在Pom.xml中加入依賴
前面介紹Shiro的時候已經
4.10.2 配置緩存
在前介紹過 編寫resources\config\ehcache.xml
<?xml version="1.0" encoding="UTF-8"?>
<ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="http://ehcache.org/ehcache.xsd"
updateCheck="false">
<diskStore path="java.io.tmpdir/Tmp_EhCache" />
<defaultCache eternal="false" maxElementsInMemory="1000"
overflowToDisk="false" diskPersistent="false" timeToIdleSeconds="0"
timeToLiveSeconds="600" memoryStoreEvictionPolicy="LRU" />
<cache name="role" eternal="false" maxElementsInMemory="10000"
overflowToDisk="false" diskPersistent="false" timeToIdleSeconds="0"
timeToLiveSeconds="0" memoryStoreEvictionPolicy="LFU" />
</ehcache>
在配置Shiro的時候,在ShiroConfig中配置過
這里在config包名下建立EhCacheConfig.java
@Configuration
@EnableCaching
public class EhCacheConfig {
@Bean
public EhCacheCacheManager ehCacheCacheManager(EhCacheManagerFactoryBean bean){
return new EhCacheCacheManager(bean.getObject());
}
@Bean
public EhCacheManagerFactoryBean ehCacheManagerFactoryBean(){
EhCacheManagerFactoryBean cacheManagerFactoryBean=new EhCacheManagerFactoryBean();
cacheManagerFactoryBean.setConfigLocation(new ClassPathResource("config/ehcache.xml"));
cacheManagerFactoryBean.setShared(true);
return cacheManagerFactoryBean;
}
}
4.10.3 編寫緩存代碼
使用EhCache使用到兩個注解
@Cacheable:負責將方法的返回值加入到緩存中,參數3
@CacheEvict:負責清除緩存,參數4
我們新建一個Controller來測試緩存代碼 EhCacheController.java
五 打包發布
1) 打開 View>Tool Windows>Terminal
2)在終端輸入
>mvn clean
>mvn install
系統會在根目錄下生成 target
六 總結
本章快速實踐學習了一套完整的基於Spring Boot開發一個信息管理系統的知識點,本章的目的並不是掌握所有涉及的知識點,而是對Spring Boot整體的項目有一定的了解。對開發的環境有一定的了解。
我們發現幾乎所有的功能都可以通過引用第三方依賴實現相關功能,換句話說就是大部分功能別人都寫好了。
我們通過總結又發現,所有依賴的功能在使用上都是一致的,他們包括
1)引入pom.xml中的依賴
2)配置插件(各個插件有獨立的配置,可以參加插件的官方文檔)
3)在代碼中編寫或使用引入的插件
4)編寫測試代碼測試
5)運行查看效果
