Spring Boot 綜合示例-整合thymeleaf、mybatis、shiro、logging、cache開發一個文章發布管理系統


一、概述

經過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)運行查看效果

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM