SpringBoot学习笔记

文章目录

Springboot

快速入门

创建项目

编写启动器和TestDemo

建议启动类以*Application的命名结尾,并放置在顶层包(cn.hxj.projectname.*.Application)的下面,方便扫描注解扫描

// 当接收外部的web请求时会考虑给标有该注解类
// @RestController = @Controller + @@ResponseBody 源码也是这2个注解的组合
@RestController
// 近似于 @ComponentScan + @EnableAutoConfiguration(可以使用这2个注解替代)
@SpringBootApplication
//建议放在顶层包下
public class SpringbootTemplateApplication {

    @RequestMapping("/")
    String home() {
        System.out.println("Hello Springboot");
        return "Hello World!";
    }

    public static void main(String[] args) {
        // SpringApplication.run(SpringbootTemplateApplication.class, args);

        SpringApplication app = new SpringApplication(SpringbootTemplateApplication.class);
        app.setBannerMode(Banner.Mode.OFF);
        app.run(args);

        // new SpringApplicationBuilder()
        // 		// .bannerMode(Banner.Mode.OFF) // 推荐使用yaml配置
        // 		.sources(SpringbootTemplateApplication.class)
        // 		.run(args);
    }

}

默认pom文件

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>
	<parent>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-parent</artifactId>
		<version>2.5.1</version>
		<relativePath/> <!-- lookup parent from repository -->
	</parent>

	<groupId>cn.hxj</groupId>
	<artifactId>springboot-template</artifactId>
	<version>0.0.1-SNAPSHOT</version>
	<name>springboot-template</name>
	<description>Demo project for Spring Boot study</description>
	<properties>
		<java.version>13</java.version>
	</properties>

	<dependencies>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</artifactId>
		</dependency>

		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-test</artifactId>
			<scope>test</scope>
		</dependency>
	</dependencies>

	<build>
		<plugins>
		<!-- to create an executable jar, we need to add the spring-boot-maven-plugin to our pom.xml. -->
			<plugin>
				<groupId>org.springframework.boot</groupId>
				<artifactId>spring-boot-maven-plugin</artifactId>
			</plugin>
		</plugins>
	</build>

</project>

图标控制

自定义新图标

在resources目录下新增banner.txt,图案生成可搜索“ASCII 图案生成”,或直接使用该网站

banner.txt

Application Version: ${application.version}${application.formatted-version}
Spring Boot Version: ${spring-boot.version}${spring-boot.formatted-version}
///
// why-use step  template  top-to-bottom //
///

控制图标显示

代码设置
SpringApplication app = new SpringApplication(SpringbootTemplateApplication.class);
app.setBannerMode(Banner.Mode.OFF);
app.run(args);

// 流式编程
// new SpringApplicationBuilder()
// 		// .bannerMode(Banner.Mode.OFF) // 推荐使用yaml配置
// 		.sources(SpringbootTemplateApplication.class)
// 		.run(args);
application配置文件设置
spring:
  main:
    banner-mode: "console"

日志

日志门面:slf4j
日志实现:log4j、logback、log4j2、java.utl.logging
springboot默认使用:slf4j + logback
slf4j可以整合其他日志,通过桥接器等模式
日志技术一般使用了:门面模式、工厂模式等设计模式
日志可以用来替代打印语句

日志等级

debug:替代原来的输出语句
info:执行的方法流程等信息
warn:警告信息
error:替代try-catch里的打印信息

springboot配置logback日志

logback-spring.xml

<?xml version="1.0" encoding="UTF-8"?>
<!--
    每隔30s自动检查配置文件是否改变,改变的话重新加载配置
    configuration: scan="true" scanPeriod="5 seconds"
 -->
<configuration>
    <!-- 这个是引入springboot的defaults.xml配置 -->
    <!--<include resource="org/springframework/boot/logging/logback/defaults.xml"/>-->
    <conversionRule conversionWord="clr" converterClass="org.springframework.boot.logging.logback.ColorConverter" />
    <conversionRule conversionWord="wex" converterClass="org.springframework.boot.logging.logback.WhitespaceThrowableProxyConverter" />
    <conversionRule conversionWord="wEx" converterClass="org.springframework.boot.logging.logback.ExtendedWhitespaceThrowableProxyConverter" />

    <property name="CONSOLE_LOG_PATTERN"
              value="${CONSOLE_LOG_PATTERN:-%clr(%d{${LOG_DATEFORMAT_PATTERN:-HH:mm}}){faint} %clr(${LOG_LEVEL_PATTERN:-%5p}) %clr(:){faint} %m %n %clr(${PID:- }){magenta} %clr(---){faint} %clr([%15.15t]){faint} %clr(%-40.40logger{39}){cyan} %n${LOG_EXCEPTION_CONVERSION_WORD:-%wEx}}"/>
    <property name="CONSOLE_LOG_CHARSET" value="${CONSOLE_LOG_CHARSET:-default}"/>
    <property name="FILE_LOG_PATTERN" value="${FILE_LOG_PATTERN:-%d{${LOG_DATEFORMAT_PATTERN:-yyyy-MM-dd HH:mm:ss.SSS}} ${LOG_LEVEL_PATTERN:-%5p} ${PID:- } --- [%t] %-40.40logger{39} : %m%n${LOG_EXCEPTION_CONVERSION_WORD:-%wEx}}"/>
    <property name="FILE_LOG_CHARSET" value="${FILE_LOG_CHARSET:-default}"/>
    <!-- 自定义 -->

    <property name="log_dir" value="logs"/>

    <appender name="consoleAppender" class="ch.qos.logback.core.ConsoleAppender">
        <target>System.out</target> <!-- 默认标准输出,可以改为err -->
        <encoder>
            <charset>UTF-8</charset>
            <pattern>${CONSOLE_LOG_PATTERN}</pattern>
            <!--<pattern>[%p] %d{yyyy-MM-dd HH:mm:ss} %m %n</pattern>-->
        </encoder>
    </appender>

    <appender name="rollingFileAppender" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <file>${log_dir}/roll_logback.log</file>
        <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
            <pattern>${FILE_LOG_PATTERN}</pattern>
            <!--<pattern>[%p] %d{yyyy-MM-dd HH:mm:ss} %m %n</pattern>-->
        </encoder>
        <rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
            <!-- 按天或文件大小进行拆分, 加上.gz/.zip就是压缩包 -->
            <fileNamePattern>${log_dir}/rolling.%d{yyyy-MM-dd}.%i.log.gz</fileNamePattern>
            <!--<maxHistory>30</maxHistory>-->
            <!-- 按文件大小拆分 -->
            <maxFileSize>8MB</maxFileSize>
            <!-- 最大容量 -->
            <totalSizeCap>1GB</totalSizeCap>
            <!-- 清除 -->
            <cleanHistoryOnStart>true</cleanHistoryOnStart>
        </rollingPolicy>
        <!-- 过滤器 -->
        <!--
        <filter class="ch.qos.logback.classic.filter.ThresholdFilter">
            <level>ERROR</level>
        </filter>
        -->
    </appender>

    <appender name="asyncAppender" class="ch.qos.logback.classic.AsyncAppender">
        <!-- 指定某个 appender 启用异步模式 -->
        <appender-ref ref="consoleAppender"/>
        <appender-ref ref="rollingFileAppender"/>
    </appender>

    <root level="info"> <!-- 一般开发info,上线error -->
        <!-- <appender-ref ref="asyncAppender" />--> <!-- 小项目没必要异步 -->
        <appender-ref ref="consoleAppender"/>
        <appender-ref ref="rollingFileAppender"/>
    </root>

    <!-- 优先级高于root -->
    <logger name="cn.hxj.springboottemplate.dao" level="DEBUG"/>
    <logger name="cn.hxj.springboottemplate.controller" level="DEBUG"/>
    <logger name="cn.hxj.springboottemplate.service" level="DEBUG"/>

</configuration>

springboot使用日志(配合IDEA-live-templates)

一般都使用slf4j的日志门面api,而不使用logback的api,为了便于日志实现的切换
由于springboot提供了其他日志的转移器jar,所以可以把其他日志实现整合到slf4j+logback

在这里插入图片描述
在这里插入图片描述

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

@RestController
@RequestMapping("/hello")
public class HelloController {

    private static final Logger logger = LoggerFactory.getLogger(HelloController.class);//logc 补全

    @GetMapping("/sayHi")
    public String sayHi(int i) {
        logger.info("进入{}类的{}方法", "HelloController", "sayHi");//logm 补全
        logger.info("i = " + i);// logv 补全
        logger.info("i = " + i);// logp 补全
        try {
            throw new Exception("抛出异常");
        } catch (Exception e) {
            logger.error(e.getMessage());
        }
        logger.info("结束执行sayHi方法");
        return "Hello";
    }
}
模板配置

在这里插入图片描述

切换配置文件

application.为主配置文件,application-*.为子配置文件

application.yaml

spring:
  profiles:
    # 值为application-*的*
    active: dev

application-dev.yaml,同理可以配置testprod环境的切换

# 注意必须是以 application-*.yaml 这种格式
spring:
  main:
    banner-mode: "console"
server:
  port: 8080
  servlet:
    context-path: /springboot-template

Freemarker & Thymeleaf 视图技术集成

Freemarker视图集成(建议使用)

​ SpringBoot内部支持Freemarker视图技术的集成,并提供了自动化配置类FreeMarkerAutoConfiguration,借助自动化配置可以很方便的集成Freemarker基础到SpringBoot环境中。这里借助入i ]项目引入Freemarker环境配置。

  • Starter坐标引入

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-freemarker</artifactId>
    </dependency>
    
  • 添加Freemarker配置信息
    Freemarker默认默认视图路径为resources/templates目录(由自动化配置类FreemarkerProperties决定),该目录可以进行在application.yml中进行修改

    spring:
      freemarker:
        # 点击该key就可以进入源码看到默认路径为 "classpath:/templates/"
        template-loader-path: classpath:/templates/freemarker-view/ # 使用和jsp一样,这里就不赘述了
        # 设置视图后缀 默认值为 ftlh
        suffix: .ftl
        # 响应类型 默认值如下
        content-type: text/html
        # 设置文件编码 默认值如下
        charset: utf-8
    

在这里插入图片描述

IDEA的注释要按ctrl + shift + /

Thymeleaf视图集成

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
thymeleaf:
prefix: classpath:/templates/thymeleaf-view/
cache: false # 开发是方便调试

静态资源访问

其它资源不可以通过浏览器(外部)直接访问
注意Maven经常不编译静态资源,这时可以把target目录删除再编译

Web静态资源的默认存储位置

"classpath:/META-INF/resources/"
"classpath:/resources/" // 注意这个的意思是可以在resources目录下新建一个resources放静态资源,而不是resources可以访问
"classpath:/static/"
"classpath:/public/"

可以通过配置文件找到实现的类

spring:
  web:
    resources:
      static-locations:

在这里插入图片描述

修改静态资源位置

spring:
  web:
    resources:
      # 如果有需要建议追加而不是替换
      # 点进入可以发现是一个数组,所以可以使用value可以使用数组的形式
      static-locations:
        - "classpath:/META-INF/resources/"
        - "classpath:/resources/"
        - "classpath:/static/"
        - "classpath:/public/"
        - "classpath:/my-static/"

Spring打包与部署

​ 当项目开发完毕进行部署上线时,需要对项目进行打包操作,入门中构建的项目属于普通应用,由于SpringBoot内嵌Tomcat容器,所有打包后的jar包默认可以自行运行。

Jar包部署

自定义名称
<build>
	<finalName>springboot-template</finalName>
</build>
配置打包命令

idea下配置clean compile package -Dmaven.test.skip=true执行打包命令,target 目录得到待部署的项目文件。

该命令的意思是清理 编译 打包并跳过测试

在这里插入图片描述

选中jar包复制绝对路径,命令行执行java -jar a/b/c/xxx.jar即可

另一种打包方法
$ mvn package # to create an executable jar, we need to add the spring-boot-maven-plugin to our pom.xml
$ java -jar target/myapplication-0.0.1-SNAPSHOT.jar # 运行springboot打包的jar项目
$ mvn spring-boot:run # 或者直接运行不打包

War包部署

修改pom.xml
<version>0.0.1-SNAPSHOT</version>
<packaging>war</packaging> <!-- 默认打包方式是jar -->

仅在运行时使用内嵌的tomcat

<!-- 仅在运行使用内部tomcat -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-tomcat</artifactId>
    <scope>provided</scope>
</dependency>
修改启动器类

添加容器启动加载文件(类似于读取web.xml),这里通过继承Springooterletnitialier类并重写configure方法来实现,在部署项目时指定外部Tomcat读取项目入口方法。

配置打包命令

idea下配置clean compile package -Dmaven.test.skip=true执行打包命令,target 目录得到待部署的项目文件。

整合Mybatis

修改pom文件

<!-- 集成mybatis -->
<dependency>
    <groupId>org.mybatis.spring.boot</groupId>
    <artifactId>mybatis-spring-boot-starter</artifactId>
    <version>2.1.1</version>
</dependency>

<!-- 集成springboot分页插件 -->
<dependency>
    <groupId>com.github.pagehelper</groupId>
    <artifactId>pagehelper-spring-boot-starter</artifactId>
    <version>1.2.13</version>
</dependency>

<!-- mysql驱动 -->
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
</dependency>

<!-- druid数据源 -->
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>druid</artifactId>
    <version>1.1.6</version>
</dependency>

<!-- 字符串检验工具类 -->
<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-lang3</artifactId>
</dependency>

修改配置文件

spring:
  mvc:
    hiddenmethod:
      filter:
        enabled: true # 转换表单请求为put or delete
  datasource:
    type: com.alibaba.druid.pool.DruidDataSource
    driver-class-name: com.mysql.jdbc.Driver
    # 字符串多行书写方法
    url: "jdbc:mysql://localhost:3306/springboot_template\
      ?useSSL=true&\
      useUnicode=true&\
      characterEncoding=utf8&\
      serverTimezone=Asia/Shanghai"
    username: root
    password: root
    
# mybatis配置
mybatis:
  mapper-locations: classpath:/mappers/*.xml
  type-aliases-package: cn.hxj.springboottemplate.domain
  configuration:
    # 下滑线转驼峰
    map-underscore-to-camel-case: true

# pageHelper
pagehelper:
  helper-dialect: mysql # 方言,各种SQL语言的分页语法不尽相同

配置扫描包路径

@MapperScan("cn.hxj.springboottemplate.dao")
public class SpringbootTemplateApplication {
	// run a app
}

编程测试代码

@RestController
@RequestMapping("/user")
public class UserController {
    
    UserService userService;
	
    // 只有一个构造方法,不用@Autowried也可以自动注入
    public UserController(UserService userService) {
        this.userService = userService;
    }

    @GetMapping("/name/{userName}")
    public User findUserByUserName(@PathVariable String userName) {
        return userService.findUserByUserName(userName);
    }
}
@Service
// @SuppressWarnings("all")
public class UserService {

    // 注意不能使用@Autowried,因为会找不到bean
    // 原因:IDEA无法在编辑时获取对应的bean
    @Resource
    UserMapper userMapper;

    public User findUserByUserName(String userName) {
        return userMapper.queryUserByUserName(userName);
    }
}
// 如果要使用@Authwired注解,就要在这里加上@Repository
public interface UserMapper {
    User queryUserByUserName(String userName);
}

CRUD & 分页查询

分页对象
package cn.hxj.springboottemplate.query;

public class UserQuery {
    private Integer curPage = 1; // 当前页
    private Integer pageSize = 10; // 每页显示条数

    private String userName; // 查询条件:用户名
}
持久层对象
package cn.hxj.springboottemplate.po;

public class User implements Serializable {
    Integer userId;
    String userName;
    String userPwd;
}
ResultInfo对象
package cn.hxj.springboottemplate.vo;

public class ResultInfo {
    private Integer code = 200; // 状态码,成功默认200
    private String msg = "操作成功"; // 提示信息
    private Object result;
}
校验工具类
package cn.hxj.springboottemplate.utils;

public class AssertUtil {
    private AssertUtil() {}
    public static void isTrue(Boolean flag, String msg) {
        if(flag) {
            throw new ParamsException(msg);
        }
    }
}
参数异常类
package cn.hxj.springboottemplate.exceptions;

public class ParamsException extends RuntimeException {

    private Integer code = 300;
    private String msg = "参数异常!";

    public ParamsException() {
        super("参数异常!");
    }

    public ParamsException(String msg) {
        super("参数异常!");
        this.msg = msg;
    }

    public ParamsException(Integer code, String msg) {
        super(msg);
        this.code = code;
        this.msg = msg;
    }
	// getter & setter & tostring
}

前端
<a href="user/name/zhangsan">提交get请求</a>
<hr/>

<form method="post" action="user">
    <input type="text" name="userName">
    <input type="password" name="userPwd">
    <input type="submit" value="提交post请求">
</form>
<hr/>

<form method="post" action="user">
    <input type="hidden" name="_method" value="put">
    <input type="text" name="userId">
    <input type="text" name="userName">
    <input type="password" name="userPwd">
    <input type="submit" value="提交put请求">
</form>
<hr/>

<form method="post" action="user/userId/1">
    <input type="hidden" name="_method" value="delete">
    <!-- <input type="text" name="userId"> -->
    <input type="submit" value="提交delete请求">
</form>
<hr/>

<a href="user/page?pageSize=2&curPage=2">分页请求</a>
<hr/>
controller
@RestController
@RequestMapping("user")
public class UserController {

    UserService userService;

    public UserController(UserService userService) {
        this.userService = userService;
    }

    @GetMapping("name/{userName}")
    public User findUserByUserName(@PathVariable String userName) {
        return userService.findUserByUserName(userName);
    }

    @GetMapping("userId/{userId}")
    public User findUserByUserId(@PathVariable Integer userId) {
        return userService.findUserByUserId(userId);
    }

    @PostMapping
    public ResultInfo insert(User user) {
        ResultInfo resultInfo = new ResultInfo();
        try {
            userService.insert(user);
        } catch (ParamsException e) {
            e.printStackTrace();
            resultInfo.setCode(e.getCode());
            resultInfo.setMsg(e.getMsg());
        } catch (Exception e) {
            e.printStackTrace();
            resultInfo.setCode(500);
            resultInfo.setMsg("用户添加失败!");
        }
        return resultInfo;
    }

    @PutMapping
    public ResultInfo update(User user) {
        ResultInfo resultInfo = new ResultInfo();
        try {
            userService.update(user);
        } catch (ParamsException e) {
            e.printStackTrace();
            resultInfo.setCode(e.getCode());
            resultInfo.setMsg(e.getMsg());
        } catch (Exception e) {
            e.printStackTrace();
            resultInfo.setCode(500);
            resultInfo.setMsg("用户更新失败!");
        }
        return resultInfo;
    }

    @DeleteMapping("userId/{userId}")
    public ResultInfo delete(@PathVariable Integer userId) {
        ResultInfo resultInfo = new ResultInfo();
        try {
            userService.delete(userId);
        } catch (ParamsException e) {
            e.printStackTrace();
            resultInfo.setCode(e.getCode());
            resultInfo.setMsg(e.getMsg());
        } catch (Exception e) {
            e.printStackTrace();
            resultInfo.setCode(500);
            resultInfo.setMsg("用户删除失败!");
        }
        return resultInfo;
    }
    
    @GetMapping("page")
    public PageInfo<User> findByPage(UserQuery userQuery) {
        return userService.findByPage(userQuery);
    }
}
Service
@Service
// @SuppressWarnings("all")
public class UserService {

    // @Resource
    UserMapper userMapper;

    public UserService(UserMapper userMapper) {
        this.userMapper = userMapper;
    }

    public User findUserByUserName(String userName) {
        return userMapper.selectByUserName(userName);
    }

    public User findUserByUserId(Integer userId) {
        return userMapper.selectByUserId(userId);
    }

    public void insert(User user) {
        AssertUtil.isTrue(StringUtils.isBlank(user.getUserName()), "用户名不能为空");
        AssertUtil.isTrue(StringUtils.isBlank(user.getUserPwd()), "密码不能为空");
        AssertUtil.isTrue(userMapper.selectByUserName(user.getUserName()) != null, "用户名已存在");
        AssertUtil.isTrue(userMapper.insert(user) < 1, "用户添加失败");
    }

    public void update(User user) {
        // 更新时必须传入用户id,假设该id是当前登录用户的id
        AssertUtil.isTrue(StringUtils.isBlank(user.getUserName()), "用户名不能为空");
        AssertUtil.isTrue(StringUtils.isBlank(user.getUserPwd()), "密码不能为空");
        AssertUtil.isTrue(user.getUserId() == null, "账号未登录");
        User temp = userMapper.selectByUserName(user.getUserName());
        AssertUtil.isTrue(temp != null && !temp.getUserId().equals(user.getUserId()), "用户名已存在");
        AssertUtil.isTrue(userMapper.update(user) < 1, "更新失败");
    }

    public void delete(Integer userId) {
        AssertUtil.isTrue(userId == null || userMapper.selectByUserId(userId) == null, "用户不存在");
        AssertUtil.isTrue(userMapper.delete(userId) < 1, "删除失败");
    }
    
    public PageInfo<User> findByPage(UserQuery userQuery) {
        PageHelper.startPage(userQuery.getCurPage(), userQuery.getPageSize());
        return new PageInfo<User>(userMapper.selectByPage(userQuery));
    }
}
dao
@Repository
public interface UserMapper {
    User selectByUserName(String userName);

    User selectByUserId(Integer userId);

    int insert(User user);

    int update(User user);

    int delete(Integer userId);
    
    List<User> selectByPage(UserQuery userQuery);
}
userMapper.xml
<?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="cn.hxj.springboottemplate.dao.UserMapper">
    <select id="selectByUserName" parameterType="String" resultType="User">
        select user_id, user_name, user_pwd
        from tb_user
        where user_name = #{userName}
    </select>

    <select id="selectByUserId" parameterType="int" resultType="User">
        select user_id, user_name, user_pwd
        from tb_user
        where user_id = #{userId}
    </select>

    <insert id="insert" parameterType="User">
        insert into tb_user(user_name, user_pwd)
        values (#{userName}, #{userPwd})
    </insert>

    <update id="update" parameterType="User">
        update tb_user
        set user_name = #{userName},
            user_pwd = #{userPwd}
        where
            user_id = #{userId}
    </update>

    <delete id="delete" parameterType="int">
        delete from tb_user
        where user_id = #{userId}
    </delete>
    
    <select id="selectByPage" resultType="User">
        select user_id, user_name, user_pwd
        from tb_user
        <where>
            <if test="userName != null and userName.trim().length() > 0">
                and user_name like concat('%', #{userName}, '%')
            </if>
        </where>
    </select>
</mapper>

PageHelper实现原理

玩转spring全家桶在0x3fhxj2中,可以看看,非常nice

当然PageHelper的原理得找mybatis插件的实现原理

API文档构建工具- Swagger2

由于Spring Boot能够快速开发、便捷部署等特性,通常在使用Spring Boot构建Restful接应用时考虑到多终端的原因,这些终端会共用很多底层业务逻辑,因此我们会抽象出这样一层来同时服务于多个移动端或者Web前端。对于不同的终端公用一套接口API时,对于联调测试的时候就需要知道后端提供的接口API列表文档,对于服务端开发人员来说就需要编写接口文档,描述接口的调用地址、参数结果等,这里借助第三方构建I具Swagger2来实现API文档生成功能。

修改pom.xml文件

<!-- API构建 -->
<dependency>
    <groupId>io.springfox</groupId>
    <artifactId>springfox-swagger2</artifactId>
    <version>2.9.2</version>
</dependency>

<dependency>
    <groupId>io.springfox</groupId>
    <artifactId>springfox-swagger-ui</artifactId>
    <version>2.9.2</version>
</dependency>

添加配置类

package cn.hxj.springboottemplate.config;

@Configuration
@EnableSwagger2
public class Swagger2 {
    @Bean
    public Docket createRestApi() {
        return new Docket(DocumentationType.SWAGGER_2)
                .apiInfo(apiInfo())
                .select()
                .apis(RequestHandlerSelectors.basePackage("cn.hxj.springboottemplate.controller"))
                .paths(PathSelectors.any())
                .build();
    }

    private ApiInfo apiInfo() {
        return new ApiInfoBuilder()
                .title("用户管理接口API文档")
                .version("1.0")
                .build();
    }
}

Swagger2常用注解

@Api
@Api:用在请求的类上,说明该类的作用
    tags="说明该类的作用"
@Api(tags="APP用户注册Controller")
@ApiOperation
@ApiOperation: "用在请求方法上,说明方法的作用"
    value="说明方法的作用"
    notes="方法的备注说明"
@ApiOperation(value="用户注册", notes="手机号、密码都是必填项,年龄是选填项,但必须是数字")
@ApiImplicitParams
@ApiImplicitParams: 用于请求在方法上,包含一组参数说明
    @ApiImplicitParam: 用在 @ApiImplicitParams 注解中,指定一个请求参数的配置信息
        name: 参数名
        value: 参数的汉字说明、解释
        required: 参数是否必须传
        paramType: 参数放在哪个地方
            header --> 请求参数的获取: @RequestHeader
			query --> 请求参数的获取: @RequestParam
            path(用于restful接口) --> 请求参数的获取:@PathVariable
            body(不常用)
            from(不常用)
        dataType: 参数类型,默认String,其它值dataType="Integer"
        defaultValue: 参数的默认值
@ApiImplicitParams({
    @ApiImplicitParam(name="mobile", value="手机号", required=true, paramType="form"),
    @ApiImplicitParam(name="password", value="密码", required=true, paramType="form"),
    @ApiIMplicitParam(name="age", value="年龄", required=true, paramType="form", dataType="Integer")
})
@ApiResponses
@ApiResponses: 用于请求的方法上,表示一组响应
    @ApiResponse: 用在@ApiResponse中,一般表单一个错误的响应消息
        code: 数字,例如:404
        message: 消息,例如“请求参数没填写好”
        response: 抛出异常的类
@ApiOperation(value="select请求", notes="多个参数,多种的查询参数类型")
@ApiResponses({
    @ApiResponse(code=400, message="请求参数没填写好"),
    @ApiResponse(code=404, message="请求路径或页面跳转路径不对")
})
@ApiModel
@ApiModel: 用于响应类上,表示一个返回响应数据的信息
    (这种一般用在post创建时,使用@RequestBody这样的场景,请求参数无法使用@ApiImplicitParam注解进行描述)
@ApiModelProperty:用在属性上,描述响应类的属性
@ApiModel(description="返回响应数据")
public class RestMessage implements Serializable {
    @ApiModelProperty(value="是否成功")
    private boolean success = true;
    @ApiModelProperty(value="返回对象")
    private Object data;
    @ApiModelProperty(value="错误编码")
    private Integer code;
    @ApiModelProperty(value="错误信息")
    private String message;
}

示例

Controller层
@Api(tags = "用户模块")
@RestController
@RequestMapping("user")
public class UserController {

    UserService userService;

    public UserController(UserService userService) {
        this.userService = userService;
    }

    @ApiOperation(value = "根据用户名查找用户记录", notes = "用户名不能为空")
    @ApiImplicitParam(name = "userName", value = "用户名", required = true,
            dataType = "String", // 参数类型,默认String
            paramType = "path") // 表示参数从路径上获取
    @GetMapping("name/{userName}")
    public User findUserByUserName(@PathVariable String userName) {
        return userService.findUserByUserName(userName);
    }

    @ApiOperation(value = "根据用户ID查找用户记录", notes = "用户ID不能为空")
    @ApiImplicitParam(name = "userId", value = "用户ID", required = true,
            dataType = "Integer", // 参数类型,默认String
            paramType = "path") // 表示参数从路径上获取
    @GetMapping("userId/{userId}")
    public User findUserByUserId(@PathVariable Integer userId) {
        return userService.findUserByUserId(userId);
    }

    @ApiOperation(value = "添加用户", notes = "用户不能为空")
    @ApiImplicitParam(name = "user", value = "用户实体类",
            dataType = "User", // 参数类型,默认String
            paramType = "form") // 表示参数从表单上获取
    @PostMapping
    public ResultInfo insert(User user) {
        ResultInfo resultInfo = new ResultInfo();
        try {
            userService.insert(user);
        } catch (ParamsException e) {
            e.printStackTrace();
            resultInfo.setCode(e.getCode());
            resultInfo.setMsg(e.getMsg());
        } catch (Exception e) {
            e.printStackTrace();
            resultInfo.setCode(500);
            resultInfo.setMsg("用户添加失败!");
        }
        return resultInfo;
    }

    @ApiOperation(value = "更新用户", notes = "用户不能为空")
    @ApiImplicitParam(name = "user", value = "用户实体类",
            dataType = "User", // 参数类型,默认String
            paramType = "form") // 表示参数从表单上获取
    @PutMapping
    public ResultInfo update(User user) {
        ResultInfo resultInfo = new ResultInfo();
        try {
            userService.update(user);
        } catch (ParamsException e) {
            e.printStackTrace();
            resultInfo.setCode(e.getCode());
            resultInfo.setMsg(e.getMsg());
        } catch (Exception e) {
            e.printStackTrace();
            resultInfo.setCode(500);
            resultInfo.setMsg("用户更新失败!");
        }
        return resultInfo;
    }

    @ApiOperation(value = "根据用户ID删除用户记录", notes = "用户ID不能为空")
    @ApiImplicitParam(name = "userId", value = "用户ID", required = true,
            dataType = "Integer", // 参数类型,默认String
            paramType = "path") // 表示参数从路径上获取
    @DeleteMapping("userId/{userId}")
    public ResultInfo delete(@PathVariable Integer userId) {
        ResultInfo resultInfo = new ResultInfo();
        try {
            userService.delete(userId);
        } catch (ParamsException e) {
            e.printStackTrace();
            resultInfo.setCode(e.getCode());
            resultInfo.setMsg(e.getMsg());
        } catch (Exception e) {
            e.printStackTrace();
            resultInfo.setCode(500);
            resultInfo.setMsg("用户删除失败!");
        }
        return resultInfo;
    }

    @ApiOperation(value = "根据分页条件查询用户记录", notes = "默认第1页,一页10条记录")
    @ApiImplicitParam(name = "userQuery", value = "用户查询对象",
            dataType = "UserQuery", // 参数类型,默认String
            paramType = "form") // 表示参数从路径上获取
    @GetMapping("page")
    public PageInfo<User> findByPage(UserQuery userQuery) {
        return userService.findByPage(userQuery);
    }
}
实体类
@ApiModel(description = "用户实体类")
public class User implements Serializable {

    @ApiModelProperty(name = "userId", value = "用户ID", notes = "唯一")
    Integer userId;
    @ApiModelProperty(name = "userName", value = "用户名", notes = "唯一")
    String userName;
    String userPwd;
}

SpringBoot应用热部署

什么是热部署?

热部署,就是在应用正在运行的时候升级软件(增加业务/修改bug),却不需要重新启动应用。
大家都知道在项目开发过程中,常常会改动页面数据或者修改数据结构,为了显示改动效果,往往需要重启应用查看改变效果,其实就是重新编译生成了新的Class文件,这个文件里记录着和代码等对应的各种信息,然后Class文件将被虚拟机的ClassLoader加载。而热部署正是利用了这个特点,它监听到如果有Class文件改动了,就会创建一个新的 Claass oader进行加载该文件,经过一系列的过程,最终将结果呈现在我们眼前,Spring Boot通过配置DevTools工具来达到热部署效果。
在原理上是使用了两个ClassLoader,一个ClassLoader加载那些不会改变的类(第三方Jar包),另一个ClassLoader加载会更改的类,称为restart ClassLoader,这样在有代码更改的时候,原来的restart ClassLoader被丢弃,重新创建一个restart ClassLoader,由于需要加载的类相比较少,所以实现了较快的重启时间。

热部署环境配置与测试

配置DevTools环境

devtools可以实现页面热部署(即页面修改后会立即生效,这个可以直接在application.properties文件中spring.thymeleaf.cache=false来实现),实现类文件热部署(类文件修改后不会立即生效),实现对属性文件的热部署。即devtools会监听classpath下的文件变动,并且会立即重启应用(发生在保存时机),注意:因为其采用的虚拟机机制,该项重启是很快的。配置了后在修改java 文件后也就支持了热启动,不过这种方式是属于项目重启(速度比较快的项目重启),会清空session中的值,也就是如果有用户登陆的话,项目重启后需要重新陆。

默认情况下,/META-INF/maven, /META-INF/resources, /resources, /static, /templates, /public 这些文件夹下的文件修改不会使应用重启,但是会重新加载( devtools内嵌了一一个LiveReload server,当资源发生改变时,浏览器刷新)

<!-- 热部署依赖 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-devtools</artifactId>
    <scope>runtime</scope>
    <!-- 当这个项目被继承后,这个不向下传递 -->
    <optional>true</optional>
</dependency>
<!-- 热部署生效 -->
<plugin>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-maven-plugin</artifactId>
    <configuration>
        <fork>true</fork> <!-- 如果没有配置,默认热部署不生效 -->
    </configuration>
</plugin>
全局配置文件配置

在application.yml中配置spring.devtools.restart.enabled=false,此时restart 类加载器还会初始化,但不会监视文件更新。

spring:
  # 热部署配置
  devtools:
    restart:
      enabled: true
      # 设置重启的目录,添加目录的文件需要restart
      additional-paths: src/main/java
      # 解决项目自动重新编译后接口报404的问题
      poll-interval: 3000
      quiet-period: 1000
IDEA修改配置

当我们修改了Java类后,IDEA默认是不自动编译的,而spring boot devtools又是监测classpath下的文件发生变化才会重启应用,所以需要设置IDEA的自动编译。

  • 静态自动编译
    • File -> Settings -> Compiler -> Build Project automatically(注意要在other settings配置,不然会只作用于当前项目)
  • 动态自动编译
    • Registry属性修改,ctrl+ shift + alt+/,选择Registry,勾上Compiler autoMake allow when app running
  • 修改配置
    • 在这里插入图片描述
配置取消自动保存

参考

因为IDEA如果设置自动编译又自动保存,那在启动时修改项目就会一直编译,所以关了自动保存比较好

示例
@GetMapping("name/{userName}")
public User findUserByUserName(@PathVariable String userName) {
    System.out.println("热部署");
    System.out.println("不会自动部署, 因为去掉了自动保存");
    // 通过修改打印信息看是否能实现热部署
    // System.out.println("没有热部署");
    // System.out.println("更新classes");
    return userService.findUserByUserName(userName);
}

修改后按ctrl+s保存就会热部署,如果没生效就点下锤子

单元测试

建议Service层测试写单元测试,在对应的类按ctrl+shift+T就可以生成一个测试类

至于Controller层可以使用PostMan或浏览器或Swagger2等方法测试

Spring Boot 2.2.0 版本开始引入 JUnit5 作为单元测试默认库,当前我使用的版本是2.5.1,所以相较于Junit4写法会有所变动

添加依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope>
</dependency>

示例

Service层
@SpringBootTest
class UserServiceTest {

    @Autowired
    private UserService userService;

    @BeforeEach
    void setUp() {
    }

    @AfterEach
    void tearDown() {
    }

    @Test
    void insert() throws Exception {
        userService.insert(new User("junit5+springboot2.2以上", "123abc"));
    }
}
Controller层
@SpringBootTest(classes = {SpringbootTemplateApplication.class})//classes可以省略
@AutoConfigureMockMvc
class UserControllerTest {
    @Autowired
    private MockMvc mockMvc;

    private static final Logger logger = LoggerFactory.getLogger(UserControllerTest.class);
    
    @Test
    void findUserByUserName() throws Exception {
        MvcResult mvcResult = mockMvc.perform(MockMvcRequestBuilders.get("/user/name/admin")).
                andExpect(MockMvcResultMatchers.status().isOk()).
                andReturn();
        MockHttpServletResponse response = mvcResult.getResponse();
        logger.info("响应状态: {} ", response.getStatus());
        logger.info("响应内容: {} ", response.getContentAsString());
    }
    
    @Test
    void findByPage() throws Exception {
        // 构建请求
        MockHttpServletRequestBuilder request = MockMvcRequestBuilders.get("/user/page")
                .contentType("text/html") // 指定请求的contentType头信息
                .accept(MediaType.APPLICATION_JSON); // 指定请求的Accept头信息
        // 发送请求,获取请求结果
        ResultActions perform = mockMvc.perform(request);
        // 请求结果验证
        perform.andExpect(MockMvcResultMatchers.status().isOk());
        // 表示执行完成后返回相应的结果
        MvcResult mvcResult = perform.andReturn();
        // 得到执行后的响应
        MockHttpServletResponse response = mvcResult.getResponse();

        logger.info("响应状态: {} ", response.getStatus());
        logger.info("响应内容: {} ", response.getContentAsString());
    }
}

分布式缓存Ecache整合

EhCache是一个比较成熟的Java缓存框架,最早从hibernate发展而来,是进程中的缓存系统, 它提供了用内存,磁盘文件存储,以及分布式存储方式等多种灵活的cache管理方案,快速简单。
Spring Boot对Ehcache的使用提供支持,所以在Spring Boot中只需简单配置即可使用Ehcache实现数据缓存处理。

SpringCache相关注解

SpringBoot缓存实现内部使用SpringCache实现缓存控制,这里集成Ehcache实际上是对SpringCache抽象的其中一种实现,这里在使用Ehcache实现缓存控制时相关注解说明如下:

@CacheConfig

用于标注在类上,可以存放该类中所有缓存的公有属性,比如设置缓存的名字。

@CacheConfig(cacheNames = "users")
public interface UserService {  }

配置了该注解的数据访问对象中返回的内容将存储在名为users的缓存对象中,我们也可以不适应该注解,直接通过@Cacheable自己配置缓存集的名字来定义。

@Cacheable

应用到读取数据的方法上,即可缓存的方法,如查找方法,先从缓存中读取,如果没有再调用响应的方法获取数据,然后把数据添加到缓存中。
该注解主要有下面几个参数:

  • value, cacheNames: 两个等同的参数( cacheNames为Spring 4新增,作为value的别名),用于指定缓存存储的集合名。由于Spring 4中新增了@CacheConfig,因此在Spring 3中原本必须有的value属性,也成为非必需项了。
  • key: 缓存对象存储在Map集合中的key值,非必需, 缺省按照函数的所有参数组合作为key值,若自己配置需使用SpEL表达式,比如: @Cacheable(key= “#p0”):使用函数第一个参数作为缓存的key值,更多关于SpEL表达式的详细内容可参考官方文档。
  • condition: 缓存对象的条件,非必需,也需使用SpEL表达式, 只有满足表达式条件的内容才会被缓存,比如: @Cacheable(key = “#p0”, condition = “#p0.length() ❤️”),表示只有当第一个参数的长度小于3的时候才 会被缓存。
  • unless: 另外一个缓存条件参数,非必需,需使用SpEL表达式。它不同于condition参数的地方在于它的判断时机,该条件是在函数被调用之后才做判断的,所以它可以通过对result进行判断。
  • keyGenerator: 用于指定key生成器,非必需。若需要指定一 个自定义的key生成器,我们需要去实现
    org.springframework.cache.interceptor.KeyGenerator接口,并使用该参数来指定。需要注意的是:该参数与key是互斥的。
  • cacheManager: 用于指定使用哪个缓存管理器,非必需。只有当有多个时才需要使用
  • cacheResolver: 用于指定使用那个缓存解析器,非必需。需通过org.springframework.cache.interceptor.CacheResolver接口来实现自己的缓存解析器,并用该参数指定。
@Cacheable(cacheNames="user", key="#id")
User selectUserById(final Integer id);
@CachePut

应用到写数据的方法上,如新增、修改方法,调用方法时会自动把相应的数据放入缓存,@CachePut的参数与@Cacheable类似

@CachePut(cacheNames="user", key="#user.id")
public User save(User user) {
    users.add(user);
    return user;
}
@CacheEvict

应用到移除数据的方法上,如删除方法,调用方法时会从缓存中移除相应的数据

@CacheEvict(value = "user", key = "#id")
void delete(final Integer id) ;

除了同@Cacheable一样的参数之外,@CacheEvict 还有下面两个参数:

  • allEntries: 非必需,默认为false。当为true时,会移除所有数据
  • beforeInvocation: 非必需,默认为false, 会在调用方法之后移除数据。当为true时,会在调用方法之前移除数据。
@Caching

组合多个Cache注解使用

// 将id --> user, username --> user, age --> user进行缓存
@Caching(
	put = {
        @CachePut(value = "user", key = "#user.id"),
        @CachePut(value = "user", key = "#user.username"),
        @CachePut(value = "user", key = "#user.age"),
    }
)

环境搭建

pom.xml依赖添加
<!-- Spring-boot 缓存支持 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-cache</artifactId>
</dependency>

<!-- 缓存实现 Ehcache -->
<dependency>
    <groupId>net.sf.ehcache</groupId>
    <artifactId>ehcache</artifactId>
</dependency>
配置文件添加

resources目录下新增ehcache.xml文件

<ehcache name= "mycache">
        <!--
        如果不使用磁盘存储,只需要将diskStore注释掉即可;
        如果使用,需要在ehcache . xml文件中的ehcahce元素下的定义一个diskStore元素并指定其path属性。
        -->
<diskStore path="D:\hxj\dev\workspace\springboot\springboot-template"/>
        <!--
        name :缓存名称。
        maxElementsInMemory :缓存最大数目
        maxElementsOnDisk :硬盘最大缓存个数。
        eternal:对象是否永久有效,一但设置了 ,timeout将不起作用。
        overflowToDisk :是否保存到磁盘,当系统宕机时
        timeToIdleSeconds :设置对象在失效前的允许闲置时间(单位:秒)。
            仅当eternal=false对象不是永久有效时使用,可选属性,默认值是0,表示可闲置时间无穷大。
        timeToLiveSeconds :设置对象在失效前允许存活时间(单位:秒)。
        最大时间介于创建时间和失效时间之间。
        仅当eternal=false对象不是永久有效时使用,默认是0,也就是对象存活时间无穷大。
        diskPersistent:是否缓存虚拟机重启期数据
            Whether the disk store persists between restarts of the Virtual Machine .
        The default value is false.
        diskSpoolBufferSizeMB:这个参数设置DiskStore (磁盘缓存)的缓存区大小。
            默认是30MB。每个Cache都应该有自己的一个缓冲区。
        diskExpiryThreadIntervalSeconds:磁盘失效线程运行时间间隔,默认是120秒。
        memoryStoreEvictionPolicy:当达到maxElementsInMemory限制时,会根据指定的策略去清理内存
            默认策略是LRU (最近最少使用)。你可以设置为FIFO (先进先出)或是LFU (较少使用)。
        clearOnFlush:内存数量最大时是否清除。
        memoryStoreEvictionPolicy :
            可选策略有:
                LRU (最近最少使用,默认策略)
                Less Frequently Used, 就是例子中使用的策略,就是一直以来最少被使用的。
                FIFO (先进先出)
                first in first out, 这个是大家最熟的,先进先出。
                LFU (最少访问次数)
                Least Recently Used,最近最少使用的。
                缓存的元素有一个时间戳,当缓存容量满了,而又需要腾出地方来缓存新的元素的时候,
                那么现有缓存元素中时间戳离当前时间最远的元素将被清出缓存。
        -->
    -- >
    <defaultCache
            maxElementsInMemory= "10000"
            eternal="false"
            timeToIdleSeconds= "120"
            timeToLiveSeconds= "120"
            maxElementsOnDisk="10000000"
            diskExpiryThreadIntervalSeconds="120"
            memoryStoreEvictionPolicy="LRU" />

    <cache name= "users"
            eternal="false"
            maxElementsInMemory="100"
            overflowToDisk="false"
            diskPersistent="false"
            timeToIdleSeconds="0"
            timeToLiveSeconds="300"
            memoryStoreEvictionPolicy="LRU"/>
</ehcache>
修改全局配置文件
spring:
# ehcache配置文件
  cache:
    ehcache:
      config: ehcache.xml
启动缓存
@EnableCaching
public class SpringbootTemplateApplication {
}
JavaBean对象实现序列化

这样才能进行磁盘存储

public class User implements Serializable {
}

示例

用户查询
@Service
public class UserService {
    // 注意不要导错包,是springframework的包
	@Cacheable(cacheNames = "users", key = "#userId")
    public User findUserByUserId(Integer userId) {
        return userMapper.selectByUserId(userId);
    }
}
用户分页查询
@Cacheable(cacheNames = "users", 
           key = "#userQuery.userName + '-' + #userQuery.curPage+ '-' + #userQuery.pageSize")
public PageInfo<User> findByPage(UserQuery userQuery) {
    // 分页参数
    PageHelper.startPage(userQuery.getCurPage(), userQuery.getPageSize());
    // 分页对象
    return new PageInfo<User>(userMapper.selectByPage(userQuery));
}
用户更新 & 删除缓存
@CachePut(cacheNames = "users", key = "#user.userId") // 必须有返回值存入缓存
public User update(User user) {
    // 更新时必须传入用户id,假设该id是当前登录用户的id
    AssertUtil.isTrue(StringUtils.isBlank(user.getUserName()), "用户名不能为空");
    AssertUtil.isTrue(StringUtils.isBlank(user.getUserPwd()), "密码不能为空");
    AssertUtil.isTrue(user.getUserId() == null, "账号未登录");
    User temp = userMapper.selectByUserName(user.getUserName());
    AssertUtil.isTrue(temp != null && !temp.getUserId().equals(user.getUserId()), "用户名已存在");
    AssertUtil.isTrue(userMapper.update(user) < 1, "更新失败");
    return user;
}

@CacheEvict(cacheNames = "users", key = "#userId")
public void delete(Integer userId) {
    AssertUtil.isTrue(userId == null || userMapper.selectByUserId(userId) == null, "用户不存在");
    AssertUtil.isTrue(userMapper.delete(userId) < 1, "删除失败");
}

定时调度集成-Quartz

在日常项目运行中,我们总会有需求在某一时间段周期性的执行某个动作。比如每天在某个时间段导出报表,或者每隔多久统计一次现在在线的用户量等。
在Spring Boot中有Java自带的java.util.Timer类,也有强大的调度器Quartz,还有SpringBoot自带的Scheduled来实现。Scheduled 在Spring3.X引入,默认SpringBoot自带该功能,使用起来也很简单,在启动类级别添加@EnableScheduling注解即可引入定时任务环境。但遗憾的是Scheduled默认不支持分布式环境,这里主要讲解Quartz时钟调度框架与Spring Boot集成。

环境整合

添加jobs 包,定义待执行job任务。实现Job接口,并且在execute 方法中实现自己的业务逻辑。

package cn.hxj.springboottemplate.jobs;

public class MyFirstJob implements Job {
    private static final Logger logger = LoggerFactory.getLogger(MyFirstJob.class);

    @Override
    public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException {
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        logger.info("{} Hello Quartz", sdf.format(new Date()));
    }
}

构建调度配置类

创建JobDetail 实例并定义Trigger 注册到scheduler,启动scheduler 开启调度

package cn.hxj.springboottemplate.config;

@Configuration
public class QuartzConfig {

    // 定义Job实例
    @Bean
    public JobDetail jobDetail() {
        // JobBuilder:定义和创建JobDetail实例的接口
        return JobBuilder.newJob(MyFirstJob.class).storeDurably().build();
    }

    @Bean
    public Trigger trigger1() {
        // 构造特定行为的调度
        SimpleScheduleBuilder scheduleBuilder = SimpleScheduleBuilder.simpleSchedule()
                .withIntervalInSeconds(1) // 每秒执行一次
                .repeatForever();// 无限重复
        // 生成调度触发器
        return TriggerBuilder.newTrigger()
                .withIdentity("trigger1", "group1")
                .withSchedule(scheduleBuilder)
                .forJob(jobDetail())
                .build();
    }

    @Bean
    public Trigger trigger2() {
        return TriggerBuilder.newTrigger()
                .withIdentity("trigger2", "group1")
                // 5 秒执行一次
                .withSchedule(CronScheduleBuilder.cronSchedule("0/5 * * * * ? *"))
                .forJob(jobDetail())
                .build();
    }

}

全局异常 & 事务控制

Spring Boot事务支持

在使用Jdbc作为数据库访问技术时,Spring Boot框架定义了基于jdbc的PlatformTransactionManager接口的实现DataSourceTransactionManager,并在Spring Boot应用启动时自动进行配置。如果使用jpa的话Spring Boot同样提供了对应实现。

这里Spring Boot集成了mybatis框架,mybatis 底层数据访问层实现基于jdbc来实现,所以在Spring Boot环境下对事务进行控制,事务实现由Spring Boot实现并自动配置,在使用时通过注解方式标注相关方法加入事务控制即可。

@Transactional(propagation = Propagation.REQUIRED)
public void insert(User user) {
    AssertUtil.isTrue(StringUtils.isBlank(user.getUserName()), "用户名不能为空");
    AssertUtil.isTrue(StringUtils.isBlank(user.getUserPwd()), "密码不能为空");
    AssertUtil.isTrue(userMapper.selectByUserName(user.getUserName()) != null, "用户名已存在");
    AssertUtil.isTrue(userMapper.insert(user) < 1, "用户添加失败");
}

@Transactional(propagation = Propagation.REQUIRED)
@CachePut(cacheNames = "users", key = "#user.userId") // 必须有返回值存入缓存
public User update(User user) {
    // 更新时必须传入用户id,假设该id是当前登录用户的id
    AssertUtil.isTrue(StringUtils.isBlank(user.getUserName()), "用户名不能为空");
    AssertUtil.isTrue(StringUtils.isBlank(user.getUserPwd()), "密码不能为空");
    AssertUtil.isTrue(user.getUserId() == null, "账号未登录");
    User temp = userMapper.selectByUserName(user.getUserName());
    AssertUtil.isTrue(temp != null && !temp.getUserId().equals(user.getUserId()), "用户名已存在");
    AssertUtil.isTrue(userMapper.update(user) < 1, "更新失败");
    return user;
}

@Transactional(propagation = Propagation.REQUIRED)
@CacheEvict(cacheNames = "users", key = "#userId")
public void delete(Integer userId) {
    AssertUtil.isTrue(userId == null || userMapper.selectByUserId(userId) == null, "用户不存在");
    AssertUtil.isTrue(userMapper.delete(userId) < 1, "删除失败");
}

这里仅仅作为示例,如果方法内只有一个SQL语句执行,加事务控制是没有意义的

Spring Boot全局异常处理

SpringMvc中对异常统一处理提供了相应处理方式,推荐大家使用的是实现接口HandlerExceptionResolver的方式,对代码侵入性较小。
在Spring Boot应用中同样提供了对异常的全局性处理,相关注解如下:

  • @ControllerAdvice
    该注解组合了@Component注解功能,最常用的就是作为全局异常处理的切面类,同时通过该注解可以指定包扫描的范围。@ControllerAdvice 约定了几种可行的返回值,如果是直接返回model类的话,需要使用@ResponseBody进行json转换。
  • @ExceptionHandler
    该注解在Spring 3.X版本引入,在处理异常时标注在方法级别,代表当前方法处理的异常类型有哪些具体应用以Restful接口为例,测试保存用户接口。

全局异常应用

异常抛出与全局异常捕获

  • UserController 查询接口

    @ApiOperation(value = "根据用户名查找用户记录", notes = "用户名不能为空")
        @ApiImplicitParam(name = "userName", value = "用户名", required = true,
                dataType = "String", // 参数类型,默认String
                paramType = "path") // 表示参数从路径上获取
        @GetMapping("name/{userName}")
        public User findUserByUserName(@PathVariable String userName) {
            return userService.findUserByUserName(userName);
        }
    
  • UserService抛出自定义参数异常

    public User findUserByUserName(String userName) {
        AssertUtil.isTrue(StringUtils.isBlank(userName), "用户名不能为空");
        return userMapper.selectByUserName(userName);
    }
    
  • 全局异常处理类

    package cn.hxj.springboottemplate.exceptions.hander;
    
    // 全局异常处理类
    @ControllerAdvice
    public class GlobalExceptionHandler {
    
        @ExceptionHandler(value = Exception.class)
        @ResponseBody
        public ResultInfo exceptionResolver(Exception e) {
            ResultInfo resultInfo = new ResultInfo();
            resultInfo.setCode(300);
            resultInfo.setMsg("系统异常");
            /* 处理特定的异常信息 */
            /*
            if(e instanceof ParamsException) {
                ParamsException exception = (ParamsException) e;
                resultInfo.setCode(exception.getCode());
                resultInfo.setMsg(exception.getMsg());
            }
            */
            return resultInfo;
        }
    
        @ExceptionHandler(value = ParamsException.class)
        public ResultInfo paramsExceptionResolver(ParamsException e) {
            ResultInfo resultInfo = new ResultInfo();
            resultInfo.setCode(e.getCode());
            resultInfo.setMsg(e.getMsg());
            return resultInfo;
        }
    }
    

数据校验-Validation

日常项目开发中,对于前端提交的表单,后台接口接收到表单数据后,为了程序的严谨性,通常后端会加入业务参数的合法校验操作来避免程序的非技术性bug,这里对于客户端提交的数据校验,SpringBoot 通过spring-boot-starter-validation模块包含了数据校验的工作。
这里主要介绍Spring Boot中对请求数据进行校验,相关概念如下

  • JSR303 : JSR303是一项标准,只提供规范不提供实现,规定一些校验规范即校验注解,如@Null,@NotNull, @Pattern, 位于javax.validation.constraints包下。JSR-349 是其升级版本,添加了一-些新特性。
  • Hibernate Validation : Hibernate Validation是对这个规范的实现,并增加了一些其他校验注解,如@Email,@Length, @Range 等等。
  • Spring Validation : Spring Validation对Hibernate Validation进行了二次封装,在Spring MVC模块中添加了自动校验,并将校验信息封装进了特定的类中。

环境配置

springboot-2.3开始,校验包被独立成了一个starter组件,所以需要引入validation和web,而springboot-2.3之前的版本只需要引入 web 依赖就可以了

<!-- 检验 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-validation</artifactId>
</dependency>

校验相关注解

注解功能
@AssertFalse
@AssertTrue
@DecimalMax
@DecimalMin
@Digits
@Future
@Past
@Max
@Min
@NotNull
@Min
@Pattern
@Size
@Email
@Length
@NotBlank
@NotEmpty
@Range
@URL
可以为null,如果不为null的话必须为false
可以为nll,如果不为null的话必须为true
设置不能超过最大值
设置不能超过最小值I
设置必须是数字且数字整数的位数和小数的位数必须在指定范围内
日期必须在当前日期的未来
日期必须在当前日期的过去
最大不得超过此最大值
最大不得小于此最小值
不能为null,可以是空
最大不得小于此最小值
必须满足指定的正则表达式
集合、数组、map等的size()值必须在指定范围内
必须是email格式
长度必须在指定范围内
字符串不能为null,字符串trim()后也不能等于“”
不能为null,集合、数组、map等size()不能为0; 字符串trim()后可以等于”
值必须在指定范围内
必须是一个URL

示例

实体类参数校验注解
@ApiModel(description = "用户实体类")
public class User implements Serializable {

    @ApiModelProperty(name = "userId", value = "用户ID", notes = "唯一")
    @NotBlank(message = "用户id不能为空")
    Integer userId;

    @ApiModelProperty(name = "userName", value = "用户名", notes = "唯一")
    @NotBlank(message = "用户名不能为空")
    @Size(min = 8, max = 32, message = "用户名最少8位,最多32位")
    String userName;

    @Pattern(regexp = "^(?![0-9]+$)(?![a-zA-Z]+$)[0-9A-Za-z\\W]{6,18}$",
             message = "用户密码最少8位,最多32位,必须包含数据和字母,可以包含特殊字符")
    String userPwd;
}
注解要校验字段
@ApiOperation(value = "添加用户", notes = "用户不能为空")
@ApiImplicitParam(name = "user", value = "用户实体类",
                  dataType = "User", // 参数类型,默认String
                  paramType = "form") // 表示参数从表单上获取
@PostMapping
public ResultInfo insert(@Valid User user) {
    ResultInfo resultInfo = new ResultInfo();
    try {
        // userService.insert(user);
    } catch (ParamsException e) {
        e.printStackTrace();
        resultInfo.setCode(e.getCode());
        resultInfo.setMsg(e.getMsg());
    } catch (Exception e) {
        e.printStackTrace();
        resultInfo.setCode(500);
        resultInfo.setMsg("用户添加失败!");
    }
    return resultInfo;
}
绑定异常的处理
// 全局异常处理类
@ControllerAdvice
public class GlobalExceptionHandler {
    @ExceptionHandler(value = BindException.class)
    @ResponseBody // 注意要响应json格式数据
    public ResultInfo bindExceptionResolver(BindException e) {
        ResultInfo resultInfo = new ResultInfo();
        resultInfo.setCode(500);
        resultInfo.setMsg("参数校验失败");
        resultInfo.setResult(e.getBindingResult().getFieldError().getDefaultMessage());
        return resultInfo;
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值