一、概述

经过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语句

  1. SET NAMES utf8mb4;
  2. SET FOREIGN_KEY_CHECKS = 0;
  3.  
  4. -- ----------------------------
  5. -- Table structure for t_article
  6. -- ----------------------------
  7. DROP TABLE IF EXISTS `t_article`;
  8. CREATE TABLE `t_article` (
  9. `id` int(11) NOT NULL AUTO_INCREMENT,
  10. `title` varchar(255) NOT NULL,
  11. `content` varchar(255) NOT NULL,
  12. `post_time` datetime NOT NULL,
  13. `post_status` int(11) NOT NULL,
  14. `create_by` datetime NOT NULL,
  15. PRIMARY KEY (`id`)
  16. ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
  17.  
  18. 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中输入

  1. server:
  2. port: 8991

  

4.2 增加Mybatis支持,编写数据库访问代码

4.2.1 编辑Pom.xml 增加依赖

本章使用mybatis和阿里巴巴的driud连接池来链接操作数据库

在pom.xml中增加依赖如下,注意有4个依赖引入,分别是mysql链接支持、jdbc支持、druid的alibaba连接池支持、mybatis支持。

  1. <!--jdbc数据库支持-->
  2. <dependency>
  3. <groupId>mysql</groupId>
  4. <artifactId>mysql-connector-java</artifactId>
  5. <scope>runtime</scope>
  6. </dependency>
  7. <dependency>
  8. <groupId>org.springframework.boot</groupId>
  9. <artifactId>spring-boot-starter-jdbc</artifactId>
  10. </dependency>
  11. <!--druid -->
  12. <dependency>
  13. <groupId>com.alibaba</groupId>
  14. <artifactId>druid</artifactId>
  15. <version>1.0.28</version>
  16. </dependency>
  17. <!--mybatis -->
  18. <dependency>
  19. <groupId>org.mybatis</groupId>
  20. <artifactId>mybatis</artifactId>
  21. <version>3.4.4</version>
  22. </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的应用程序配置

  1. spring:
  2. datasource:
  3. type: com.alibaba.druid.pool.DruidDataSource
  4. driverClassName: com.mysql.jdbc.Driver
  5. url: jdbc:mysql://localhost:3306/demo_article?useSSL=false&useUnicode=true&characterEncoding=utf8
  6. #mysql用户名
  7. username: root
  8. #mysql密码
  9. password: 123
  10. #初始化线程池数量
  11. initialSize: 1
  12. #空闲连接池的大小
  13. minIdle: 3
  14. #最大激活量
  15. maxActive: 20
  16. # 配置获取连接等待超时的时间
  17. maxWait: 60000
  18. # 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒
  19. timeBetweenEvictionRunsMillis: 60000
  20. # 配置一个连接在池中最小生存的时间,单位是毫秒
  21. minEvictableIdleTimeMillis: 30000
  22. validationQuery: select 'x'
  23. testWhileIdle: true
  24. testOnBorrow: false
  25. testOnReturn: false
  26. # 打开PSCache,并且指定每个连接上PSCache的大小
  27. poolPreparedStatements: true
  28. maxPoolPreparedStatementPerConnectionSize: 20
  29. # 配置监控统计拦截的filters,去掉后监控界面sql无法统计,'wall'用于防火墙
  30. filters: stat,wall,slf4j
  31. # 通过connectProperties属性来打开mergeSql功能;慢SQL记录
  32. connectionProperties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=5000
  33. # 合并多个DruidDataSource的监控数据
  34. #useGlobalDataSourceStat: true  

问题:com.mysql.jdbc.Driver 不能加载问题,因确认 mysql-connector-java 的依赖引入。

4.2.3 配置mybatis

在application.yml中增加

  1. mybatis:
  2. configuration:
  3. #true来开启驼峰功能。
  4. map-underscore-to-camel-case: true
  5. #正则扫描mapper映射的位置
  6. mapper-locations: mybatis/**/*Mapper.xml
  7. #正则扫描实体类package的
  8. typeAliasesPackage: com.fishpro.springstudy.**.domain

4.2.4 编写实体类domain.ArticleDO.java  

1)在 com.fishpro.sprintstudy.domain包下新建java类 ArticleDO.java

2)编写代码如下(后期可以采用自动生成的方法)

  1. package com.fishpro.springstudy.domain;
  2.  
  3. import java.util.Date;
  4.  
  5. public class ArticleDO {
  6. private Integer id;
  7. private String title;
  8. private String content;
  9. private Date postTime;
  10. private Integer postStatus;
  11. private Date createBy;
  12.  
  13. public Integer getId() {
  14. return id;
  15. }
  16.  
  17. public void setId(Integer id) {
  18. this.id = id;
  19. }
  20.  
  21. public String getTitle() {
  22. return title;
  23. }
  24.  
  25. public void setTitle(String title) {
  26. this.title = title;
  27. }
  28.  
  29. public String getContent() {
  30. return content;
  31. }
  32.  
  33. public void setContent(String content) {
  34. this.content = content;
  35. }
  36.  
  37. public Date getPostTime() {
  38. return postTime;
  39. }
  40.  
  41. public void setPostTime(Date postTime) {
  42. this.postTime = postTime;
  43. }
  44.  
  45. public Integer getPostStatus() {
  46. return postStatus;
  47. }
  48.  
  49. public void setPostStatus(Integer postStatus) {
  50. this.postStatus = postStatus;
  51. }
  52.  
  53. public Date getCreateBy() {
  54. return createBy;
  55. }
  56.  
  57. public void setCreateBy(Date createBy) {
  58. this.createBy = createBy;
  59. }
  60. }

  

4.2.5 编写mybatis的mapper的xml

根据配置文件中的配置

  1. #正则扫描mapper映射的位置
    mapper-locations: mybatis/**/*Mapper.xml

我们在resources/下创建mybatis文件夹,并创建文件ArticleMapper.xml 包括了

1)获取单个实体

2)获取分页列表

3)插入

4)更新

5)删除

5)批量删除

  1. <?xml version="1.0" encoding="UTF-8"?>
  2. <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
  3.  
  4. <mapper namespace="com.fishpro.springstudy.dao.ArticleDao">
  5.  
  6. <select id="get" resultType="com.fishpro.springstudy.domain.ArticleDO">
  7. select `id`,`title`,`content`,`post_time`,`post_status`,`create_by` from t_article where id = #{value}
  8. </select>
  9.  
  10. <select id="list" resultType="com.fishpro.springstudy.domain.ArticleDO">
  11. select `id`,`title`,`content`,`post_time`,`post_status`,`create_by` from t_article
  12. <where>
  13. <if test="id != null and id != '-1' " > and id = #{id} </if>
  14. <if test="title != null and title != '' " > and title = #{title} </if>
  15. <if test="content != null and content != '' " > and content = #{content} </if>
  16. <if test="postTime != null and postTime != '' " > and post_time = #{postTime} </if>
  17. <if test="postStatus != null and postStatus != '-1' " > and post_status = #{postStatus} </if>
  18. <if test="createBy != null and createBy != '' " > and create_by = #{createBy} </if>
  19. </where>
  20. <choose>
  21. <when test="sort != null and sort.trim() != ''">
  22. order by ${sort} ${order}
  23. </when>
  24. <otherwise>
  25. order by id desc
  26. </otherwise>
  27. </choose>
  28. <if test="offset != null and limit != null">
  29. limit #{offset}, #{limit}
  30. </if>
  31. </select>
  32.  
  33. <select id="count" resultType="int">
  34. select count(*) from t_article
  35. <where>
  36. <if test="id != null and id != '-1' " > and id = #{id} </if>
  37. <if test="title != null and title != '' " > and title = #{title} </if>
  38. <if test="content != null and content != '' " > and content = #{content} </if>
  39. <if test="postTime != null and postTime != '' " > and post_time = #{postTime} </if>
  40. <if test="postStatus != null and postStatus != '-1' " > and post_status = #{postStatus} </if>
  41. <if test="createBy != null and createBy != '' " > and create_by = #{createBy} </if>
  42. </where>
  43. </select>
  44.  
  45. <insert id="save" parameterType="com.fishpro.springstudy.domain.ArticleDO" useGeneratedKeys="true" keyProperty="id">
  46. insert into t_article
  47. (
  48. `title`,
  49. `content`,
  50. `post_time`,
  51. `post_status`,
  52. `create_by`
  53. )
  54. values
  55. (
  56. #{title},
  57. #{content},
  58. #{postTime},
  59. #{postStatus},
  60. #{createBy}
  61. )
  62. </insert>
  63.  
  64. <update id="update" parameterType="com.fishpro.springstudy.domain.ArticleDO">
  65. update t_article
  66. <set>
  67. <if test="title != null">`title` = #{title}, </if>
  68. <if test="content != null">`content` = #{content}, </if>
  69. <if test="postTime != null">`post_time` = #{postTime}, </if>
  70. <if test="postStatus != null">`post_status` = #{postStatus}, </if>
  71. <if test="createBy != null">`create_by` = #{createBy}</if>
  72. </set>
  73. where id = #{id}
  74. </update>
  75.  
  76. <delete id="remove">
  77. delete from t_article where id = #{value}
  78. </delete>
  79.  
  80. <delete id="batchRemove">
  81. delete from t_article where id in
  82. <foreach item="id" collection="array" open="(" separator="," close=")">
  83. #{id}
  84. </foreach>
  85. </delete>
  86.  
  87. </mapper>

  

4.2.6 编写dao

Dao是通过Mybats自动与Mapper对应的

  1. package com.fishpro.springstudy.dao;
  2.  
  3. import com.fishpro.springstudy.domain.ArticleDO;
  4.  
  5. import java.util.List;
  6. import java.util.Map;
  7.  
  8. import org.apache.ibatis.annotations.Mapper;
  9.  
  10. @Mapper
  11. public interface ArticleDao {
  12.  
  13. ArticleDO get(Integer id);
  14.  
  15. List<ArticleDO> list(Map<String,Object> map);
  16.  
  17. int count(Map<String,Object> map);
  18.  
  19. int save(ArticleDO article);
  20.  
  21. int update(ArticleDO article);
  22.  
  23. int remove(Integer id);
  24.  
  25. int batchRemove(Integer[] ids);
  26. }

注意:自此我们已经完成了实体类到具体数据库的映射操作,下面4.4.7编写一个controller类方法,直接测试。  

4.2.7 编写一个RestController测试dao

虽然,原则上,我们需要建立service层,才能编写controller,现在我们不妨先测试下我们编写的Dao是否正确。

ArticleController.cs

  1. package com.fishpro.springstudy.controller;
  2.  
  3. import com.fishpro.springstudy.dao.ArticleDao;
  4. import com.fishpro.springstudy.domain.ArticleDO;
  5. import org.springframework.beans.factory.annotation.Autowired;
  6. import org.springframework.web.bind.annotation.RequestMapping;
  7. import org.springframework.web.bind.annotation.RestController;
  8.  
  9. import java.util.Date;
  10.  
  11. @RequestMapping("/article")
  12. @RestController
  13. public class ArtcileController {
  14. @Autowired
  15. private ArticleDao articleDao;
  16.  
  17. @RequestMapping("/test")
  18. public String test(){
  19. ArticleDO articleDO=new ArticleDO();
  20. articleDO.setTitle("testing");
  21. articleDO.setContent("content");
  22. articleDO.setCreateBy(new Date());
  23. articleDO.setPostStatus(0);
  24. articleDO.setPostTime(new Date());
  25.  
  26. int i= articleDao.save(articleDO);
  27. if(i>0)
  28. return "ok";
  29. else
  30. return "fail";
  31.  
  32. }
  33. }

  

在浏览器输入 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

  1. public interface ArticleService {
  2.  
  3. ArticleDO get(Integer id);
  4. List<ArticleDO> list(Map<String, Object> map);
  5. int count(Map<String, Object> map);
  6. int save(ArticleDO article);
  7. int update(ArticleDO article);
  8. int remove(Integer id);
  9. int batchRemove(Integer[] ids);
  10. }

  

ArticleServiceImpl.java

  1. @Service
  2. public class ArticleServiceImpl implements ArticleService {
  3. @Autowired
  4. private ArticleDao articleDao;
  5.  
  6. @Override
  7. public ArticleDO get(Integer id){
  8. return articleDao.get(id);
  9. }
  10.  
  11. @Override
  12. public List<ArticleDO> list(Map<String, Object> map){
  13. return articleDao.list(map);
  14. }
  15.  
  16. @Override
  17. public int count(Map<String, Object> map){
  18. return articleDao.count(map);
  19. }
  20.  
  21. @Override
  22. public int save(ArticleDO article){
  23. return articleDao.save(article);
  24. }
  25.  
  26. @Override
  27. public int update(ArticleDO article){
  28. return articleDao.update(article);
  29. }
  30.  
  31. @Override
  32. public int remove(Integer id){
  33. return articleDao.remove(id);
  34. }
  35.  
  36. @Override
  37. public int batchRemove(Integer[] ids){
  38. return articleDao.batchRemove(ids);
  39. }
  40.  
  41. }

  

注意实现接口文件 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中

  1. @RequestMapping("/test")
  2. public String test(){
  3. return "test";
  4. }

4)Get还是Post等方法

Post 在方法上加入@PostMapping("/postTest") 和  @ResponseBody (表示返回JSON格式)注解

  1. @PostMapping("/postTest")
  2. @ResponseBody
  3. public ArticleDO postTest(){
  4. ArticleDO model=articleDao.get(1);
  5. return model;
  6. }

在Postman(谷歌下载)http://localhost:8991/article/postTest 如下图:

Get 在方法上加入 @GetMapping  

5)参数的注解

HttpServletRequest

通常我们web方法的参数是HttpServletRequest,我们也可以在方法参数中设置HttpServletRequest参数,如下

  1. @GetMapping("/paramTest")
  2. public String paramTest(HttpServletRequest request){
  3. if(request.getParameter("d")!=null)
  4. return request.getParameter("d").toString();
  5. else
  6. return "not find param name d";
  7. }

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方法  

  1. @GetMapping("/paramNameTest")
  2. public String paramNameTest(HttpServletRequest request,@RequestParam("name") String name){
  3. if(!"".equals(name))
  4. return name;
  5. else
  6. return "not find param name ";
  7. }

@PathVariable 参数在路由中显示

  1. @GetMapping("/paramPathTest/{name}")
  2. public String paramPathTest(HttpServletRequest request,@PathVariable("name") String name){
  3. if(request.getParameter("d")!=null)
  4. return request.getParameter("d").toString();
  5. else
  6. return "not find param name d";
  7. }

@RequestBody 参数为Json

  1. @PostMapping("/jsonPostJsonTest")
  2. @ResponseBody
  3. public ArticleDO jsonPostJsonTest(@RequestBody ArticleDO articleDO){
  4. return articleDO;
  5. }

  

  

4.4.2 编写文章的新增、编辑、删除、获取列表等Controller层代码

为了统一管理返回状态,我们定义个返回的基础信息包括返回的代码、信息等信息 如下,表示统一使用Json作为返回信息

  1. {"code":1,"msg":"返回信息","data":Object}

对应的返回类

com.fishpro.springstudy.domain.Rsp.java

  1. public class Rsp extends HashMap<String ,Object> {
  2. private static final long serialVersionUID = 1L;
  3.  
  4. public Rsp() {
  5. put("code", 0);
  6. put("msg", "操作成功");
  7. }
  8.  
  9. public static Rsp error() {
  10. return error(1, "操作失败");
  11. }
  12.  
  13. public static Rsp error(String msg) {
  14. return error(500, msg);
  15. }
  16.  
  17. public static Rsp error(int code, String msg) {
  18. Rsp r = new Rsp();
  19. if(msg==null)
  20. {
  21. msg="发生错误";
  22. }
  23. r.put("code", code);
  24. r.put("msg", msg);
  25. return r;
  26. }
  27.  
  28. public static Rsp ok(String msg) {
  29. Rsp r = new Rsp();
  30. r.put("msg", msg);
  31. return r;
  32. }
  33.  
  34. public static Rsp ok(Map<String, Object> map) {
  35. Rsp r = new Rsp();
  36. r.putAll(map);
  37. return r;
  38. }
  39.  
  40. public static Rsp ok() {
  41. return new Rsp();
  42. }
  43.  
  44. @Override
  45. public Rsp put(String key, Object value) {
  46. super.put(key, value);
  47. return this;
  48. }
  49. }

  

在ArticleController.java里面,我们编写 相关的方法,全部的java代码如下:

注意:这里我们不研究分页的方法(后面讲)。  

  1. /**
  2. * 文章首页 存放列表页面
  3. * */
  4. @GetMapping()
  5. String Article(){
  6. return "article/index";
  7. }
  8.  
  9. /**
  10. * 获取文章列表数据 不考虑分页
  11. * */
  12. @ResponseBody
  13. @GetMapping("/list")
  14. public List<ArticleDO> list(@RequestParam Map<String, Object> params){
  15. //查询列表数据
  16. List<ArticleDO> articleList = articleService.list(params);
  17.  
  18. return articleList;
  19. }
  20.  
  21. /**
  22. * 文章添加页面的路由
  23. * */
  24. @GetMapping("/add")
  25. String add(){
  26. return "article/add";
  27. }
  28.  
  29. /**
  30. * 文章编辑页面的路由
  31. * */
  32. @GetMapping("/edit/{id}")
  33. String edit(@PathVariable("id") Integer id,Model model){
  34. ArticleDO article = articleService.get(id);
  35. model.addAttribute("article", article);
  36. return "article/edit";
  37. }
  38.  
  39. /**
  40. * Post方法,保存数据 这里不考虑权限
  41. */
  42. @ResponseBody
  43. @PostMapping("/save")
  44. public Rsp save(ArticleDO article){
  45. if(articleService.save(article)>0){
  46. return Rsp.ok();
  47. }
  48. return Rsp.error();
  49. }
  50. /**
  51. * Post方法,修改数据 这里不考虑权限
  52. */
  53. @ResponseBody
  54. @RequestMapping("/update")
  55. public Rsp update( ArticleDO article){
  56. articleService.update(article);
  57. return Rsp.ok();
  58. }
  59.  
  60. /**
  61. * Post方法,删除数据 这里不考虑权限
  62. */
  63. @PostMapping( "/remove")
  64. @ResponseBody
  65. public Rsp remove( Integer id){
  66. if(articleService.remove(id)>0){
  67. return Rsp.ok();
  68. }
  69. return Rsp.error();
  70. }
  71.  
  72. /**
  73. * Post方法,批量删除数据 这里不考虑权限
  74. */
  75. @PostMapping( "/batchRemove")
  76. @ResponseBody
  77. public Rsp remove(@RequestParam("ids[]") Integer[] ids){
  78. articleService.batchRemove(ids);
  79. return Rsp.ok();
  80. }

  

说明:

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中添加依赖

  1. <!-- 模板引擎 Thymeleaf 依赖 -->
  2. <dependency>
  3. <groupId>org.springframework.boot</groupId>
  4. <artifactId>spring-boot-starter-thymeleaf</artifactId>
  5. </dependency>
  6. <!--thymeleaf 兼容非严格的html5-->
  7. <dependency>
  8. <groupId>net.sourceforge.nekohtml</groupId>
  9. <artifactId>nekohtml</artifactId>
  10. </dependency>

  

4.5.2 配置Thymeleaf

编辑 application.yml

  1. spring:
  2. thymeleaf:
  3. mode: LEGACYHTML5
  4. cache: false
  5. 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方法,在浏览器中验证

保存新增数据代码

  1. function save() {
  2. $.ajax({
  3. cache : true,
  4. type : "POST",
  5. url : "/article/save",
  6. data : $('#signupForm').serialize(),// 你的formid
  7. async : false,
  8. error : function(request) {
  9. parent.layer.alert("Connection error");
  10. },
  11. success : function(data) {
  12. if (data.code == 0) {
  13. parent.layer.msg("操作成功");
  14. parent.reLoad();
  15. var index = parent.layer.getFrameIndex(window.name); // 获取窗口索引
  16. parent.layer.close(index);
  17.  
  18. } else {
  19. parent.layer.alert(data.msg)
  20. }
  21.  
  22. }
  23. });
  24. }

  

 注意:本页面没有用到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这里不做细化,后面单独实践。

  1. <form class="form-horizontal m-t" id="signupForm">
  2. <input id="id" name="id" th:value="${article.id}" type="hidden">
  3. <div class="form-group">
  4. <label class="col-sm-3 control-label"></label>
  5. <div class="col-sm-8">
  6. <input id="title" name="title" th:value="${article.title}" class="form-control" type="text">
  7. </div>
  8. </div>
  9. <div class="form-group">
  10. <label class="col-sm-3 control-label"></label>
  11. <div class="col-sm-8">
  12. <input id="content" name="content" th:value="${article.content}" class="form-control" type="text">
  13. </div>
  14. </div>
  15. <div class="form-group">
  16. <label class="col-sm-3 control-label"></label>
  17. <div class="col-sm-8">
  18. <input id="postStatus" name="postStatus" th:value="${article.postStatus}" class="form-control" type="text">
  19. </div>
  20. </div>
  21. <div class="form-group">
  22. <div class="col-sm-8 col-sm-offset-3">
  23. <button type="submit" class="btn btn-primary">提交</button>
  24. </div>
  25. </div>
  26. </form>

  

3)  建立数据路由:ArticleController中编写bootstrap-table的ajax请求方法 update

4)运行:编写页面的ajax方法,在浏览器中验证

  1. function update() {
  2. $.ajax({
  3. cache : true,
  4. type : "POST",
  5. url : "/article/update",
  6. data : $('#signupForm').serialize(),// 你的formid
  7. async : false,
  8. error : function(request) {
  9. parent.layer.alert("Connection error");
  10. },
  11. success : function(data) {
  12. if (data.code == 0) {
  13. parent.layer.msg("操作成功");
  14. parent.reLoad();
  15. var index = parent.layer.getFrameIndex(window.name); // 获取窗口索引
  16. parent.layer.close(index);
  17.  
  18. } else {
  19. parent.layer.alert(data.msg)
  20. }
  21.  
  22. }
  23. });
  24. }

  

4.5.7 删除文章功能

因为删除不需要单独编写界面,流程与新增、编辑都不一样,删除直接在列表页面进行触发。

1)  建立数据路由:ArticleController中编写bootstrap-table的ajax请求方法 remove

2)运行:编写页面的ajax方法,在浏览器中验证

  1. function remove(id) {
  2. layer.confirm('确定要删除选中的记录?', {
  3. btn : [ '确定', '取消' ]
  4. }, function() {
  5. $.ajax({
  6. url : prefix+"/remove",
  7. type : "post",
  8. data : {
  9. 'id' : id
  10. },
  11. success : function(r) {
  12. if (r.code==0) {
  13. layer.msg(r.msg);
  14. reLoad();
  15. }else{
  16. layer.msg(r.msg);
  17. }
  18. }
  19. });
  20. })
  21. }

  

总结:编写代码工作实际上是枯燥无味的,实际上面的,三层结构代码是可以全部自动生成的,没有必要手动来编写,只不过,在这里,拿出来讲解说明部分原理。

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完美结合)

  1. <!-- ehchache -->
  2. <dependency>
  3. <groupId>org.springframework.boot</groupId>
  4. <artifactId>spring-boot-starter-cache</artifactId>
  5. </dependency>
  6. <dependency>
  7. <groupId>net.sf.ehcache</groupId>
  8. <artifactId>ehcache</artifactId>
  9. </dependency>
  10. <!--shiro -->
  11. <dependency>
  12. <groupId>org.apache.shiro</groupId>
  13. <artifactId>shiro-core</artifactId>
  14. <version>1.3.2</version>
  15. </dependency>
  16. <dependency>
  17. <groupId>org.apache.shiro</groupId>
  18. <artifactId>shiro-spring</artifactId>
  19. <version>1.3.2</version>
  20. </dependency>
  21. <!-- shiro ehcache -->
  22. <dependency>
  23. <groupId>org.apache.shiro</groupId>
  24. <artifactId>shiro-ehcache</artifactId>
  25. <version>1.3.2</version>
  26. </dependency>
  27. <dependency>
  28. <groupId>com.github.theborakompanioni</groupId>
  29. <artifactId>thymeleaf-extras-shiro</artifactId>
  30. <version>1.2.1</version>
  31. </dependency>

ehcache配置

ehcache 需要在resources下新建config文件夹,并新建ehcache.xml文配置文件

  1. <?xml version="1.0" encoding="UTF-8"?>
  2. <ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  3. xsi:noNamespaceSchemaLocation="http://ehcache.org/ehcache.xsd"
  4. updateCheck="false">
  5. <diskStore path="java.io.tmpdir/Tmp_EhCache" />
  6. <defaultCache eternal="false" maxElementsInMemory="1000"
  7. overflowToDisk="false" diskPersistent="false" timeToIdleSeconds="0"
  8. timeToLiveSeconds="600" memoryStoreEvictionPolicy="LRU" />
  9. <cache name="role" eternal="false" maxElementsInMemory="10000"
  10. overflowToDisk="false" diskPersistent="false" timeToIdleSeconds="0"
  11. timeToLiveSeconds="0" memoryStoreEvictionPolicy="LFU" />
  12. </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,拥有一些权限。

  1. /**
  2. * 授权 假设
  3. * system:article:index 列表
  4. * system:article:add 增加权限
  5. * system:article:edit 修改权限
  6. * system:article:remove 删除权限
  7. * system:article:batchRemove 批量删除权限
  8. * */
  9. @Override
  10. protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
  11. Long userId= ShiroUtils.getUserId();
  12. Set<String> permissions=new HashSet<>();
  13. permissions.add("system:article:index");
  14. permissions.add("system:article:add");
  15. permissions.add("system:article:edit");
  16. permissions.add("system:article:remove");
  17. permissions.add("system:article:batchRemove");
  18.  
  19. SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
  20. info.setStringPermissions(permissions);
  21. return info;
  22. }
  23.  
  24. /**
  25. * 认证 给出一个假设的admin用户
  26. * */
  27. @Override
  28. protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
  29. String username=(String)authenticationToken.getPrincipal();
  30. Map<String ,Object> map=new HashMap<>(16);
  31. map.put("username",username);
  32. String password =new String((char[]) authenticationToken.getCredentials());
  33.  
  34. if(!"admin".equals(username) || !"1234569".equals(password)){
  35.  
  36. throw new IncorrectCredentialsException("账号或密码不正确");
  37. }
  38. UserDO user=new UserDO();
  39. user.setId(1L);
  40. user.setUsername(username);
  41. user.setPassword(password);
  42.  
  43. SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(user, password, getName());
  44. return info;
  45. }

3)在shiro包名下,新建一个ShiroUtils.java的类,作为公用的类

  1. @Autowired
  2. private static SessionDAO sessionDAO;
  3.  
  4. public static Subject getSubjct() {
  5. return SecurityUtils.getSubject();
  6. }
  7. public static UserDO getUser() {
  8. Object object = getSubjct().getPrincipal();
  9. UserDO userDO=new UserDO();
  10. return (UserDO)object;
  11. }
  12. public static Long getUserId() {
  13. return getUser().getId();
  14. }
  15.  
  16. public static void logout() {
  17. getSubjct().logout();
  18. }
  19.  
  20. public static List<Principal> getPrinciples() {
  21. List<Principal> principals = null;
  22. Collection<Session> sessions = sessionDAO.getActiveSessions();
  23. return principals;
  24. }

4)在shiro包名下新建BDSessionListener.java,实现 SessionListener接口

  1. private final AtomicInteger sessionCount = new AtomicInteger(0);
  2.  
  3. @Override
  4. public void onStart(Session session) {
  5. sessionCount.incrementAndGet();
  6. }
  7.  
  8. @Override
  9. public void onStop(Session session) {
  10. sessionCount.decrementAndGet();
  11. }
  12.  
  13. @Override
  14. public void onExpiration(Session session) {
  15. sessionCount.decrementAndGet();
  16.  
  17. }
  18.  
  19. public int getSessionCount() {
  20. return sessionCount.get();
  21. }

  

  

  

5)在1)中的包名 config下增加类ShiroConfig.java

详细代码见 源码下载

  1. /**
  2. * shiroFilterFactoryBean 实现过滤器过滤
  3. * setFilterChainDefinitionMap 表示设置可以访问或禁止访问目录
  4. * @param securityManager 安全管理器
  5. * */
  6. @Bean
  7. ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager) {
  8. ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
  9. shiroFilterFactoryBean.setSecurityManager(securityManager);
  10. //设置登录页面
  11. shiroFilterFactoryBean.setLoginUrl("/login");
  12. //登录后的页面
  13. shiroFilterFactoryBean.setSuccessUrl("/article/index");
  14. //未认证页面提示
  15. shiroFilterFactoryBean.setUnauthorizedUrl("/403");
  16. //设置无需加载权限的页面过滤器
  17. LinkedHashMap<String, String> filterChainDefinitionMap = new LinkedHashMap<>();
  18. filterChainDefinitionMap.put("/fonts/**", "anon");
  19. filterChainDefinitionMap.put("/css/**", "anon");
  20. filterChainDefinitionMap.put("/js/**", "anon");
  21. filterChainDefinitionMap.put("/index", "anon");
  22. //authc 有权限
  23. //filterChainDefinitionMap.put("/**", "authc");
  24. filterChainDefinitionMap.put("/**", "authc");
  25. //设置过滤器
  26. shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
  27. return shiroFilterFactoryBean;
  28. }

  

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包名下)

  1. @GetMapping("/login")
  2. public String login(){
  3. return "/login";
  4. }
  1. /**
    * 登录按钮对应的 服务端api
    * @param username 用户名
    * @param password 用户密码
    * @return Rsp 返回成功或失败 Json格式
    * */
    @ResponseBody
    @PostMapping("/login")
    public Rsp ajaxLogin(@RequestParam String username, @RequestParam String password){
  2.  
  3. 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中加入依赖

这个已经有了

  1. <dependency>
  2. <groupId>org.springframework.boot</groupId>
  3. <artifactId>spring-boot-starter-test</artifactId>
  4. <scope>test</scope>
  5. </dependency>

4.7.4 编写基于Controller的单元测试

  1. @RunWith(SpringRunner.class)
  2. @SpringBootTest(classes = SpringstudyApplication.class)
  3. @AutoConfigureMockMvc
  4. public class ArticleControllerTests {
  5.  
  6. private URL base;
  7. //定义mockmvc
  8. private MockMvc mvc;
  9.  
  10. //注入WebApplicationContext
  11. @Autowired
  12. private WebApplicationContext webApplicationContext;
  13.  
  14. /**
  15. * 在测试之前 初始化mockmvc
  16. * */
  17. @Before
  18. public void testBefore() throws Exception{
  19. String url = "http://localhost:8991";
  20. this.base = new URL(url);
  21. //mvc = MockMvcBuilders.standaloneSetup(new ArticleController()).build();
  22. mvc = MockMvcBuilders.webAppContextSetup(webApplicationContext).build();
  23. }
  24. @After
  25. public void testAfter(){
  26. System.out.println("测试后");
  27. }
  28.  
  29. /**
  30. * 使用一个测试
  31. * */
  32. @Test
  33. public void saveTest() throws Exception{
  34. MultiValueMap<String, String> map = new LinkedMultiValueMap<>();
  35. map.add("title", "是时候认真学习SpringBoot了");
  36. map.add("content", "是时候认真学习SpringBoot了");
  37. mvc.perform(MockMvcRequestBuilders.post("/article/save").accept(MediaType.ALL)
  38. .params(map))
  39. .andExpect(MockMvcResultMatchers.status().isOk())
  40. .andDo(MockMvcResultHandlers.print());
  41. }
  42. }

  

问题:因为没有使用过MockMvc,总是测试失败,其实对于陌生的功能点,最好找个简明的知识点学习下。

4.8 加入Web全局拦截器WebMvcConfigurer

通常我们在程序中需要全局处理包括

1)时间格式化问题

2)跨域请求问题

3)路由适配大小写问题

等等,这些问题,不可能在每个页面每个功能的时候一一去做处理,这样工作繁琐,并且容易忘记处理。这里需要加入全局配置。

在Spring Boot 2.0 (Spring 5.0)中已经取消了 WebMvcConfigurerAdapter

  1. @Configuration
  2. public class WebConfigurer implements WebMvcConfigurer {
  3.  
  4. /**
  5. * 注入路径匹配规则 忽略URL大小写
  6. * */
  7. @Override
  8. public void configurePathMatch(PathMatchConfigurer configurer) {
  9. org.springframework.util.AntPathMatcher matcher=new org.springframework.util.AntPathMatcher();
  10. matcher.setCachePatterns(false);
  11. configurer.setPathMatcher(matcher);
  12. }
  13.  
  14. /**
  15. * 支持跨域提交
  16. * */
  17. @Override
  18. public void addCorsMappings(CorsRegistry registry) {
  19.  
  20. registry.addMapping("/**")
  21. .allowCredentials(true)
  22. .allowedHeaders("*")
  23. .allowedOrigins("*")
  24. .allowedMethods("*");
  25.  
  26. }
  27. }

  

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)能不能像增加注解一样指定哪些类或方法使用日志

具体配置如下:

  1. #slf4j日志配置 logback配置见 resources/logback-spring.xml
  2. logging:
  3. level:
  4. root: error
  5. com.fishpor.springstudy: info

 logback配置则使用xml,具体路径是 resources/logback-spring.xml ,没有此文件则新建文件,加入如下代码

  1. <?xml version="1.0" encoding="UTF-8"?>
  2. <configuration scan="true" scanPeriod="60 seconds" debug="false">
  3. <contextName>logback</contextName>
  4. <!--输出到控制台-->
  5. <appender name="console" class="ch.qos.logback.core.ConsoleAppender">
  6. <encoder>
  7. <pattern>%d{HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n</pattern>
  8. </encoder>
  9. </appender>
  10.  
  11. <!--按天生成日志-->
  12. <appender name="logFile" class="ch.qos.logback.core.rolling.RollingFileAppender">
  13. <Prudent>true</Prudent>
  14. <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
  15. <FileNamePattern>
  16. applog/%d{yyyy-MM-dd}/%d{yyyy-MM-dd}.log
  17. </FileNamePattern>
  18. </rollingPolicy>
  19. <layout class="ch.qos.logback.classic.PatternLayout">
  20. <Pattern>
  21. %d{yyyy-MM-dd HH:mm:ss} -%msg%n
  22. </Pattern>
  23. </layout>
  24. </appender>
  25.  
  26. <logger name="com.glsafesports.pine" additivity="false">
  27. <appender-ref ref="console"/>
  28. <appender-ref ref="logFile" />
  29. </logger>
  30.  
  31. <root level="error">
  32. <appender-ref ref="console"/>
  33. <appender-ref ref="logFile" />
  34. </root>
  35. </configuration>

  

4.9.2 在代码中应用

在ArticleController中加入测试方法

  1. /**
  2. * 测试 log
  3. * */
  4. @GetMapping("/log")
  5. @ResponseBody
  6. public String log(){
  7. logger.info("info:");
  8. logger.error("info:");
  9. logger.warn("info:");
  10. return "log";
  11. }

4.9.3 运行效果

  

4.10 加入缓存功能

缓存也是我们系统中常用的功能,这里我们使用比较简单的 ehcache

另外时下更多的使用 redis 来作为缓存,这个后面单独实战。

4.10.1 在Pom.xml中加入依赖

前面介绍Shiro的时候已经

4.10.2 配置缓存

在前介绍过 编写resources\config\ehcache.xml

  1. <?xml version="1.0" encoding="UTF-8"?>
  2. <ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  3. xsi:noNamespaceSchemaLocation="http://ehcache.org/ehcache.xsd"
  4. updateCheck="false">
  5. <diskStore path="java.io.tmpdir/Tmp_EhCache" />
  6. <defaultCache eternal="false" maxElementsInMemory="1000"
  7. overflowToDisk="false" diskPersistent="false" timeToIdleSeconds="0"
  8. timeToLiveSeconds="600" memoryStoreEvictionPolicy="LRU" />
  9. <cache name="role" eternal="false" maxElementsInMemory="10000"
  10. overflowToDisk="false" diskPersistent="false" timeToIdleSeconds="0"
  11. timeToLiveSeconds="0" memoryStoreEvictionPolicy="LFU" />
  12. </ehcache>

  

在配置Shiro的时候,在ShiroConfig中配置过

这里在config包名下建立EhCacheConfig.java

  1. @Configuration
  2. @EnableCaching
  3. public class EhCacheConfig {
  4.  
  5. @Bean
  6. public EhCacheCacheManager ehCacheCacheManager(EhCacheManagerFactoryBean bean){
  7. return new EhCacheCacheManager(bean.getObject());
  8. }
  9.  
  10. @Bean
  11. public EhCacheManagerFactoryBean ehCacheManagerFactoryBean(){
  12. EhCacheManagerFactoryBean cacheManagerFactoryBean=new EhCacheManagerFactoryBean();
  13. cacheManagerFactoryBean.setConfigLocation(new ClassPathResource("config/ehcache.xml"));
  14. cacheManagerFactoryBean.setShared(true);
  15. return cacheManagerFactoryBean;
  16. }
  17. }

  

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)运行查看效果

Spring Boot 2.x 综合示例-整合thymeleaf、mybatis、shiro、logging、cache开发一个文章发布管理系统的更多相关文章

  1. Spring Boot 2.0 快速集成整合消息中间件 Kafka

    欢迎关注个人微信公众号: 小哈学Java, 每日推送 Java 领域干货文章,关注即免费无套路附送 100G 海量学习.面试资源哟!! 个人网站: https://www.exception.site ...

  2. 解决Spring Boot(2.1.3.RELEASE)整合spring-data-elasticsearch3.1.5.RELEASE报NoNodeAvailableException[None of the configured nodes are available

    Spring Boot(2.1.3.RELEASE)整合spring-data-elasticsearch3.1.5.RELEASE报NoNodeAvailableException[None of ...

  3. Spring Boot 2.x基础教程:使用MyBatis的XML配置方式

    上一篇我们介绍了如何在Spring Boot中整合我们国人最常用的MyBatis来实现对关系型数据库的访问.但是上一篇中使用了注解方式来实现,而对于很多MyBatis老用户还是习惯于XML的开发方式, ...

  4. Spring Boot 监听 Activemq 中的特定 topic ,并将数据通过 RabbitMq 发布出去

    1.Spring Boot 和 ActiveMQ .RabbitMQ 简介 最近因为公司的项目需要用到 Spring Boot , 所以自学了一下, 发现它与 Spring 相比,最大的优点就是减少了 ...

  5. Eureka心跳健康检查机制和Spring boot admin 节点状态一直为DOWN的排查(忽略某一个节点的健康检查)

    https://www.jdon.com/springcloud/eureka-health-monitoring.html 运行阶段执行健康检查的目的是为了从Eureka服务器注册表中识别并删除不可 ...

  6. spring boot中连接数据库报错500(mybatis)

    spring boot中连接数据库报错500(mybatis) pom.xml中的依赖 <!-- 集成mybatis--> <dependency> <groupId&g ...

  7. Spring Boot (四)模板引擎Thymeleaf集成

    一.Thymeleaf介绍 Thymeleaf是一种Java XML / XHTML / HTML5模板引擎,可以在Web和非Web环境中使用.它更适合在基于MVC的Web应用程序的视图层提供XHTM ...

  8. Spring Boot2 系列教程 (十二) | 整合 thymeleaf

    前言 如题,今天介绍 Thymeleaf ,并整合 Thymeleaf 开发一个简陋版的学生信息管理系统. SpringBoot 提供了大量模板引擎,包含 Freemarker.Groovy.Thym ...

  9. Spring Boot Ftp Client 客户端示例支持断点续传

    本章介绍 Spring Boot 整合 Ftpclient 的示例,支持断点续传 本项目源码下载 1 新建 Spring Boot Maven 示例工程项目 注意:是用来 IDEA 开发工具 File ...

随机推荐

  1. Java接口的实例应用:致敬我的偶像——何塞·穆里尼奥

    文/沉默王二 曹操在<短歌行>中为杜康酒打过一个价值一亿个亿的广告——“何以解忧,唯有杜康”,我替曹操感到惋惜的是他本人并不会收到这笔不菲的代言费.想一想,要是三国时期的明星人物们有这个代 ...

  2. curl zip上传并且解压

     上传文件: /** * 上传文件 * @param string $file 文件路径 */ function FileUpload($file){ $data = array('file'=> ...

  3. Content Security Policy (CSP) 介绍

    当我不经意间在 Twitter 页面 view source 后,发现了惊喜. <!DOCTYPE html> <html lang="en"> <h ...

  4. (二)通过fork编写一个简单的并发服务器

    概述 那么最简单的服务端并发处理客户端请求就是,父进程用监听套接字监听,当有连接过来时那么监听套接字就变成了已连接套接字(源和目的的IP和端口都包含了),这时候就可以和客户端通信,但此时其他客户端无法 ...

  5. JDK源码分析(5)之 HashMap 相关

    HashMap作为我们最常用的数据类型,当然有必要了解一下他内部是实现细节.相比于 JDK7 在JDK8 中引入了红黑树以及hash计算等方面的优化,使得 JDK8 中的HashMap效率要高于以往的 ...

  6. 使用MaxCompute Java SDK运行安全相关命令

    使用MaxCompute Console的同学,可能都使用过MaxCompute安全相关的命令.官方文档上有详细的MaxCompute安全指南,并给出了安全相关语句汇总.   简而言之,权限管理.列级 ...

  7. github总结(4)--关于git reset --hard这个命令的惨痛教训

    背景叙述: 前几天,上传自己的个站到git上的时候,手欠脑发晕的用了次git reset --hard xxxxxx 命令.由于只在线上传入了一个index.html页面(自己都不知道自己咋想的,就这 ...

  8. SpringBoot整合系列-整合H2

    原创作品,可以转载,但是请标注出处地址:https://www.cnblogs.com/V1haoge/p/9959855.html SpringBoot整合H2内存数据库 一般我们在测试的时候习惯于 ...

  9. msf中exploit的web_delivery模块

    背景:目标设备存在远程文件包含漏洞或者命令注入漏洞,想在目标设备上加载webshell,但不想在目标设备硬盘上留下任何webshell文件信息 解决思路:让目标设备从远端服务器加载webshell代码 ...

  10. cocos creator主程入门教程(一)—— 初识creator

    五邑隐侠,本名关健昌,10年游戏生涯,现隐居五邑.本系列文章以TypeScript为介绍语言. 我们在cocos creator新建一个Hello TypeScript项目,都会有一个assets/S ...