# 基本介绍

  • SpringBoot是由Pivotal团队在2013年开始研发,2014年4月发布第一个版本的全新开源的轻量级框架
  • 不仅继承了Spring框架原有的优秀特性,还简化了Spring应用的整个搭建和开发过程
  • SpringBoot通过集成大量的框架使得依赖包的版本冲突,以及引用的不稳定性等问题得到了很好的解决

# 程序优点

  • 自动配置:简化常用工程相关配置
  • 起步依赖:简化依赖配置
  • 辅助功能:如嵌入式服务器、安全、指标,健康检测

# 文件介绍

.gitignore # 分布式版本控制系统git的配置文件,意思为忽略提交
mvnw  # maven wrapper的缩写,作用是在maven-wrapper.properties中记录你要使用的maven版本
.mvn文件夹  # 存放mvnw相关文件
mvnw.cmd # 执行mvnw命令的cmd入口,mvnw适用于Linux,mvnw.cmd适用于Windows
.iml文件 # idea的工程配置文件
.idea文件夹 # 存放项目的配置信息,包括数据源,类库,字符编码,历史记录,版本控制等信息
pom.xml # 是项目级别的配置文件

注意

# 起步依赖

只要依赖名字中有starter,就一定是起步依赖。
starter是SpringBoot 中常见项目名称,定义了当前项目使用的所有坐标,以达到减少依赖配置的目的。

使用任意坐标时,仅书写GAV中的G和A,V由SpringBoot提供,如发生坐标错误,再指定version(要小心版本冲突)

G:groupid
A:artifactId
V:version

# 依赖管理

parent是所有SpringBoot项目要继承的项目,定义了若干个坐标版本号,达到减少依赖冲突的目的

# 主启动类

创建的每一个 SpringBoot 程序时都包含一个类似于下面的类,我们将这个类称作引导类、主启动类

//方式一
@SpringBootApplication
public class Springboot01QuickstartApplication {
    public static void main(String[] args) {
        SpringApplication.run(Springboot01QuickstartApplication.class, args);
		
		//可以不传args参数,这样就不能接受命令行参数
		SpringApplication.run(Springboot01QuickstartApplication.class);
    }
}

//方式二:
//默认启动类集成SpringBootServletInitiallizer,并重写configure()方法
@SpringBootApplication
public class FileuploadApplication extends SpringBootServletInitializer {
	public static void main(String[] args) {
		SpringApplication.run(FileuploadApplication.class, args);
	}

	@Override
	protected SpringApplicationBuilder configure(SpringApplicationBuilder builder) {
		return builder.sources(FileuploadApplication.class);
	}
}

# 切换web服务器

  • spring-boot-starter-web 依赖实现内置Tomcat服务器
  • jetty是maven私服使用的服务器,比Tomcat更轻量级,扩展性更强,但大型应用一般用的还是Tomcat
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
    <exclusions>
        <exclusion>
            <artifactId>spring-boot-starter-tomcat</artifactId>
            <groupId>org.springframework.boot</groupId>
        </exclusion>
    </exclusions>
</dependency>

<!--引入 jetty 的起步依赖-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-jetty</artifactId>
</dependency>

# 打包启动

在构建 SpringBoot 工程时已经在 pom.xml 中配置了spring-boot-maven-plugin插件

<plugin>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-maven-plugin</artifactId>
</plugin>

Plugin 'org.springframework.boot:spring-boot-maven-plugin' not found 两种解决办法:

<!--在本地仓库中找到对应的版本,再到 pom 引入即可-->
<version>2.7.12</version>

<!--直接用变量的形式指定版本号-->
<version>${project.parent.version}</version>

进入 jar 包所在位置,地址栏输入cmd回车,在 命令提示符 中输入如下命令

java -jar springboot.jar

启动jar包并覆盖设置端口、 切换环境

java –jar springboot.jar –-server.port=88 –-spring.profiles.active=test

标准日志输出到info.log文件,错误日志输入到error.log文件

# windows环境
java -jar app_name.jar >> log\info-%date:~0,4%%date:~5,2%%date:~8,2%.log 2>&1 &
java -jar app_name.jar >> log\info-%date:~0,4%%date:~5,2%%date:~8,2%.log 2>log\error-​​%date:~0,4%%date:~5,2%%date:~8,2%.log &

# Linux环境
java -jar app_name.jar >> /home/log/log-$(date +%Y-%m-%d).log 2>&1 &
java -jar app_name.jar >> /home/log/info-$(date +%Y-%m-%d).log 2>/home/log/error-$(date +%Y-%m-%d).log &

注意

非springboot的maven是不能用java -jar命令的

# 配置文件

# 三种配置文件

  • application.properties
#虚拟目录,如果不设定,默认是 /
server.servlet.context-path : /demo 
  • application.yml(常用)
server:
  #虚拟目录,如果不设定,默认是 /
  servlet:
    context-path: /demo
  • application.yaml 配置内容和 yml 的配置文件中的内容相同
server:
	port: 82

注意

  • SpringBoot 程序的配置文件名必须是 application,只是后缀名不同而已
  • 优先级:application.properties > application.yml > application.yaml

# yaml配置文件

  • 大小写敏感
  • 数组数据在数据书写位置的下方使用减号作为数据开始符号
enterprise:
  name: itcast
  age: 16
  tel: 4006184000
  subject:
    - Java
    - 前端
    - 大数据
	

subject2: [Java,前端,大数据]

user:
- 
 name: zhangsan
 age: 18
- 
 name: lisi
 age: 20
 
user2: [{name: zhangsan,age: 18},{name: lisi,age: 20}]


msg1: 'hello \n world' #单引号会直接输出转义字符
msg2: "hello \n world" #双引号会识别转义字符
  • 单文件中可以通过---实现多文件的效果(只有properties文件支持)
#设置启用的环境
spring:
  profiles:
    active: dev  #表示使用的是开发环境的配置
 
---
#开发
spring:
  profiles: dev
server:
  port: 80
---
#生产
spring:
  profiles: pro
server:
  port: 81
---
#测试
spring:
  profiles: test
server:
  port: 82
---
  • 变量引用
name: lisi

person:
  name: ${name} #引用上面定义的name

# 常用配置

官方配置文档 (opens new window)

properties下新建logback.xml,清理启动日志

<?xml version="1.0" encoding="UTF-8"?>
<configuration>

</configuration>
#服务器配置
server:
  port: 80
  servlet:
    context-path: /
	
#spring配置	
spring:
  datasource: (需要导入druid)
    type: com.alibaba.druid.pool.DruidDataSource
    url: jdbc:mysql://localhost/ssm_db?serverTimezone=UTC
    username: root
    password: 填密码
    driver-class-name: com.mysql.cj.jdbc.Driver
  #或者	(需要导入druid-spring-boot-starter)
  datasource:
    druid: 
	  url: jdbc:mysql://localhost/ssm_db?serverTimezone=UTC
	  username: root
	  password: 填密码
	  driver-class-name: com.mysql.cj.jdbc.Driver
	
  main:
    #关闭banner
    banner-mode: off
#日志
log:
  level:
    root: error
	#设置哪个包的日志级别
	com.sylone: info

#mybatis-plus
mybatis-plus:
  global-config:
    db-config:
	  id-type: assign_id #自增类型
      table-prefix: tbl_ #数据库前缀名
	  logic-delete-field:deleted #逻辑删除字段
	  logic-not-delete-value:0 #没删时逻辑删除字段值0
	  logic-delete-value:1 #删时逻辑删除字段值1
    #关闭banner
    banner: false
#打开日志
  configuration:
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl

注意

配置文件是数字时不要以0和0X开头,否则会被当成八进制或者十六进制,可以添加双引号解决

# 配置文件数据读取

有三种读取方式

lesson: SpringBoot
 
server:
  port: 80
 
enterprise:
  name: itcast
  age: 16
  tel: 4006184000
  subject:
    - Java
    - 前端
    - 大数据

  • 使用 @Value注解读取(适合少量数据)
@RestController
@RequestMapping("/books")
public class BookController {
    
    @Value("${lesson}")
    private String lesson;
    @Value("${server.port}")
    private Integer port;
    @Value("${enterprise.subject[0]}")
    private String subject_00;
 
    @GetMapping("/{id}")
    public String getById(@PathVariable Integer id){
        System.out.println(lesson);
        System.out.println(port);
        System.out.println(subject_00);
        return "hello , spring boot!";
    }
}
  • 自动注入Environment对象读取(少用,了解)
@RestController
@RequestMapping("/books")
public class BookController {
    
    @Autowired
    private Environment env;
    
    @GetMapping("/{id}")
    public String getById(@PathVariable Integer id){
        System.out.println(env.getProperty("lesson"));
        System.out.println(env.getProperty("enterprise.name"));
        System.out.println(env.getProperty("enterprise.subject[0]"));
        return "hello , spring boot!";
    }
}
  • 自定义实体类bean读取(最常用)
@Data
@Component
//ConfigurationProperties译为配置属性,prefix前缀必须设为"enterprise",不加冒号,不能少字母
@ConfigurationProperties(prefix = "enterprise")
public class Enterprise {
    private String name;
    private int age;
    private String tel;
    private String[] subject;
}

//BookController
@RestController
@RequestMapping("/books")
public class BookController {
    
    @Autowired
    private Enterprise enterprise;
 
    @GetMapping("/{id}")
    public String getById(@PathVariable Integer id){
        System.out.println(enterprise.getName());
        System.out.println(enterprise.getAge());
        System.out.println(enterprise.getSubject());
        System.out.println(enterprise.getTel());
        System.out.println(enterprise.getSubject()[0]);
        return "hello , spring boot!";
    }
}

为了加强管理第三方配置类,这种方式还可以优化:

 






 
 









 












//在引导类上开启@EnableConfigurationProperties注解,可以将对应的类加入Spring容器
@SpringBootApplication
@EnableConfigurationProperties(ServerConfig.class)
public class Springboot13ConfigurationApplication {
	
}

//在对应的类上直接使用@ConfigurationProperties进行属性绑定,不用添加@Component
//否则必须添加@Component注解,将类添加到容器管理的bean中
@Data
@ConfigurationProperties(prefix = "servers")
//这里不用加@component了
public class ServerConfig {
    private String ipAddress;
    private int port;
    private long timeout;
}

//EnableConfigurationProperties还可以起到关联的作用,在使用时加载bean,不用时不加载
@EnableConfigurationProperties(ServerConfig.class)//关联注解 强制设置哪一个类成为bean
@Data
public class CartoonCatAndMouse {
  
    private ServerConfig serverConfig;

    public CartoonCatAndMouse(ServerConfig serverConfig){
        this.serverConfig = serverConfig;
    }
}

注意

EnableConfigurationProperties和component不能同时使用,它们获取的是2个不同的bean
使用${ServerConfig.port}获取值时必须使用@Component("ServerConfig")
再通过扫描或者import导入的方式添加到容器

如果出现警告需要添加

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-configuration-processor</artifactId>
    <optional>true</optional>
</dependency>

# maven和springboot多环境开发兼容

Maven和springboot都有profile环境配置,在实际开发中,应该maven为主,springboot为辅。springboot读取maven多环境配置

<build>
	<plugins>
		<plugin>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-maven-plugin</artifactId>
		</plugin>
		<!--添加插件maven-resources-plugin,让pom.xml里的属性值可以在其他地方使用-->
		<plugin>
			<groupId>org.apache.maven.plugins</groupId>
			<artifactId>maven-resources-plugin</artifactId>
			<version>3.2.0</version>
			<!--一定要配置confguration的2个子节点:encoding和useDefaultDelimiters-->
			<configuration>
				<encoding>utf-8</encoding>
				<useDefaultDelimiters>true</useDefaultDelimiters>
			</configuration>
		</plugin>
	</plugins>
</build>

<!--配置了3个环境-->
<profiles>
	<profile>
		<id>dev</id>
		<properties>
			<pom.profile>dev</pom.profile>
		</properties>
		<activation>
			<activeByDefault>true</activeByDefault>
		</activation>
	</profile>
	<profile>
		<id>test</id>
		<properties>
			<pom.profile>test</pom.profile>
		</properties>
	</profile>

	<profile>
		<id>prod</id>
		<properties>
			<pom.profile>prod</pom.profile>
		</properties>
	</profile>
</profiles>

springboot的application.yml的配置

#设置启用的环境
spring:
  profiles:
    active: ${pom.profile}
	
	#或者,这样就不需要插件maven-resources-plugin
	active: @profile.active@

注意

基于springBoot读取Maven配置属性的前提下,如果更新pom.xml时需要手动compile方可生效

# 配置文件位置

SpringBoot 中4级配置文件放置位置:

1级:resource:application.yml
2级:resource:config/application.yml
3级:file :application.yml
4级:file :config/application.yml

级别越高优先级越高,一二级resource是开发用的,三四级file是打包后的包目录。

# 配置热更新

<!--添加spring-boot-devtools依赖-->
<dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-devtools</artifactId>
   <optional>true</optional>
</dependency>
<plugin>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-maven-plugin</artifactId>
   <!-- 如果没有该配置,devtools不会生效 -->
   <configuration>
      <fork>true</fork>
   </configuration>
</plugin>

激活热更新Ctrl+F9,或者配置Idea

setting->complier->Build project automatically

禁用热部署,可以在启动类中设置(级别最高)

@SpringBootApplication
public class Springboot01QuickstartApplication {
    public static void main(String[] args) {
		//禁用热部署
		System.setproperty("spring.devtools.restart.enabled","false")
        SpringApplication.run(Springboot01QuickstartApplication.class, args);
    }
}

# 高级配置

# 第三方Bean属性绑定

  • 使用@ConfigurationProperties为第三方bean绑定属性
@Bean
@ConfigurationProperties(prefix = "datasource")
public DruidDataSource dataSource(){
	DruidDataSource ds = new DruidDataSouce();
	return ds;
	//输出:driverClassName:com.mysql.jdbc.Driver
}
# 配置文件
datasource:
  driverClassName:com.mysql.jdbc.Driver

# 宽松绑定

  • @ConfigurationProperties绑定的属性支持属性名宽松绑定
  • 绑定前缀名命名规范:仅能使用纯小写字母、数字、下划线作为合法的字符
  • 宽松绑定不支持注解@Value引用单个属性的方式
public class ServerConfig{
	private String ipAddress;
	private int port;
	private long timeout;
}
servers:
  # 驼峰模式
  ipAddress: 192.168.10.10
  # 下划线模式
  ip_address: 192.168.10.10
  # 中划线模式
  ip-address: 192.168.10.10
  # 常量模式
  IP-ADDRESS: 192.168.10.10
  # 烤肉串模式
  ip-add-ress: 192.168.10.10
  port: 1234
  timeout: -1

# 常用计量单位

SpringBoot支持JDK8提供的时间与空间计量单位

@Component
@Data
@ConfigurationProperties(prefix ="servers")
public class ServerConfig {
	private String ipAddress;
	private int port;
	private long timeout;
	//时间计量单位
	@DurationUnit(ChronoUnit.MINUTES)
	private Duration serverTimeOut;
	//空间计量单位
	@DataSizeUnit(DataUnit.MEGABYTES)
	private DataSize dataSize;
}

# 开启Bean数据校验

  • 添加JSR303规范坐标与Hibernate校验框架的坐标
<dependency>
	<groupId>javax.validation</groupId>
	<artifactId>validation-api</artifactId>
</dependency>
<dependency>
	<groupId>org.hibernate.validator</groupId>
	<artifactId>hibernate-validator</artifactId>
</dependency>
  • 对Bean开启校验功能,设置校验规则
@Component
@Data
@ConfigurationProperties(prefix = "servers")
//开启校验功能
@Validated
public class ServerConfig{
	@Max(value = 400,message = "最大值不能超过400")
	private int port;
}

# 日志配置

# 添加日志记录操作

@RestController
@RequestMapping("/books")
public class BookController extends BaseController {
	//添加日志
	private static final Logger log = LoggerFactory.getLogger(BookController.class);
	
	@GetMapping
	public String getById()(
		System.out.println("springboot is running...");
		//日志等级
		Log.debug("debug ...");
		Log.info("info ...");
		log.warn("warn ...");
		log.error("error ...");
		return "springboot is running...";
    }
}

# 配置文件

logging:
	#设置分组
	group:
	    #自定义组名,设置当前组中所包含的包
		ebank: com,itheima,controller,com.itheima.service,com.itheima.dao
		iservice: com.alibaba
	level:
		root: info
		#为对包设置日志级别
		com.itheima.controller: debug
		#为对应组设置日志级别
		ebank: warn

# 优化日志对象创建代码

使用lombok提供的注解@Slf4j简化开发,减少日志对象的声明操作

@Slf4j
@RestController
@RequestMapping("/books")
public class BookController extends BaseController {
	
	@GetMapping
	public String getById()(
		System.out.println("springboot is running...");
		//日志等级
		Log.debug("debug ...");
		Log.info("info ...");
		log.warn("warn ...");
		log.error("error ...");
		return "springboot is running...";
    }
}

# 日志输出格式控制

日志输出格式控制

  • PID:进程ID,用于表明当前操作所处的进程,当多服务同时记录日志时,该值可用于协助调试程序
  • 所属类/接口名:显示SprinaBoot重写后的信息,名称过长时,简化包名书写为首字母,其至直接删除

# 自定义输出格式

logging:
	#设置分组
	group:
	    #自定义组名,设置当前组中所包含的包
		ebank: com,itheima,controller,com.itheima.service,com.itheima.dao
		iservice: com.alibaba
	level:
		root: info
		#为对包设置日志级别
		com.itheima.controller: debug
		#为对应组设置日志级别
		ebank: warn
	pattern:
	    console: "%d - $m %n"
		#日期,5位长度带颜色的日志类型,16位长度的线程名,40位长度的类名,消息,换行 
		console "%d %clr(%5p) --- [%16t] %-40.40c %m %n"

# 保存日志到文件

logging:
	#设置分组
	group:
	    #自定义组名,设置当前组中所包含的包
		ebank: com,itheima,controller,com.itheima.service,com.itheima.dao
		iservice: com.alibaba
	level:
		root: info
		#为对包设置日志级别
		com.itheima.controller: debug
		#为对应组设置日志级别
		ebank: warn
	pattern:
	    console: "%d - $m %n"
		#日期,5位长度带颜色的日志类型,16位长度的线程名,40位长度的类名,消息,换行 
		console "%d %clr(%5p) --- [%16t] %-40.40c %m %n"
	file:
	    name: server.log
	logback:
	    rollingpolicy:
		   #server.2023-01-01.0.log
		   file-name-pattern:server.%d{yyyy-MM-dd}.%i.log
		   max-file-size:10MB

# logback 详细配置

<?xml version="1.0" encoding="UTF-8"?>
<configuration scan="true" scanPeriod="60 seconds" debug="false">
    <!-- 日志存放路径 -->
	<property name="log.path" value="logs/ruoyi-file" />
   <!-- 日志输出格式 -->
	<property name="log.pattern" value="%d{HH:mm:ss.SSS} [%thread] %-5level %logger{20} - [%method,%line] - %msg%n" />

    <!-- 控制台输出 -->
	<appender name="console" class="ch.qos.logback.core.ConsoleAppender">
		<encoder>
			<pattern>${log.pattern}</pattern>
		</encoder>
	</appender>

    <!-- 系统日志输出 -->
	<appender name="file_info" class="ch.qos.logback.core.rolling.RollingFileAppender">
	    <file>${log.path}/info.log</file>
        <!-- 循环政策:基于时间创建日志文件 -->
		<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <!-- 日志文件名格式 -->
			<fileNamePattern>${log.path}/info.%d{yyyy-MM-dd}.log</fileNamePattern>
			<!-- 日志最大的历史 60天 -->
			<maxHistory>60</maxHistory>
		</rollingPolicy>
		<encoder>
			<pattern>${log.pattern}</pattern>
		</encoder>
		<filter class="ch.qos.logback.classic.filter.LevelFilter">
            <!-- 过滤的级别 -->
            <level>INFO</level>
            <!-- 匹配时的操作:接收(记录) -->
            <onMatch>ACCEPT</onMatch>
            <!-- 不匹配时的操作:拒绝(不记录) -->
            <onMismatch>DENY</onMismatch>
        </filter>
	</appender>

    <appender name="file_error" class="ch.qos.logback.core.rolling.RollingFileAppender">
	    <file>${log.path}/error.log</file>
        <!-- 循环政策:基于时间创建日志文件 -->
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <!-- 日志文件名格式 -->
            <fileNamePattern>${log.path}/error.%d{yyyy-MM-dd}.log</fileNamePattern>
			<!-- 日志最大的历史 60天 -->
			<maxHistory>60</maxHistory>
        </rollingPolicy>
        <encoder>
            <pattern>${log.pattern}</pattern>
        </encoder>
        <filter class="ch.qos.logback.classic.filter.LevelFilter">
            <!-- 过滤的级别 -->
            <level>ERROR</level>
			<!-- 匹配时的操作:接收(记录) -->
            <onMatch>ACCEPT</onMatch>
			<!-- 不匹配时的操作:拒绝(不记录) -->
            <onMismatch>DENY</onMismatch>
        </filter>
    </appender>

    <!-- 系统模块日志级别控制  -->
	<logger name="com.ruoyi" level="info" />
	<!-- Spring日志级别控制  -->
	<logger name="org.springframework" level="warn" />

	<root level="info">
		<appender-ref ref="console" />
	</root>
	
	<!--系统操作日志-->
    <root level="info">
        <appender-ref ref="file_info" />
        <appender-ref ref="file_error" />
    </root>
</configuration>

file属性与fileNamePattern属性的关系

以2019-06-04为例,如果你设置了File属性,当天你只能看到check.log日志文件,2019-06-05才会看到check.201-06-04.log文件。但是如果你忽略了,你当天就能看到check.2019-06-04.log文件,但你始终看不到check.log文件

# 高级测试

# 加载测试专用属性

比多环境开发中的测试环境影响范围更小,仅对当前测试类有效

  • 在启动测试环境时可以通过properties参数设置测试环境专用的属性
@SpringBootTest(properties = {"test.prop=testValue1"})
public class PropertiesAndArgsTest {
	@Value("${test.prop}")
	private String msg;
	
	@Test
	void testProperties(){
		System.out.printIn(msg);
	}
}
  • 在启动测试环境时可以通过args参数设置测试环境专用的传入参数
@SpringBootTest(args = {"--test.arg=testValue2"})
public class PropertiesAndArgsTest{
	@Value("${test.arg}")
	private String msg;
	
	@Test
	void testArgs(){
		System.outprintIn(msg);
	}
}
  • 使用@Import注解加载当前测试类专用的配置
@SpringBootTest
//加载当前测试类专用的配置
@Import(MsgConfig.class)
public class ConfigurationTest {
	@Autowired
	private String msg;
	
	@Test
	void testConfiguration(){
		System.out.println(msg);
	}
}

# web环境模拟测试

  • 模拟端口
//DEFINED_PORT:使用自定义的端口作为web服务器端口
//RANDOM_PORT:使用随机端口作为web服务器端口
//MOCK:根据当前设置确认是否启动web环境,例如使用了Servlet的API就启动web环境,属于适配性的配置
//NONE:不启动web环境
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class WebTest {
	@Test
	void testRandomPort(){
		
	}
}

通过上述配置,启动测试程序时就可以正常启用web环境了,建议大家测试时使用RANDOM_PORT

  • 虚拟请求测试
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
//开启虚拟MVC调用
@AutoConfigureMockMvc
public class WebTest {

    @Test
	//定义发起虚拟调用的对象MockMVC,通过自动装配的形式初始化对象
    void testWeb(@Autowired MockMvc mvc) throws Exception {
		
        //创建虚拟请求,当前访问/books
        MockHttpServletRequestBuilder builder = MockMvcRequestBuilders.get("/books");
        //执行对应的请求
        mvc.perform(builder);
    }
}
  • 虚拟请求响应状态匹配
@Test
void testStatus(@Autowired MockMvc mvc) throws Exception {
	
    MockHttpServletRequestBuilder builder = MockMvcRequestBuilders.get("/books");
    ResultActions action = mvc.perform(builder);
	
    //设定预期值 与真实值进行比较,成功测试通过,失败测试失败
    //定义本次调用的预期值
    StatusResultMatchers status = MockMvcResultMatchers.status();
	
    //预计本次调用时成功的:状态200
	
    ResultMatcher ok = status.isOk();
    //添加预计值到本次调用过程中进行匹配
    action.andExpect(ok);
}
  • 虚拟请求响应体匹配(非json数据格式)
@Test
void testBody(@Autowired MockMvc mvc) throws Exception {
    MockHttpServletRequestBuilder builder = MockMvcRequestBuilders.get("/books");
    ResultActions action = mvc.perform(builder);
	
    //设定预期值 与真实值进行比较,成功测试通过,失败测试失败
    //定义本次调用的预期值
    ContentResultMatchers content = MockMvcResultMatchers.content();
    ResultMatcher result = content.string("springboot2");
	
    //添加预计值到本次调用过程中进行匹配
    action.andExpect(result);
}
  • 虚拟请求响应体匹配(json数据格式,开发中的主流使用方式)
@Test
void testJson(@Autowired MockMvc mvc) throws Exception {
    MockHttpServletRequestBuilder builder = MockMvcRequestBuilders.get("/books");
    ResultActions action = mvc.perform(builder);
	
    //设定预期值 与真实值进行比较,成功测试通过,失败测试失败
    //定义本次调用的预期值
    ContentResultMatchers content = MockMvcResultMatchers.content();
    ResultMatcher result = content.json("{\"id\":1,\"name\":\"springboot2\"}");
	
    //添加预计值到本次调用过程中进行匹配
    action.andExpect(result);
}
  • 虚拟请求响应头信息匹配
@Test
void testContentType(@Autowired MockMvc mvc) throws Exception {
    MockHttpServletRequestBuilder builder = MockMvcRequestBuilders.get("/books");
    ResultActions action = mvc.perform(builder);
	
    //设定预期值 与真实值进行比较,成功测试通过,失败测试失败
    //定义本次调用的预期值
    HeaderResultMatchers header = MockMvcResultMatchers.header();
    ResultMatcher contentType = header.string("Content-Type", "application/json");
	
    //添加预计值到本次调用过程中进行匹配
    action.andExpect(contentType);
}
  • 以下范例就是三种信息同时进行匹配校验,也是一个完整的信息匹配过程
@Test
void testGetById(@Autowired MockMvc mvc) throws Exception {
    MockHttpServletRequestBuilder builder = MockMvcRequestBuilders.get("/books");
    ResultActions action = mvc.perform(builder);

    StatusResultMatchers status = MockMvcResultMatchers.status();
    ResultMatcher ok = status.isOk();
    action.andExpect(ok);

    HeaderResultMatchers header = MockMvcResultMatchers.header();
    ResultMatcher contentType = header.string("Content-Type", "application/json");
    action.andExpect(contentType);

    ContentResultMatchers content = MockMvcResultMatchers.content();
    ResultMatcher result = content.json("{\"id\":1,\"name\":\"springboot\"}");
    action.andExpect(result);
}

# 数据层测试回滚

  • 在原始测试用例中添加注解@Transactional即可实现当前测试用例的事务不提交
  • 如果想提交事务,可以添加一个@RollBack的注解,设置回滚状态为false即可正常提交事务
@SpringBootTest
@Transactional
@Rollback(true)
public class DaoTest {
	
    @Autowired
    private BookService bookService;

    @Test
    void testSave(){
        Book book = new Book();
        book.setName("springboot3");
        book.setType("springboot3");
        book.setDescription("springboot3");

        bookService.save(book);
    }
}

# 测试数据用例设定

  • springboot提供了在配置中使用随机值的机制,确保每次运行程序加载的数据都是随机的
  • 数据的加载按照之前加载数据的形式,使用@ConfigurationProperties注解即可
@Component
@Data
@ConfigurationProperties(prefix = "testcase.book")
public class BookCase {
    private int id;
    private int id2;
    private int type;
    private String name;
    private String uuid;
    private long publishTime;
}
testcase:
  book:
    id: ${random.int}
    id2: ${random.int(10)}
    type: ${random.int!5,10!}
    name: ${random.value}
    uuid: ${random.uuid}
    publishTime: ${random.long}

对于随机值的产生,还有一些小的限定规则,比如产生的数值性数据可以设置范围等,具体如下:

${random.int}表示随机整数
${random.int(10)}表示10以内的随机数
${random.int(10,20)}表示10到20的随机数
其中()可以是任意字符,例如[],!!均可

# 整合数据层

# 数据源技术

springboot提供了3款内嵌数据源技术

  • HikariCP:默认内置数据源对象
  • Tomcat提供DataSource:HikariCP不可用且在web环境中,将使用tomcat服务器配置的数据源
  • Commons DBCP:Hikari不可用,tomcat数据源也不可用,将使用dbcp数据源
# druid的starter对应的配置如下
spring:
  datasource:
    druid:	
   	  url: jdbc:mysql://localhost:3306/ssm_db?serverTimezone=UTC
      driver-class-name: com.mysql.cj.jdbc.Driver
      username: root
      password: root

# 换成是默认的数据源HikariCP后,直接吧druid删掉就行了,如下
spring:
  datasource:
    url: jdbc:mysql://localhost:3306/ssm_db?serverTimezone=UTC
    driver-class-name: com.mysql.cj.jdbc.Driver
    username: root
    password: root

# 也可以写上是对hikari做的配置,但是url地址要单独配置,如下
spring:
  datasource:
    url: jdbc:mysql://localhost:3306/ssm_db?serverTimezone=UTC
    hikari:
      driver-class-name: com.mysql.cj.jdbc.Driver
      username: root
      password: root

注意

  • 如果导入了druid的starter就必须配置druid数据源
  • springboot2.7.8以后mysql驱动,引入的依赖是mysql-connector-j可以不加版本号
  • 在没有任何配置的情况下,Spring Boot将使用默认的H2内存数据库
  • 如果项目中不需要操作数据库,可以在启动类中添加配置
@SpringBootApplication(exclude={DataSourceAutoConfiguration})

# 持久化技术JdbcTemplate

  • 导入jdbc对应的坐标,记得是starter
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
  • 自动装配JdbcTemplate对象
@SpringBootTest
class Springboot15SqlApplicationTests {
    @Test
    void testJdbcTemplate(@Autowired JdbcTemplate jdbcTemplate){}
}
  • 使用JdbcTemplate实现查询操作(实体类封装数据的查询操作)
@Test
void testJdbcTemplate(@Autowired JdbcTemplate jdbcTemplate){

    String sql = "select * from tbl_book";
    RowMapper<Book> rm = new RowMapper<Book>() {
        @Override
        public Book mapRow(ResultSet rs, int rowNum) throws SQLException {
            Book temp = new Book();
            temp.setId(rs.getInt("id"));
            temp.setName(rs.getString("name"));
            temp.setType(rs.getString("type"));
            temp.setDescription(rs.getString("description"));
            return temp;
        }
    };
    List<Book> list = jdbcTemplate.query(sql, rm);
    System.out.println(list);
}

# 数据库技术

springboot提供了3款内置的数据库:H2、HSQL、Derby

  • 导入H2数据库对应的坐标,一共2个
<dependency>
    <groupId>com.h2database</groupId>
    <artifactId>h2</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
  • 将工程设置为web工程,启动工程时启动H2数据库
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
  • 通过配置开启H2数据库控制台访问程序,也可以使用其他的数据库连接软件操作
spring:
  h2:
    console:
      enabled: true
      path: /h2
  • web端访问路径/h2,访问密码123456,如果访问失败,先配置下列数据源,启动程序运行后再次访问/h2路径就可以正常访问了
datasource:
  url: jdbc:h2:~/test
  hikari:
    driver-class-name: org.h2.Driver
    username: sa
    password: 123456

注意

  • 完全支持jdbc和sql,使用起来和mysql没啥区别
  • url: jdbc:h2:mem:test 内存模式,数据不会持久化
  • url:jdbc:h2:~/test 嵌入模式,数据文件存储在用户目录test开头的文件中
  • url:jdbc:h2:tcp//localhost/〜/test 远程模式,访问远程的h2 数据库
  • 一个重要提醒,别忘了,上线时,把内存级数据库关闭

# 整合Redis

  • 导入springboot整合redis的starter坐标
<!--可以在创建模块的时候通过勾选的形式进行选择,同样归属NoSQL分类中-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
  • 进行基础配置
spring:
  redis:
    host: localhost
    port: 6379
  • 使用springboot整合redis的专用客户端接口操作,此处使用的是RedisTemplate
@SpringBootTest
public class RedisTest {

    @Resource
    private RedisTemplate redisTemplate;

    @Test
    void set() {
        ValueOperations valueOperations = redisTemplate.opsForValue();
        valueOperations.set("age", 44);
    }

    @Test
    void get() {
        ValueOperations valueOperations = redisTemplate.opsForValue();
        Object age = valueOperations.get("age");
        System.out.println(age);
    }

    @Test
    void hset() {
        HashOperations ops = redisTemplate.opsForHash();
        ops.put("info","b","bb");
    }

    @Test
    void hget() {
        HashOperations ops = redisTemplate.opsForHash();
        Object val = ops.get("info", "b");
        System.out.println(val);
    }

}

# Spring Data Redis 和Spring Data Rective Redis区别

Spring Data Redis # 是同步阻塞的,需要等待结果
Spring Data Rective Redis # 是非阻塞的,异步执行的

ReactiveRedisTemplate发送Redis请求后不会阻塞线程,当前线程可以去执行其他任务。 等到Redis响应数据返回后,ReactiveRedisTemplate再调度线程处理响应数据。 响应式编程可以通过优雅的方式实现异步调用以及处理异步结果,正是它的最大的意义。

# RedisTemplate 和 StringRedisTemplate 的区别

当我们使用RedisTemplate往redis中存储java对象的时候,他会顺带着将该java对象的字节码文件也同时存进了内存中,StringRedisTemplate类可以实现key和value的序列化

StringRedisTemplate

@SpringBootTest
public class RedisTemplateTest {

    @Resource
    private StringRedisTemplate stringRedisTemplate;

	@Test
	public void testJavaBean(){
		User user = new User("brrbaii", 22);
		//手动把user对象转为JSON字符串,这里使用Hutool工具类里的JSONUtil
		String UserToStr = JSONUtil.toJsonStr(user);
		//存入数据
		stringRedisTemplate.opsForValue().set("user:1",UserToStr);
		//取出数据,这里取出来的是JSON格式
		String StrUser = stringRedisTemplate.opsForValue().get("user:1");
		//手动把JSON字符串转回user对象
		User userResult = JSONUtil.toBean(StrUser, User.class);
		System.out.println(userResult);
	}

}

# redis客户端选择

springboot整合redis技术提供了多种客户端兼容模式,默认提供的是lettucs客户端技术

lettcus与jedis区别:

  • jedis连接Redis服务器是直连模式,当多线程模式下使用jedis会存在线程安全问题,解决方案可以通过配置连接池使每个连接专用,这样整体性能就大受影响
  • lettcus基于Netty框架进行与Redis服务器连接,底层设计中采用StatefulRedisConnection。 StatefulRedisConnection自身是线程安全的,可以保障并发访问安全问题,所以一个连接可以被多线程复用。当然lettcus也支持多连接实例一起工作

集成jedis客户端技术操作步骤如下:

<!--jedis坐标受springboot管理,无需提供版本号-->
<dependency>
    <groupId>redis.clients</groupId>
    <artifactId>jedis</artifactId>
</dependency>
  • 配置客户端技术类型,设置为jedis,根据需要设置对应的配置
spring:
  redis:
    host: localhost
    port: 6379
    client-type: jedis
	time-out: 2000
    lettuce:
      pool:
        max-active: 16
    jedis:
      pool:
	    # 最大连接数(负数表示没有限制)
        max-active: 200
		# 最大空闲连接数
		max-idle: 50
		# 最小空闲连接数
		min-idle: 10
  • 创建Jedis配置类,生成连接池
import com.baomidou.mybatisplus.core.toolkit.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPoolConfig;

/**
 * @author benjamin_5
 * @Description
 * @date 2022/12/22
 */
@Configuration
public class JedisConfig {

    private static final Logger logger = LoggerFactory.getLogger(JedisConfig.class);

    @Value("${spring.redis.port}")
    private Integer port;
    @Value("${spring.redis.host}")
    private String host;
    @Value("${spring.redis.password}")
    private String password;
    @Value("${spring.redis.jedis.pool.max-idle}")
    private Integer maxIdle;
    @Value("${spring.redis.jedis.pool.max-active}")
    private Integer maxActive;
    @Value("${spring.redis.jedis.pool.min-idle}")
    private Integer minIdle;
    @Value("${spring.redis.timeout}")
    private Integer timeout;

    @Bean
    public JedisPool jedisPool() {
        JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
        // 最大空闲连接数
        jedisPoolConfig.setMaxIdle(maxIdle);
        // 最大连接数
        jedisPoolConfig.setMaxTotal(maxActive);
        // 最小空闲连接数
        jedisPoolConfig.setMinIdle(minIdle);
        // 连接空闲多久后释放,当空闲时间大于该值且空闲连接大于最大空闲连接数时直接释放连接线程
        jedisPoolConfig.setSoftMinEvictableIdleTimeMillis(10000);
        // 连接最小空闲时间
        jedisPoolConfig.setMinEvictableIdleTimeMillis(1800000);
        // 获取连接时的最大等待毫秒数,小于零:阻塞不确定的时间,默认-1
        jedisPoolConfig.setMaxWaitMillis(1500);
        // 在获取连接的时候检查有效性, 默认false
        jedisPoolConfig.setTestOnBorrow(true);
        // 在空闲时检查有效性, 默认false
        jedisPoolConfig.setTestWhileIdle(true);
        // 连接耗尽时是否阻塞, false报异常,ture阻塞直到超时, 默认true
        jedisPoolConfig.setBlockWhenExhausted(false);
        if (StringUtils.isBlank(password)) {
            password = null;
        }
        JedisPool jedisPool = new JedisPool(jedisPoolConfig, host, port, timeout, password);
        logger.info("redis连接成功-{}:{}", host, port);
        return jedisPool;
    }
}
  • 直接使用jedis的接口不是很好用,我们可以创建工具类,将常用操作二次包装
package com.wu.springboot_study.util;

import com.baomidou.mybatisplus.core.toolkit.StringUtils;
import lombok.AllArgsConstructor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.exceptions.JedisConnectionException;

import java.io.*;

/**
 * @author benjamin_5
 * @Description Jedis工具类
 * @date 2022/12/22
 */
@AllArgsConstructor
@Component
public class JedisUtil {
    private static final Logger logger = LoggerFactory.getLogger(JedisUtil.class);

    private final JedisPool jedisPool;

    /**
     * 关闭资源
     * @param jedis
     */
    private void close(Jedis jedis){
        if(jedis != null){
            jedis.close();
        }
    }

    private String getKey(String namespace, String key){
        if(StringUtils.isNotBlank(namespace)){
            key = String.format("%s:%s", namespace, key);
        }
        return key;
    }

    private byte[] getKeyBytes(String namespace, String key){
        return getKey(namespace, key).getBytes();
    }

    /**
     * 设置缓存
     * @param namespace 命名空间
     * @param key 键
     * @param value 值
     * @param expireSecond 过期时间,<=0为不过期,单位s
     * @param <T>
     */
    public <T extends Serializable> void set(String namespace, String key, T value, long expireSecond){
        Jedis jedis = null;
        ByteArrayOutputStream bos = new ByteArrayOutputStream();
        try {
            jedis = jedisPool.getResource();
            ObjectOutputStream oos = new ObjectOutputStream(bos);
            oos.writeObject(value);
            oos.flush();
            if(expireSecond <= 0){
                jedis.set(getKeyBytes(namespace, key), bos.toByteArray());
            }else{
                jedis.setex(getKeyBytes(namespace, key), expireSecond ,bos.toByteArray());
            }
        }catch (JedisConnectionException e){
            logger.error("jedis connection fail: {}" , e.getMessage());
        } catch (IOException e) {
            logger.error("对象序列化失败: {}" , e.getMessage());
        } finally {
            try {
                bos.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
            close(jedis);
        }
    }

    public <T extends Serializable> void set(String namespace, String key, T value){
        set(namespace,key,value,0);
    }

    public <T extends Serializable> void set(String key, T value){
        set(null,key,value,0);
    }

    /**
     * 获取缓存
     * @param namespace 命名空间
     * @param key 键
     * @param <T> 返回值类型
     * @return
     */
    public <T extends Serializable> T get(String namespace,String key){
        Jedis jedis = null;
        ByteArrayInputStream bis = null;
        try {
            jedis = jedisPool.getResource();
            byte[] values = jedis.get(getKeyBytes(namespace, key));
            if (values == null) {
                return null;
            }
            bis = new ByteArrayInputStream(values);
            ObjectInputStream ois = new ObjectInputStream(bis);
            return (T) ois.readObject();
        }catch (ClassNotFoundException e){
            logger.error("对象类型映射失败:{}" , e.getMessage());
        }catch (JedisConnectionException e){
            logger.error("jedis connection fail: {}" , e.getMessage());
        } catch (IOException e) {
            logger.error("对象反序列化失败: {}" , e.getMessage());
        } finally {
            try {
                if(bis != null){
                    bis.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
            close(jedis);
        }
        return null;
    }

    public <T extends Serializable> T get(String key){
        return get(null, key);
    }

    /**
     * 设置过期时间
     * @param namespace 命名空间
     * @param key 键
     * @param expireSecond 过期时间 单位s
     */
    public void expire(String namespace, String key, long expireSecond){
        Jedis jedis = null;
        try {
            jedis = jedisPool.getResource();
            jedis.expire(getKeyBytes(namespace, key), expireSecond);
        }catch (JedisConnectionException e){
            logger.error("jedis connection fail: {}" , e.getMessage());
        } finally {
            close(jedis);
        }
    }

    public void expire(String key, long expireSecond){
        expire(null, key, expireSecond);
    }

    /**
     * key值是否存在
     * @param namespace 命名空间
     * @param key 键
     * @return
     */
    public boolean exists(String namespace, String key){
        Jedis jedis = null;
        try {
            jedis = jedisPool.getResource();
            return jedis.exists(getKeyBytes(namespace, key));
        }catch (JedisConnectionException e){
            logger.error("jedis connection fail: {}" , e.getMessage());
        } finally {
            close(jedis);
        }
        return false;
    }

    public boolean exists(String key){
        return exists(null, key);
    }

    /**
     * 删除缓存
     * @param namespace 命名空间
     * @param key 键
     * @return
     */
    public boolean remove(String namespace, String key){
        Jedis jedis = null;
        try {
            jedis = jedisPool.getResource();
            jedis.del(getKeyBytes(namespace, key));
            return true;
        }catch (JedisConnectionException e){
            logger.error("jedis connection fail: {}" , e.getMessage());
        } finally {
            close(jedis);
        }
        return false;
    }

    public boolean remove(String key){
        return remove(null, key);
    }

    /**
     * 键值加1
     * @param namespace 命名空间
     * @param key 键
     * @return
     */
    public Long incr(String namespace, String key){
        Jedis jedis = null;
        try {
            jedis = jedisPool.getResource();
            return jedis.incr(getKeyBytes(namespace, key));
        }catch (JedisConnectionException e){
            logger.error("jedis connection fail: {}" , e.getMessage());
        } finally {
            close(jedis);
        }
        return null;
    }

    public Long incr(String key){
        return incr(null, key);
    }

    /**
     * 键值减1
     * @param namespace
     * @param key
     * @return
     */
    public Long decr(String namespace, String key){
        Jedis jedis = null;
        try {
            jedis = jedisPool.getResource();
            return jedis.decr(getKeyBytes(namespace, key));
        }catch (JedisConnectionException e){
            logger.error("jedis connection fail: {}" , e.getMessage());
        } finally {
            close(jedis);
        }
        return null;
    }

    public Long decr(String key){
        return decr(null, key);
    }
    
}
  • 在controller中调用测试
@RestController
@RequestMapping("user")
@AllArgsConstructor
public class UserRestController {

    private final JedisUtil jedisUtil;

    @GetMapping("setCache")
    public String setCache(String key, String value){
        jedisUtil.set(key,value);
        return key + ":" + jedisUtil.get(key);
    }
}

# 整合MonggoDB

  • 导入springboot整合MongoDB的starter坐标
<!--可以在创建模块的时候通过勾选的形式进行选择,同样归属NoSQL分类中-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-mongodb</artifactId>
</dependency>
  • 进行基础配置
spring:
  data:
    mongodb:
      uri: mongodb://localhost/test
  • 使用springboot整合MongoDB的专用客户端接口MongoTemplate来进行操作
@SpringBootTest
public class MongodbTest {

    @Resource
    private MongoTemplate mongoTemplate;

    @Test
    void contextLoads() {
        Book book = new Book();
        book.setId(2);
        book.setName("springboot mongodb");
        book.setType("mongodb");
        book.setDescription("test mongodb ---- ");
        mongoTemplate.save(book);
    }

    @Test
    void find() {
        List<Book> all = mongoTemplate.findAll(Book.class);
        System.out.println(all);
    }

}

# 整合junit

  • 在springboot中,@RunWith不用特别写,springboot内部自己加上了
  • @ContextConfiguration加载SpringConfig也不用写了,因为springboot的引导类起到了配置类的作用

SpringBoot 整合 junit 特别简单,分为以下三步完成:

  • 在测试类上添加 SpringBootTest 注解
  • 使用 @Autowired 注入要测试的资源
  • 定义测试方法进行测试
@SpringBootTest
class Springboot07TestApplicationTests {
 
    @Autowired
    private BookService bookService;
 
    @Test
    public void save() {
        bookService.save();
    }
}

注意:这里的引导类所在包必须是测试类所在包及其子包

例如:引导类所在包是 com.itheima,测试类所在包是 com.itheima
如果不满足的话,就需要在使用 @SpringBootTest 时,使用 classes 属性指定引导类的字节码对象
如 @SpringBootTest(classes = Springboot07TestApplication.class)

# 整合mybatis

实际就是三个关键步骤:

  • 导入MyBatis、MySQL依赖,如果需要写controller层则再添加spring-boot-starter-web
  • 在dao接口@Mapper或引导类@MapperScan,类同于ssm中MybatisConfig的mapper扫描包
  • 导入druid依赖、在application.yml中配置数据源dataSource
//在 com.itheima.domain 包下定义实体类 Book
@Data
public class Book {
    private Integer id;
    private String name;
    private String type;
    private String description;
}

//在 com.itheima.dao 包下定义 BookDao 接口
//替代MybatisConfig里的MapperScannerConfigurer方法
//也就是Mybatis核心配置文件的<mappers>标签里的扫描mapper包
@Mapper  //或者可以在引导类扫描@MapperScan
public interface BookDao {
    @Select("select * from tbl_book where id = #{id}")
    public Book getById(Integer id);
}

//定义测试类
@SpringBootTest
class Springboot08MybatisApplicationTests {
 
	@Autowired
	private BookDao bookDao;
 
	@Test
	void testGetById() {
		Book book = bookDao.getById(1);
		System.out.println(book);
	}
}

application.yml

spring:
  datasource:
    #mysql6以后必须driver-class-name中间加cj,url设置时区,否则会报错
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/ssm_db?serverTimezone=UTC
    username: root
    password: root
	#配置使用什么数据源,需要导入 Druid 依赖
	type: com.alibaba.druid.pool.DruidDataSource

com.mysql.jdbc.Driver与com.mysql.cj.jdbc.Driver的区别

  • JDBC连接Mysql5需用com.mysql.jdbc.Driver
  • JDBC连接Mysql6需用com.mysql.cj.jdbc.Driver,同时url需要指定时区serverTimezone
  • 设定时区时,serverTimezone=UTC比中国时间早8个小时,若在中国,可设置serverTimezone=Asia/Shanghai
  • SpringBoot 版本低于2.4.3(不含),Mysql驱动版本大于8.0时,需要在url连接串中配置时区 jdbc:mysql://localhost:3306/ssm_db?serverTimezone=UTC

# PageHelper分页插件

  • 引入pageHelper依赖
<dependency>
    <groupId>com.github.pagehelper</groupId>
    <artifactId>pagehelper-spring-boot-starter</artifactId>
    <version>1.2.12</version>
</dependency>
  • 配置几个配置
# mybatis 相关配置
mybatis:
  #... 其他配置信息
  configuration-properties:
       helperDialect: mysql
       offsetAsPageNum: true
       rowBoundsWithCount: true
       reasonable: true
  mapper-locations: mybatis/mapper/*.xml
  • 使用的时候,只需在查询list前,调用 startPage 设置分页信息,即可使用分页功能
public Object getUsers(int pageNum, int pageSize) {
    PageHelper.startPage(pageNum, pageSize);
    // 不带分页的查询
    List<UserEntity> list = userMapper.selectAllWithPage(null);
    // 1. 可以将结果转换为 Page , 然后获取 count 和其他结果值
    com.github.pagehelper.Page listWithPage = (com.github.pagehelper.Page) list;
    System.out.println("listCnt:" + listWithPage.getTotal());
    // 2. 也可使用 PageInfo 来接收
    PageInfo<UserEntity> pageinfo = new PageInfo(list);
    return list;
}

# 整合SSM

需要修改的内容如下:

  • Springmvc 中 config 包下的是配置类,而 SpringBoot 工程不需要这些配置类,所以这些可以直接删除
  • dao 包下的接口上在拷贝到 springboot 工程中需要在接口中添加 @Mapper 注解(整合MyBatis)
  • BookServiceTest 测试类需要加@SpringBootTest注解,改成 SpringBoot 整合 junit 的
  • 在 SpringBoot 中是没有 webapp 目录的,静态资源需要放在 resources 下的 static 下

注意

controller包下拦截器interceptor失效,因为以前需要SpringMvcConfig实现WebMvcConfigurer 接口;因为没有了ServletConfig,springmvc也就不会拦截资源,也就不用放行静态资源了

# 整合缓存技术

  • 缓存是一种介于数据永久存储介质与数据应用之间的数据临时存储介质
  • 使用缓存可以有效的减少低速数据读取过程的次数(例如磁盘10),提高系统性能
  • 缓存不仅可以用于提高永久性存储介质的数据读取效率,还可以提供临时的数据存储空间

# SpringBoot内置缓存解决方案

  • 导入springboot提供的缓存技术对应的starter
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-cache</artifactId>
</dependency>
  • 启用缓存,在引导类上方标注注解@EnableCaching配置springboot程序中可以使用缓存
@SpringBootApplication
//开启缓存功能
@EnableCaching
public class Springboot19CacheApplication {
    public static void main(String[] args) {
        SpringApplication.run(Springboot19CacheApplication.class, args);
    }
}
  • 存入缓存,读取缓存
public interface SMSCodeService {
    public String sendCodeToSMS(String tele);
    public boolean checkCode(SMSCode smsCode);
}

@Service
public class SMSCodeServiceImpl implements SMSCodeService {
    @Autowired
    private CodeUtils codeUtils;

    //@CachePut 存入缓存,#tele 绑定参数
    @CachePut(value = "smsCode", key = "#tele")
    public String sendCodeToSMS(String tele) {
        String code = codeUtils.generator(tele);
        return code;
    }

    public boolean checkCode(SMSCode smsCode) {
        //取出内存中的验证码与传递过来的验证码比对,如果相同,返回true
        String code = smsCode.getCode();
        String cacheCode = codeUtils.get(smsCode.getTele());
        return code.equals(cacheCode);
    }
}

# @Cacheable

  • @Cacheable 注解在方法上,表示该方法的返回结果会放在缓存中,以便于以后使用相同的参数调用该方法时,会返回缓存中的值,而不会实际执行该方法
@Cacheable(value = "smsCode",key="#tele")
public String get(String tele){
	return null;
}
  • @Cacheable 提供两个参数来指定缓存名:value、cacheNames,二者选其一即可
//findById 方法与一个名为 menu 的缓存关联起来
//调用该方法时,会检查 menu 缓存,如果缓存中有结果,就不会去执行方法
@Override
@Cacheable("menu")
public Menu findById(String id) {
    Menu menu = this.getById(id);
    if (menu != null){
        System.out.println("menu.name = " + menu.getName());
    }
    return menu;
}
  • 关联多个缓存名,只要至少其中一个缓存命中了,那么这个缓存中的值就会被返回
@Override
@Cacheable({"menu", "menuById"})
public Menu findById(String id) {
	Menu menu = this.getById(id);
	if (menu != null){
		System.out.println("menu.name = " + menu.getName());
	}
	return menu;
}

# SpringBoot整合Ehcache缓存

  • 导入Ehcache的坐标
<!--不是导入Ehcache的starter,而是导入具体坐标-->
<dependency>
    <groupId>net.sf.ehcache</groupId>
    <artifactId>ehcache</artifactId>
</dependency>
  • 配置缓存技术实现使用Ehcache
spring:
  cache:
    type: ehcache
    ehcache:
      config: ehcache.xml
  • ehcache的配置文件
<?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="D:\ehcache" />

    <!--默认缓存策略 -->
    <!-- external:是否永久存在,设置为true则不会被清除,此时与timeout冲突,通常设置为false-->
    <!-- diskPersistent:是否启用磁盘持久化-->
    <!-- maxElementsInMemory:最大缓存数量-->
    <!-- overflowToDisk:超过最大缓存数量是否持久化到磁盘-->
    <!-- timeToIdleSeconds:最大不活动间隔,设置过长缓存容易溢出,设置过短无效果,
	                        可用于记录时效性数据,例如验证码-->
    <!-- timeToLiveSeconds:最大存活时间-->
    <!-- memoryStoreEvictionPolicy:缓存清除策略-->
    <defaultCache
        eternal="false"
        diskPersistent="false"
        maxElementsInMemory="1000"
        overflowToDisk="false"
        timeToIdleSeconds="60"
        timeToLiveSeconds="60"
        memoryStoreEvictionPolicy="LRU" />
		
    <!--自定义缓存策略 -->
    <cache
        name="smsCode"
        eternal="false"
        diskPersistent="false"
        maxElementsInMemory="1000"
        overflowToDisk="false"
        timeToIdleSeconds="10"
        timeToLiveSeconds="10"
        memoryStoreEvictionPolicy="LRU" />
</ehcache>
  • 注意前面的案例中,设置了数据保存的位置是smsCode
@CachePut(value = "smsCode", key = "#tele")
public String sendCodeToSMS(String tele) {
    String code = codeUtils.generator(tele);
    return code;
}	
  • 到这里springboot整合Ehcache就做完了,可以发现一点,原始代码没有任何修改,仅仅是加了一组配置就可以变更缓存供应商了

# SpringBoot整合Redis缓存

redis的配置可以在yml文件中直接进行配置,无需制作独立的配置文件

  • 导入redis的坐标
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
  • 配置缓存技术实现使用redis
spring:
  redis:
    host: localhost
    port: 6379
  cache:
    type: redis
  • 如果需要对redis作为缓存进行配置,注意不是对原始的redis进行配置,而是配置redis作为缓存使用相关的配置,隶属于spring.cache.redis节点下
spring:
  redis:
    host: localhost
    port: 6379
  cache:
    type: redis
    redis:
      use-key-prefix: false
      key-prefix: sms_
      cache-null-values: false
      time-to-live: 10s

# SpringBoot整合Memcached缓存

springboot并没有支持使用memcached作为其缓存解决方案,也就是说在type属性中没有memcached的配置选项,因此使用memcached需要通过手工硬编码的方式来使用,于是前面的套路都不适用了,需要自己写了。

memcached目前提供有三种客户端技术:

  • Memcached Client for Java
  • SpyMemcached
  • Xmemcached:性能指标各方面最好

Xmemcached的整合步骤:

  • 导入xmemcached的坐标
<dependency>
    <groupId>com.googlecode.xmemcached</groupId>
    <artifactId>xmemcached</artifactId>
    <version>2.4.7</version>
</dependency>
  • 配置memcached,制作memcached的配置类
@Configuration
public class XMemcachedConfig {
    @Bean
    public MemcachedClient getMemcachedClient() throws IOException {
        MemcachedClientBuilder mcb = new XMemcachedClientBuilder("localhost:11211");
        MemcachedClient memcachedClient = mcb.build();
        return memcachedClient;
    }
}
  • 使用xmemcached客户端操作缓存,注入MemcachedClient对象
@Service
public class SMSCodeServiceImpl implements SMSCodeService {
	
    @Autowired
    private CodeUtils codeUtils;
	
    @Autowired
    private MemcachedClient memcachedClient;

    //设置值到缓存中使用set操作
    public String sendCodeToSMS(String tele) {
        String code = codeUtils.generator(tele);
        try {
            memcachedClient.set(tele,10,code);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return code;
    }
    
	//从缓存中取值使用get操作
    public boolean checkCode(SMSCode smsCode) {
        String code = null;
        try {
            code = memcachedClient.get(smsCode.getTele()).toString();
        } catch (Exception e) {
            e.printStackTrace();
        }
        return smsCode.getCode().equals(code);
    }
}

上述代码中对于服务器的配置使用硬编码写死到了代码中,将此数据提取出来,做成独立的配置属性。

  • 定义配置类,加载必要的配置属性,读取配置文件中memcached节点信息
@Component
@ConfigurationProperties(prefix = "memcached")
@Data
public class XMemcachedProperties {
    private String servers;
    private int poolSize;
    private long opTimeout;
}
  • 定义memcached节点信息
memcached:
  servers: localhost:11211
  poolSize: 10
  opTimeout: 3000
  • 在memcached配置类中加载信息
@Configuration
public class XMemcachedConfig {
    @Autowired
    private XMemcachedProperties pro;
    @Bean
    public MemcachedClient getMemcachedClient() throws IOException {
        MemcachedClientBuilder mcb = new XMemcachedClientBuilder(pro.getServers());
        mcb.setConnectionPoolSize(props.getPoolSize());
        mcb.setOpTimeout(props.getOpTimeout());
        MemcachedClient memcachedClient = mcb.build();
        return memcachedClient;
    }
}

# SpringBoot整合jetcache缓存

jetcache严格意义上来说,并不是一个缓存解决方案,只能说他算是一个缓存框架,然后把别的缓存放到jetcache中管理,这样就可以支持AB缓存一起用了

jetcache并不是随便拿两个缓存都能拼到一起去的。目前jetcache支持的缓存方案本地缓存支持两种,远程缓存支持两种,分别如下:

  • 本地缓存(Local)
    • LinkedHashMap
    • Caffeine
  • 远程缓存(Remote)
    • Redis
    • Tair

纯远程方案:

  • 导入springboot整合jetcache对应的坐标starter,当前坐标默认使用的远程方案是redis
<dependency>
    <groupId>com.alicp.jetcache</groupId>
    <artifactId>jetcache-starter-redis</artifactId>
    <version>2.6.2</version>
</dependency>
  • 远程方案基本配置
jetcache:
  remote:
    default:
      type: redis
      host: localhost
      port: 6379
	  keyConvertor: fastjson
      valueEncode: java
      valueDecode: java
	  # 必配项,否则会报错
      poolConfig: 
        maxTotal: 50

注意

由于redis缓存中不支持保存对象,因此需要对redis设置当Object类型数据进入到redis中时如何进行类型转换。需要配置keyConvertor表示key的类型转换方式,同时标注value的转换类型方式,值进入redis时是java类型,标注valueEncode为java,值从redis中读取时转换成java,标注valueDecode为java

  • 启用缓存,在引导类上方标注注解@EnableCreateCacheAnnotation配置springboot程序中可以使用注解的形式创建缓存
@SpringBootApplication
//jetcache启用缓存的主开关
@EnableCreateCacheAnnotation
public class Springboot20JetCacheApplication {
    public static void main(String[] args) {
        SpringApplication.run(Springboot20JetCacheApplication.class, args);
    }
}
  • 创建缓存对象Cache,并使用注解@CreateCache标记当前缓存的信息,然后使用Cache对象的API操作缓存,put写缓存,get读缓存
@Service
public class SMSCodeServiceImpl implements SMSCodeService {
    @Autowired
    private CodeUtils codeUtils;
    
	//可以为某个缓存对象设置过期时间,将同类型的数据放入缓存中
    @CreateCache(name="jetCache_",expire = 10,timeUnit = TimeUnit.SECONDS)
    private Cache<String ,String> jetCache;

    public String sendCodeToSMS(String tele) {
        String code = codeUtils.generator(tele);
        jetCache.put(tele,code);
        return code;
    }

    public boolean checkCode(SMSCode smsCode) {
        String code = jetCache.get(smsCode.getTele());
        return smsCode.getCode().equals(code);
    }
}
  • 上述方案中使用的是配置中定义的default缓存,其实这个default是个名字,可以随便写,也可以随便加。例如再添加一种缓存解决方案,参照如下配置进行:
jetcache:
  remote:
    default:
      type: redis
      host: localhost
      port: 6379
      poolConfig:
        maxTotal: 50
    sms:
      type: redis
      host: localhost
      port: 6379
      poolConfig:
        maxTotal: 50
  • 如果想使用名称是sms的缓存,需要再创建缓存时指定参数area,声明使用对应缓存即可
@Service
public class SMSCodeServiceImpl implements SMSCodeService {
    @Autowired
    private CodeUtils codeUtils;
    
    @CreateCache(area="sms",name="jetCache_",expire = 10,timeUnit = TimeUnit.SECONDS)
    private Cache<String ,String> jetCache;

    public String sendCodeToSMS(String tele) {
        String code = codeUtils.generator(tele);
        jetCache.put(tele,code);
        return code;
    }

    public boolean checkCode(SMSCode smsCode) {
        String code = jetCache.get(smsCode.getTele());
        return smsCode.getCode().equals(code);
    }
}

纯本地方案:

远程方案中,配置中使用remote表示远程,换成local就是本地,只不过类型不一样而已

  • 导入springboot整合jetcache对应的坐标starter
<dependency>
    <groupId>com.alicp.jetcache</groupId>
    <artifactId>jetcache-starter-redis</artifactId>
    <version>2.6.2</version>
</dependency>
  • 本地缓存基本配置
jetcache:
  local:
    default:
      type: linkedhashmap
      keyConvertor: fastjson

注意

为了加速数据获取时key的匹配速度,jetcache要求指定key的类型转换器。简单说就是,如果你给了一个Object作为key的话,我先用key的类型转换器给转换成字符串,然后再保存。等到获取数据时,仍然是先使用给定的Object转换成字符串,然后根据字符串匹配。由于jetcache是阿里的技术,这里推荐key的类型转换器使用阿里的fastjson

  • 启用缓存
@SpringBootApplication
//jetcache启用缓存的主开关
@EnableCreateCacheAnnotation
public class Springboot20JetCacheApplication {
    public static void main(String[] args) {
        SpringApplication.run(Springboot20JetCacheApplication.class, args);
    }
}
  • 创建缓存对象Cache时,标注当前使用本地缓存
@Service
public class SMSCodeServiceImpl implements SMSCodeService {
	//cacheType控制当前缓存使用本地缓存还是远程缓存
	//cacheType如果不进行配置,默认值是REMOTE,即仅使用远程缓存方案
    @CreateCache(name="jetCache_",expire = 1000,timeUnit = TimeUnit.SECONDS,cacheType = CacheType.LOCAL)
    private Cache<String ,String> jetCache;

    public String sendCodeToSMS(String tele) {
        String code = codeUtils.generator(tele);
        jetCache.put(tele,code);
        return code;
    }

    public boolean checkCode(SMSCode smsCode) {
        String code = jetCache.get(smsCode.getTele());
        return smsCode.getCode().equals(code);
    }
}

本地+远程方案:

  • 两种方案一起使用如何配置呢?其实就是将两种配置合并到一起就可以了
jetcache:
  local:
    default:
      type: linkedhashmap
      keyConvertor: fastjson
  remote:
    default:
      type: redis
      host: localhost
      port: 6379
      poolConfig:
        maxTotal: 50
    sms:
      type: redis
      host: localhost
      port: 6379
      poolConfig:
        maxTotal: 50
  • 在创建缓存的时候,配置cacheType为BOTH即则本地缓存与远程缓存同时使用
@Service
public class SMSCodeServiceImpl implements SMSCodeService {
    @CreateCache(name="jetCache_",expire = 1000,timeUnit = TimeUnit.SECONDS,cacheType = CacheType.BOTH)
    private Cache<String ,String> jetCache;
}

方法缓存

以上方案仅支持手工控制缓存,但是springcache方案中的方法缓存特别好用,给一个方法添加一个注解,方法就会自动使用缓存。jetcache也提供了对应的功能,即方法缓存

jetcache提供了方法缓存方案,只不过名称变更了而已。在对应的操作接口上方使用注解@Cached即可

  • 导入springboot整合jetcache对应的坐标starter
<dependency>
    <groupId>com.alicp.jetcache</groupId>
    <artifactId>jetcache-starter-redis</artifactId>
    <version>2.6.2</version>
</dependency>
  • 配置缓存
jetcache:
  local:
    default:
      type: linkedhashmap
      keyConvertor: fastjson
  remote:
    default:
      type: redis
      host: localhost
      port: 6379
      keyConvertor: fastjson
      valueEncode: java
      valueDecode: java
      poolConfig:
        maxTotal: 50
    sms:
      type: redis
      host: localhost
      port: 6379
      poolConfig:
        maxTotal: 50

注意

为了实现Object类型的值进出redis,需要保障进出redis的Object类型的数据必须实现序列化接口

@Data
public class Book implements Serializable {
    private Integer id;
    private String type;
    private String name;
    private String description;
}
  • 启用缓存时开启方法缓存功能,并配置basePackages,说明在哪些包中开启方法缓存
@SpringBootApplication
//jetcache启用缓存的主开关
@EnableCreateCacheAnnotation
//开启方法注解缓存
@EnableMethodCache(basePackages = "com.itheima")
public class Springboot20JetCacheApplication {
    public static void main(String[] args) {
        SpringApplication.run(Springboot20JetCacheApplication.class, args);
    }
}
  • 使用注解@Cached标注当前方法使用缓存
@Service
public class BookServiceImpl implements BookService {
    @Autowired
    private BookDao bookDao;
    
    @Override
    @Cached(name="book_",key="#id",expire = 3600,cacheType = CacheType.REMOTE)
    public Book getById(Integer id) {
        return bookDao.selectById(id);
    }
}

远程方案的数据同步:

由于远程方案中redis保存的数据可以被多个客户端共享,这就存在了数据同步问题。jetcache提供了3个注解解决此问题,分别在更新、删除操作时同步缓存数据,和读取缓存时定时刷新数据

//更新缓存
@CacheUpdate(name="book_",key="#book.id",value="#book")
public boolean update(Book book) {
    return bookDao.updateById(book) > 0;
}

//删除缓存
@CacheInvalidate(name="book_",key = "#id")
public boolean delete(Integer id) {
    return bookDao.deleteById(id) > 0;
}

//定时刷新缓存
@Cached(name="book_",key="#id",expire = 3600,cacheType = CacheType.REMOTE)
@CacheRefresh(refresh = 5)
public Book getById(Integer id) {
    return bookDao.selectById(id);
}

数据报表:

jetcache还提供有简单的数据报表功能,帮助开发者快速查看缓存命中信息,只需要添加一个配置即可

jetcache:
  statIntervalMinutes: 1

设置后,每1分钟在控制台输出缓存数据命中信息

[DefaultExecutor] c.alicp.jetcache.support.StatInfoLogger  : jetcache stat from 2022-02-28 09:32:15,892 to 2022-02-28 09:33:00,003
cache    |    qps|   rate|   get|    hit|   fail|   expire|   avgLoadTime|   maxLoadTime
---------+-------+-------+------+-------+-------+---------+--------------+--------------
book_    |   0.66| 75.86%|    29|     22|      0|        0|          28.0|           188
---------+-------+-------+------+-------+-------+---------+--------------+--------------

# SpringBoot整合j2cache

jetcache可以在限定范围内构建多级缓存,但是灵活性不足,不能随意搭配缓存,j2cache是可以随意搭配缓存解决方案的缓存整合框架。以Ehcache与redis整合为例:

  • 导入j2cache、redis、ehcache坐标,j2cache的starter中默认包含了redis坐标,官方推荐使用redis作为二级缓存,因此此处无需导入redis坐标
<dependency>
    <groupId>net.oschina.j2cache</groupId>
    <artifactId>j2cache-core</artifactId>
    <version>2.8.4-release</version>
</dependency>
<dependency>
    <groupId>net.oschina.j2cache</groupId>
    <artifactId>j2cache-spring-boot2-starter</artifactId>
    <version>2.8.0-release</version>
</dependency>
<dependency>
    <groupId>net.sf.ehcache</groupId>
    <artifactId>ehcache</artifactId>
</dependency>
  • 配置一级与二级缓存,并配置一二级缓存间数据传递方式,配置书写在名称为j2cache.properties的文件中。如果使用ehcache还需要单独添加ehcache的配置文件
# 1级缓存
j2cache.L1.provider_class = ehcache
ehcache.configXml = ehcache.xml

# 2级缓存
j2cache.L2.provider_class = net.oschina.j2cache.cache.support.redis.SpringRedisProvider
j2cache.L2.config_section = redis
redis.hosts = localhost:6379

# 1级缓存中的数据如何到达二级缓存
j2cache.broadcast = net.oschina.j2cache.cache.support.redis.SpringRedisPubSubPolicy

注意

  • 此处配置不能乱配置,需要参照官方给出的配置说明进行。例如1级供应商选择ehcache,供应商名称仅仅是一个ehcache,但是2级供应商选择redis时要写专用的Spring整合Redis的供应商类名SpringRedisProvider,而且这个名称并不是所有的redis包中能提供的,也不是spring包中提供的。因此配置j2cache必须参照官方文档配置,而且还要去找专用的整合包,导入对应坐标才可以使用
  • 一级与二级缓存最重要的一个配置就是两者之间的数据沟通方式,此类配置也不是随意配置的,并且不同的缓存解决方案提供的数据沟通方式差异化很大,需要查询官方文档进行设置
  • 使用缓存,j2cache的使用和jetcache比较类似,但是无需开启使用的开关,直接定义缓存对象即可使用,缓存对象名CacheChannel
@Service
public class SMSCodeServiceImpl implements SMSCodeService {
    @Autowired
    private CodeUtils codeUtils;

    @Autowired
    private CacheChannel cacheChannel;

    public String sendCodeToSMS(String tele) {
        String code = codeUtils.generator(tele);
        cacheChannel.set("sms",tele,code);
        return code;
    }

    public boolean checkCode(SMSCode smsCode) {
        String code = cacheChannel.get("sms",smsCode.getTele()).asString();
        return smsCode.getCode().equals(code);
    }
}
  • j2cache的使用不复杂,配置是j2cache的核心,毕竟是一个整合型的缓存框架。缓存相关的配置过多,可以查阅j2cache-core核心包中的j2cache.properties文件中的说明。如下:
#J2Cache configuration
#########################################
# Cache Broadcast Method
# values:
# jgroups -> use jgroups's multicast
# redis -> use redis publish/subscribe mechanism (using jedis)
# lettuce -> use redis publish/subscribe mechanism (using lettuce, Recommend)
# rabbitmq -> use RabbitMQ publisher/consumer mechanism
# rocketmq -> use RocketMQ publisher/consumer mechanism
# none -> don't notify the other nodes in cluster
# xx.xxxx.xxxx.Xxxxx your own cache broadcast policy classname that implement net.oschina.j2cache.cluster.ClusterPolicy
#########################################
j2cache.broadcast = redis

# jgroups properties
jgroups.channel.name = j2cache
jgroups.configXml = /network.xml

# RabbitMQ properties
rabbitmq.exchange = j2cache
rabbitmq.host = localhost
rabbitmq.port = 5672
rabbitmq.username = guest
rabbitmq.password = guest

# RocketMQ properties
rocketmq.name = j2cache
rocketmq.topic = j2cache
# use ; to split multi hosts
rocketmq.hosts = 127.0.0.1:9876

#########################################
# Level 1&2 provider
# values:
# none -> disable this level cache
# ehcache -> use ehcache2 as level 1 cache
# ehcache3 -> use ehcache3 as level 1 cache
# caffeine -> use caffeine as level 1 cache(only in memory)
# redis -> use redis as level 2 cache (using jedis)
# lettuce -> use redis as level 2 cache (using lettuce)
# readonly-redis -> use redis as level 2 cache ,but never write data to it. if use this provider, you must uncomment `j2cache.L2.config_section` to make the redis configurations available.
# memcached -> use memcached as level 2 cache (xmemcached),
# [classname] -> use custom provider
#########################################

j2cache.L1.provider_class = caffeine
j2cache.L2.provider_class = redis

# When L2 provider isn't `redis`, using `L2.config_section = redis` to read redis configurations
# j2cache.L2.config_section = redis

# Enable/Disable ttl in redis cache data (if disabled, the object in redis will never expire, default:true)
# NOTICE: redis hash mode (redis.storage = hash) do not support this feature)
j2cache.sync_ttl_to_redis = true

# Whether to cache null objects by default (default false)
j2cache.default_cache_null_object = true

#########################################
# Cache Serialization Provider
# values:
# fst -> using fast-serialization (recommend)
# kryo -> using kryo serialization
# json -> using fst's json serialization (testing)
# fastjson -> using fastjson serialization (embed non-static class not support)
# java -> java standard
# fse -> using fse serialization
# [classname implements Serializer]
#########################################

j2cache.serialization = json
#json.map.person = net.oschina.j2cache.demo.Person

#########################################
# Ehcache configuration
#########################################

# ehcache.configXml = /ehcache.xml

# ehcache3.configXml = /ehcache3.xml
# ehcache3.defaultHeapSize = 1000

#########################################
# Caffeine configuration
# caffeine.region.[name] = size, xxxx[s|m|h|d]
#
#########################################
caffeine.properties = /caffeine.properties

#########################################
# Redis connection configuration
#########################################

#########################################
# Redis Cluster Mode
#
# single -> single redis server
# sentinel -> master-slaves servers
# cluster -> cluster servers (数据库配置无效,使用 database = 0)
# sharded -> sharded servers  (密码、数据库必须在 hosts 中指定,且连接池配置无效 ; redis://user:password@127.0.0.1:6379/0)
#
#########################################

redis.mode = single

#redis storage mode (generic|hash)
redis.storage = generic

## redis pub/sub channel name
redis.channel = j2cache
## redis pub/sub server (using redis.hosts when empty)
redis.channel.host =

#cluster name just for sharded
redis.cluster_name = j2cache

## redis cache namespace optional, default[empty]
redis.namespace =

## redis command scan parameter count, default[1000]
#redis.scanCount = 1000

## connection
# Separate multiple redis nodes with commas, such as 192.168.0.10:6379,192.168.0.11:6379,192.168.0.12:6379

redis.hosts = 127.0.0.1:6379
redis.timeout = 2000
redis.password =
redis.database = 0
redis.ssl = false

## redis pool properties
redis.maxTotal = 100
redis.maxIdle = 10
redis.maxWaitMillis = 5000
redis.minEvictableIdleTimeMillis = 60000
redis.minIdle = 1
redis.numTestsPerEvictionRun = 10
redis.lifo = false
redis.softMinEvictableIdleTimeMillis = 10
redis.testOnBorrow = true
redis.testOnReturn = false
redis.testWhileIdle = true
redis.timeBetweenEvictionRunsMillis = 300000
redis.blockWhenExhausted = false
redis.jmxEnabled = false

#########################################
# Lettuce scheme
#
# redis -> single redis server
# rediss -> single redis server with ssl
# redis-sentinel -> redis sentinel
# redis-cluster -> cluster servers
#
#########################################

#########################################
# Lettuce Mode
#
# single -> single redis server
# sentinel -> master-slaves servers
# cluster -> cluster servers (数据库配置无效,使用 database = 0)
# sharded -> sharded servers  (密码、数据库必须在 hosts 中指定,且连接池配置无效 ; redis://user:password@127.0.0.1:6379/0)
#
#########################################

## redis command scan parameter count, default[1000]
#lettuce.scanCount = 1000
lettuce.mode = single
lettuce.namespace =
lettuce.storage = hash
lettuce.channel = j2cache
lettuce.scheme = redis
lettuce.hosts = 127.0.0.1:6379
lettuce.password =
lettuce.database = 0
lettuce.sentinelMasterId =
lettuce.maxTotal = 100
lettuce.maxIdle = 10
lettuce.minIdle = 10
# timeout in milliseconds
lettuce.timeout = 10000
# redis cluster topology refresh interval in milliseconds
lettuce.clusterTopologyRefresh = 3000

#########################################
# memcached server configurations
# refer to https://gitee.com/mirrors/XMemcached
#########################################

memcached.servers = 127.0.0.1:11211
memcached.username =
memcached.password =
memcached.connectionPoolSize = 10
memcached.connectTimeout = 1000
memcached.failureMode = false
memcached.healSessionInterval = 1000
memcached.maxQueuedNoReplyOperations = 100
memcached.opTimeout = 100
memcached.sanitizeKeys = false

# 整合定时任务

# cron表达式语法

[秒] [分] [小时] [日] [月] [周] [年]

cron表达式语法

通配符说明:

  • * 表示所有值。 例如:在分的字段上设置 *,表示每一分钟都会触发
  • ? 表示不需要关心当前这个属性。例如:在每月的10号触发但不关心是周几,应设置为 0 0 0 10 * ?
  • - 表示区间。例如:在小时上设置 “10-12”,表示 10,11,12点都会触发
  • , 表示指定多个值。例如:在周字段上设置 “MON,WED,FRI” 表示周一,周三和周五触发
  • / 用于递增触发。如在秒上面设置”5/15” 表示从5秒开始,每增15秒触发(5,20,35,50)
  • L 表示最后的意思。在日字段设置上,表示当月的最后一天, 在周字段上表示星期六。如果在”L”前加上数字,则表示该数据的最后一个。例如在周字段上设置”6L”这样的格式,则表示“本月最后一个星期五”
  • W 表示离指定日期的最近那个工作日(周一至周五)。例如在日字段上置”15W”,表示离每月15号最近的那个工作日触发
  • # 表示每月的第几个周几,例如在周字段上设置”6#3”表示在每月的第三个周六。注意如果指定”#5”,正好第五周没有周六,则不会触发该配置

小提示

  • L和W可以一组合使用。如果在日字段上设置LW则表示在本月的最后一个工作日触发
  • 周字段的设置,若使用英文字母是不区分大小写的,即MON与mon相同

# SpringBoot整合Quartz

  • 导入springboot整合Quartz的starter
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-quartz</artifactId>
</dependency>
  • 定义任务Bean,按照Quartz的开发规范制作,继承QuartzJobBean
public class MyQuartz extends QuartzJobBean {
   @Override
   protected void executeInternal(JobExecutionContext context) throws JobExecutionException{
        System.out.println("quartz task run...");
   }
}
  • 创建Quartz配置类,定义工作明细(JobDetail)与触发器的(Trigger)bean
@Configuration
public class QuartzConfig {
    @Bean
    public JobDetail printJobDetail(){
        //绑定具体的工作
        return JobBuilder.newJob(MyQuartz.class).storeDurably().build();
    }
    @Bean
    public Trigger printJobTrigger(){
        ScheduleBuilder schedBuilder = CronScheduleBuilder.cronSchedule("0/5 * * * * ?");
        //绑定对应的工作明细
        return TriggerBuilder.newTrigger().forJob(printJobDetail())
		                                  .withSchedule(schedBuilder).build();
    }
}

注意

  • 工作明细中要设置对应的具体工作,使用newJob()操作传入对应的工作任务类型即可
  • 触发器需要绑定任务,使用forJob()操作传入绑定的工作明细对象。此处可以为工作明细设置名称然后使用名称绑定,也可以直接调用对应方法绑定。触发器中最核心的规则是执行时间,此处使用调度器定义执行时间,执行时间描述方式使用的是cron表达式

# SpringBoot整合Task

  • 开启定时任务功能,在引导类上开启定时任务功能的开关,使用注解@EnableScheduling
@SpringBootApplication
//开启定时任务功能
@EnableScheduling
public class Springboot22TaskApplication {
    public static void main(String[] args) {
        SpringApplication.run(Springboot22TaskApplication.class, args);
    }
}
  • 定义Bean,在对应要定时执行的操作上方,使用注解@Scheduled定义执行的时间,执行时间的描述方式还是cron表达式
@Component
public class MyBean {
    @Scheduled(cron = "0/1 * * * * ?")
    public void print(){
        System.out.println(Thread.currentThread().getName()+" :spring task run...");
    }
}
  • 如果想对定时任务进行相关配置,可以通过配置文件进行
spring:
  task:
   	scheduling:
      pool:
       	size: 1							# 任务调度线程池大小 默认 1
      thread-name-prefix: ssm_      	# 调度线程名称前缀 默认 scheduling-      
        shutdown:
          await-termination: false		# 线程池关闭时等待所有任务完成
          await-termination-period: 10s	# 调度线程关闭前最大等待时间,确保最后一定关闭

# SpringBoot整合javamail

学习邮件发送之前先了解3个概念:

  • SMTP(Simple Mail Transfer Protocol):简单邮件传输协议,用于发送电子邮件的传输协议
  • POP3(Post Office Protocol - Version 3):用于接收电子邮件的标准协议
  • IMAP(Internet Mail Access Protocol):互联网消息协议,是POP3的替代协议

简单说就是SMPT是发邮件的标准,POP3是收邮件的标准,IMAP是对POP3的升级

  • 导入springboot整合javamail的starter
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-mail</artifactId>
</dependency>
  • 配置邮箱的登录信息
spring:
  mail:
    host: smtp.126.com
    username: test@126.com
    password: test

注意

password并不是邮箱账号的登录密码,是邮件供应商提供的一个加密后的密码,需要到邮件供应商的设置页面找POP3或IMAP,开启这些设置

  • 使用JavaMailSender接口发送邮件
@Service
public class SendMailServiceImpl implements SendMailService {
    @Autowired
    private JavaMailSender javaMailSender;

    //发送人
    private String from = "test@qq.com";
    //接收人
    private String to = "test@126.com";
    //标题
    private String subject = "测试邮件";
    //正文
    private String context = "测试邮件正文内容";

    @Override
    public void sendMail() {
        SimpleMailMessage message = new SimpleMailMessage();
        message.setFrom(from+"(小甜甜)");
        message.setTo(to);
        message.setSubject(subject);
        message.setText(context);
        javaMailSender.send(message);
    }
}

发送简单邮件仅需要提供对应的4个基本信息就可以了,如果想发送复杂的邮件,需要更换邮件对象。使用MimeMessage可以发送特殊的邮件

  • 发送网页正文邮件
@Service
public class SendMailServiceImpl2 implements SendMailService {
    @Autowired
    private JavaMailSender javaMailSender;

    //发送人
    private String from = "test@qq.com";
    //接收人
    private String to = "test@126.com";
    //标题
    private String subject = "测试邮件";
    //正文
    private String context = "<img src='ABC.JPG'/><a href='https://www.itcast.cn'>点开</a>";

    public void sendMail() {
        try {
            MimeMessage message = javaMailSender.createMimeMessage();
            MimeMessageHelper helper = new MimeMessageHelper(message);
			//可以单独设置收件人名称:(小甜甜)
            helper.setFrom(to+"(小甜甜)");
            helper.setTo(from);
            helper.setSubject(subject);
			//此处设置正文支持html解析
            helper.setText(context,true);		

            javaMailSender.send(message);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
  • 发送带有附件的邮件
@Service
public class SendMailServiceImpl2 implements SendMailService {
    @Autowired
    private JavaMailSender javaMailSender;

    //发送人
    private String from = "test@qq.com";
    //接收人
    private String to = "test@126.com";
    //标题
    private String subject = "测试邮件";
    //正文
    private String context = "测试邮件正文";

    public void sendMail() {
        try {
            MimeMessage message = javaMailSender.createMimeMessage();
			//此处设置支持附件
            MimeMessageHelper helper = new MimeMessageHelper(message,true);		
            helper.setFrom(to+"(小甜甜)");
            helper.setTo(from);
            helper.setSubject(subject);
            helper.setText(context);

            //添加附件
            File f1 = new File("springboot_23_mail-0.0.1-SNAPSHOT.jar");
            File f2 = new File("resources\\logo.png");

            helper.addAttachment(f1.getName(),f1);
            helper.addAttachment("最靠谱的培训结构.png",f2);

            javaMailSender.send(message);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

# 整合主从数据库

dynamic-datasource-spring-boot-starter (opens new window) 是一个基于 springboot 的快速集成多数据源的启动器

  • 本框架只做 切换数据源 这件核心的事情,并不限制你的具体操作,切换了数据源可以做任何 CRUD
  • 配置文件所有以下划线 _ 分割的数据源"首部"即为组的名称,相同组名称的数据源会放在一个组下
  • 切换数据源可以是组名,也可以是具体数据源名称。组名则切换时采用负载均衡算法切换
  • 默认的数据源名称为 master ,你可以通过 spring.datasource.dynamic.primary 修改
  • 支持数据库敏感配置信息 加密(可自定义) ENC()
  • 方法上的注解优先于类上注解

# 使用方法

  • 引入dynamic-datasource-spring-boot-starter
<dependency>
  <groupId>com.baomidou</groupId>
  <artifactId>dynamic-datasource-spring-boot-starter</artifactId>
  <version>${version}</version>
</dependency>
  • 配置数据源
spring:
  datasource:
    dynamic:
      primary: master #设置默认的数据源或者数据源组,默认值即为master
      strict: false #严格匹配数据源,默认false使用默认数据源,true未匹配到指定数据源时抛异常
      datasource:
        master:
          url: jdbc:mysql://xx.xx.xx.xx:3306/dynamic
          username: root
          password: 123456
          driver-class-name: com.mysql.jdbc.Driver # 3.2.0开始支持SPI可省略此配置
        slave_1:
          url: jdbc:mysql://xx.xx.xx.xx:3307/dynamic
          username: root
          password: 123456
          driver-class-name: com.mysql.jdbc.Driver
        slave_2:
          url: ENC(xxxxx) # 内置加密,使用请查看详细文档
          username: ENC(xxxxx)
          password: ENC(xxxxx)
          driver-class-name: com.mysql.jdbc.Driver

配置数据源

  • 使用 @DS 切换数据源
@Service
@DS("slave")
public class UserServiceImpl implements UserService {

  @Autowired
  private JdbcTemplate jdbcTemplate;

  public List selectAll() {
    return  jdbcTemplate.queryForList("select * from user");
  }
  
  @Override
  @DS("slave_1")
  public List selectByCondition() {
    return  jdbcTemplate.queryForList("select * from user where age >10");
  }
}

# 手动切换数据源

在需要切换数据源的方法中使用DynamicDataSourceContextHolder类实现手动切换,使用方法如下:

public List<SysUser> selectUserList(SysUser user)
{
	DynamicDataSourceContextHolder.setDataSourceType(DataSourceType.SLAVE.name());
	List<SysUser> userList = userMapper.selectUserList(user);
	DynamicDataSourceContextHolder.clearDataSourceType();
	return userList;
}

注意

DataSourceAutoConfiguration.class默认会帮我们自动配置单数据源,所以,如果想在项目中使用多数据源就需要排除它,手动指定多数据源。

@SpringBootApplication(exclude={DataSourceAutoConfiguration.calss})

# 整合swagger

Spring已经将Swagger纳入自身的标准,建立了Spring-swagger项目,现在叫Springfox。通过在项目中引入Springfox ,即可非常简单快捷的使用Swagger

# 简单使用

  • 引入依赖
<dependency>
    <groupId>io.springfox</groupId>
    <artifactId>springfox-swagger2</artifactId>
</dependency>
<dependency>
    <groupId>io.springfox</groupId>
    <artifactId>springfox-swagger-ui</artifactId>
</dependency>
  • 编写SwaggerConfig配置Swagger
@Configuration //配置类
@EnableSwagger2// 开启Swagger2的自动配置
public class SwaggerConfig { 
	
}
  • 访问测试:启动项目,输入http://ip地址:端口号/swagger-ui.html

运行时报错:failed to start bean 'documentationpluginsbootstrapper'

需要在配置文件中加上如下配置,将 SpringMVC 默认路径匹配策略改成AntPathMatcher

spring.mvc.pathmatch.matching-strategy=ant_path_matcher

# 配置Swagger

  • 配置Docket实例,Swagger实例Bean是Docket,所以通过配置Docket实例来配置Swaggger具体参数
@Bean 
public Docket docket() {
   return new Docket(DocumentationType.SWAGGER_2);
}
  • 通过apiInfo()属性配置文档信息
//配置文档信息
private ApiInfo apiInfo() {
   Contact contact = new Contact("联系人名字", "http://xxx.xxx.com联系人链接", "联系邮箱");
   return new ApiInfo(
           "Swagger学习", // 标题
           "学习演示如何配置Swagger", // 描述
           "v1.0", // 版本
           "http://terms.service.url/组织链接", // 组织链接
           contact, // 联系人信息
           "Apach 2.0 许可", // 许可
           "许可链接", // 许可连接
           new ArrayList<>()// 扩展
  );
}
  • Docket实例关联上apiInfo()
@Bean
public Docket docket() {
   return new Docket(DocumentationType.SWAGGER_2).apiInfo(apiInfo());
}

# 配置扫描接口

  • 构建Docket时通过select()方法配置扫描接口
@Bean
public Docket docket() {
   return new Docket(DocumentationType.SWAGGER_2)
      .apiInfo(apiInfo())
	  // 通过select()去配置扫描接口,RequestHandlerSelectors配置如何扫描接口
      .select().apis(RequestHandlerSelectors.basePackage("com.ljh.controller"))
      .build();
}

//RequestHandlerSelectors除了通过包路径配置扫描接口外,还可以通过配置其他方式扫描接口
any() // 扫描所有,项目中的所有接口都会被扫描到
none() // 不扫描接口
//通过方法上的注解扫描,如withMethodAnnotation(GetMapping.class)只扫描get请求
withMethodAnnotation(final Class<? extends Annotation> annotation)
//通过类上的注解扫描,如withClassAnnotation(Controller.class)只扫描有controller注解的类中的接口
withClassAnnotation(final Class<? extends Annotation> annotation)
//根据包路径扫描接口
basePackage(final String basePackage) 
  • 可以选择配置接口扫描过滤
//paths(PathSelectors.ant("/ljh/**"))
@Bean
public Docket docket() {
   return new Docket(DocumentationType.SWAGGER_2)
      .apiInfo(apiInfo())
      .select().apis(RequestHandlerSelectors.basePackage("com.ljh.controller"))
       // 配置如何通过path过滤,即这里只扫描请求以/ljh开头的接口
      .paths(PathSelectors.ant("/ljh/**"))
      .build();
}

//PathSelectors其它可选值:
any() // 任何请求都扫描
none() // 任何请求都不扫描
regex(final String pathRegex) // 通过正则表达式控制
ant(final String antPattern) // 通过ant()控制

# 配置Swagger开关

  • 通过enable()方法配置是否启用Swagger
@Bean
public Docket docket() {
   return new Docket(DocumentationType.SWAGGER_2)
      .apiInfo(apiInfo())
	  //配置是否启用Swagger,如果是false,在浏览器将无法访问
      .enable(false) 
      .select()
      .apis(RequestHandlerSelectors.basePackage("com.ljh.controller"))
      .paths(PathSelectors.ant("/ljh/**"))
      .build();
}
  • 动态配置项目处于开发环境显示Swagger
//动态配置当项目处于开发环境(dev)或者test时显示swagger,处于生产环境(prod)时不显示
@Bean
public Docket docket(Environment environment) {
   // 设置要显示swagger的环境
   Profiles of = Profiles.of("dev", "test");
   // 判断当前是否处于该环境
   // 通过 enable() 接收此参数判断是否要显示
   boolean b = environment.acceptsProfiles(of);
   
   return new Docket(DocumentationType.SWAGGER_2)
      .apiInfo(apiInfo())
      .enable(b) //配置是否启用Swagger,如果是false,在浏览器将无法访问
      .select()
      .apis(RequestHandlerSelectors.basePackage("com.ljh.controller"))
      .paths(PathSelectors.ant("/ljh/**"))
      .build();
}

# 配置API分组

  • 通过groupName()方法配置分组,如果没有配置分组,默认是default
@Bean
public Docket docket(Environment environment) {
   return new Docket(DocumentationType.SWAGGER_2).apiInfo(apiInfo())
      .groupName("hello") // 配置分组
       // 省略配置....
}
  • 配置多个分组只需要配置多个docket即可
@Bean
public Docket docket1(){
   return new Docket(DocumentationType.SWAGGER_2).groupName("group1");
}
@Bean
public Docket docket2(){
   return new Docket(DocumentationType.SWAGGER_2).groupName("group2");
}
@Bean
public Docket docket3(){
   return new Docket(DocumentationType.SWAGGER_2).groupName("group3");
}

# 配置统一认证可以实现全局token认证

@Bean
public Docket docket() {
   return new Docket(DocumentationType.SWAGGER_2)
      .apiInfo(apiInfo())
	  //配置是否启用Swagger,如果是false,在浏览器将无法访问
      .enable(false) 
      .select()
      .apis(RequestHandlerSelectors.basePackage("com.ljh.controller"))
      .paths(PathSelectors.ant("/ljh/**"))
      .build()
	  // 设置鉴权方式
	  .securitySchemes(securitySchemes())
	  //设置鉴权范围
	  .securityContexts(securityContexts());
}

//设置鉴权策略
//Swagger-ui为我们提供了三种鉴权方式:ApiKey、BasicAuth、OAuth
private List<ApiKey> securitySchemes() {
	List<ApiKey> apiKeys = new ArrayList<>();
	//API 密钥的名字(可任意取)、键名、和所处位置
	apiKeys.add(new ApiKey("Authorization", "Authorization", "header"));
	return apiKeys;
}

// 配置默认的全局鉴权策略的开关,以及通过正则表达式进行匹配;默认 ^.*$ 匹配所有URL
private List<SecurityContext> securityContexts() {
	List<SecurityContext> securityContexts = new ArrayList<>();
	securityContexts.add(SecurityContext.builder()
	        //通过AuthorizationScope对象的构造函数配置了作用域
			//再通过SecurityReference()对象和上面的鉴权方式匹配
			.securityReferences(defaultAuth())
			//forPaths() 中放入要生效的接口路径的正则表达式
			//PathSelectors.regex(“^(?!auth).*$”) 表示所有带有 auth的接口路径
			.operationSelector(o -> o.requestMappingPattern().matches("/.*"))
			.build());
	return securityContexts;
}

private List<SecurityReference> defaultAuth() {
	AuthorizationScope authorizationScope = new AuthorizationScope("global", "accessEverything");
	AuthorizationScope[] authorizationScopes = new AuthorizationScope[1];
	authorizationScopes[0] = authorizationScope;
	List<SecurityReference> securityReferences = new ArrayList<>();
	//SecurityReference对象中的第一个参数需要和ApiKey中的name相同,实现计划和范围匹配
	securityReferences.add(new SecurityReference("Authorization", authorizationScopes));
	return securityReferences;
}

# Swagger注解

//类注解,在控制器类添加此注解,可以对控制器类进行功能说明
@Api(value = "提供用户列表接口",tags = "用户管理")

//方法注解
@ApiOperation

//接口方法注解,添加此注解的方法将不会生成到接口文档中
@ApiIgnore


//当接口参数和返回对象为对象参数时,在实体类中添加注解说明
//在聚合工程中需要分别在相应的子工程中引入swagger2依赖
@ApiModel@ApiModelProperty


//方法参数注解
@ApiImplicitParams@ApiImplicitParam

# 路径匹配规则

Spring Boot 2.6及以上默认路劲的匹配规则是 path_pattern_parser,2.6之前默认的是 ant_path_matcher

spring:
  mvc:
    pathmatch:
      matching-strategy: path_pattern_parser # ant_path_matcher

# 两者的差别

  • ANT_PATH_MATCHER 是 Spring MVC 中常用的一种路径匹配器
  • PATH_PATTERN_PARSER是一种更复杂的匹配策略,它支持更多的条件匹配
ant_path_matcher
//通配符可以在中间,如:abc/**/xyz

path_pattern_parser
//通配符只能定义在尾部,如:abc/xyz/**
//可以使用{*path}接收多级路由。path可以随意取名,与@PathVariable名称对应即可
  • 定义{*path},可以获取多级路径,没有{**path}这种写法
//访问/pass/11/22/33 输出 pass#/11/22/33
@RequestMapping("/pass/{*path}")
public String pass(@PathVariable String path) {
    System.out.println("pass#" + path);
    return "pass#" + path;
}
  • /*可以接收一级路径,两者都可以
//访问/pass/11 输出 pass#/pass/11/
@RequestMapping("/pass/*")
public String pass(HttpServletRequest request) {
    System.out.println("pass#" + request.getServletPath());
    return "pass#" + request.getServletPath();
}
  • /**可以接收多级路径,两者都可以
//访问/pass/11/22/33 输出 pass#/pass/11/22/33
@RequestMapping("/pass/**")
public String pass(HttpServletRequest request) {
    System.out.println("pass#" + request.getServletPath());
    return "pass#" + request.getServletPath();
}

# 常用注解的使用

# @Primary

讨论Spring的@Primary注解,该注解是框架在3.0版中引入的
其作用与功能,当有多个相同类型的bean时,使用@Primary来赋予bean更高的优先级

# 为什么需要@Primary?

在某些情况下,需要注册多个相同类型的bean。如下有Employee类型的zhangSanEmployee和liSiEmployee:

@Configuration
public class PrimaryConfig {
    @Bean
    public Employee zhangSanEmployee() {
        return new Employee("张三");
    }

    @Bean
    public Employee liSiEmployee() {
        return new Employee("李四");
    }
}

运行应用程序,与@Autowired一起应用于注入。Spring会抛出NoUniqueBeanDefinitionException
要访问相同类型的bean,常使用@Qualifier(“beanName”)注解,通过别名控制访问相同类型

@Configuration
public class PrimaryConfig {

   @Bean
   @Qualifier("zhangSanEmployee")
   public Employee zhangSanEmployee() {
       return new Employee("张三");
   }

   @Bean
   @Qualifier("liSiEmployee")
   public Employee liSiEmployee() {
       return new Employee("李四");
   }
}

# 将@Primary和@Bean一起使用

@Configuration
public class PrimaryConfig {

    @Bean
    public Employee zhangSanEmployee() {
        return new Employee("张三");
    }

    @Bean
    @Primary
    public Employee liSiEmployee() {
        return new Employee("李四");
    }
}

用@Primary标记liSiEmployee()bean。 Spring将优先于zhangSanEmployee()注入liSiEmployee()

@Test
public void test1() {
    AnnotationConfigApplicationContext context
        = new AnnotationConfigApplicationContext(PrimaryConfig.class);

    Employee employee = context.getBean(Employee.class);
    System.out.println(employee);//Employee(name=李四)

}

# 将@Primary与@Component一起使用

有一个Manager接口和两个子类bean

public interface Manager {
    String getManagerName();
}

//覆盖Manager接口的getManagerName()
@Component
public class DepartmentManager implements Manager {
    @Override
    public String getManagerName() {
        return "Department manager";
    }
}

//覆盖Manager接口的getManagerName()
//用@Primary标记
@Component
@Primary
public class GeneralManager implements Manager {
    @Override
    public String getManagerName() {
        return "General manager";
    }
}

//测试
@RunWith(SpringRunner.class)
@SpringBootTest
public class PrimaryTest {
    @Resource
    private ApplicationContext context;
    @Test
    public void test2() {
        ManagerService service = context.getBean(ManagerService.class);
        Manager manager = service.getManager();
        System.out.println(manager.getManagerName());//General manager
    }
}

# @Import三种用法

  • @Import一个普通类:spring会将该类加载到spring容器中,被导入的bean无需使用注解声明为bean







 

 





//没有任何注解
public class MyClass {
	public void test() {
		System.out.println("test方法");
	}
}

//此形式可以有效的降低源代码与Spring技术的耦合度
@Import(MyClass.class)
//导入配置类时,配置类上可以不用加@Configuration,可以将其内部定义的Bean注册到容器中
@Import(Dbconfig.class)
public class ImportConfig {
	
}
  • @Import一个类:该类实现了ImportSelector接口,重写selectImports方法并返回String[]数组的对象,数组中的类都会注入到spring容器当中
public class MyImportSelector implements ImportSelector{
	@Override
	public String[] selectImports(AnnotationMetadata metadata){
	  //ImportConfig类上是否有Import注解
	  boolean flag =metadata.hasAnnotation("org.springframework.context.annotation.Import");
	  if(flag){
	      return new String[l{"com.itheima.domain.Dog"};
	  }
	  return new String[]{"com.itheima.domain.Cat"};
	}
}

//import该类
@Import(MyImportSelector.class)
public class ImportConfig {
	
}
  • @Import一个类:该类实现了ImportBeanDefinitionRegistrar接口,在重写的registerBeanDefinitions方法里面,能拿到BeanDefinitionRegistry的注册器,能手工往beanDefinitionMap中注册 beanDefinition
public class MyClassRegistry {
	public void test() {
		System.out.println("MyClassRegistry test方法");
	}
}

public class MyImportRegistry implements ImportBeanDefinitionRegistrar{
	@Override
	public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, 
	                                    BeanDefinitionRegistry registry) {
											
		RootBeanDefinition bd = new RootBeanDefinition();
		bd.setBeanClass(MyClassRegistry.class);
		registry.registerBeanDefinition("myClassRegistry", bd);
	}
}

//import该类
@Import(MyImportRegistry.class)
public class ImportConfig {
	
}

# Enable注解

  • SpringBoot中提供了很多Enable开头的注解,这些注解都是用于动态启用某些功能的
  • 其底层原理是使用@Import注解导入一些配置类,实现Bean的动态加载

# EnableAutoConfiguration注解原理解析

跟踪@SpringBootApplication的源码,去了解它的加载流程 EnableAutoConfiguration @EnableAutoConfiguration注解内部使用@Import(AutoconfigurationImportselector.class)来加载配置类

# condition条件判断

Condition 是在Spring 4.0 增加的条件判断功能,借助这个功能可以实现选择性的创建 Bean

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Conditional {

	Class<? extends Condition>[] value();
}

我们会发现@Conditional()源码只有一个属性:Class< ? extends Condition >[] value()

  • 需要导入class
  • 因为是数组,可以导入多个class
  • 并且这些class需要是Condition 接口的子类

Condition 接口的源码

@FunctionalInterface
public interface Condition {
	boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata);
}

# 简单使用

如果我们导入jedis坐标,则可以创建 myuser 的bean,否则不可以创建

  • 创建myuser类
public class MyUser {
    private  String name;
}
  • 创建一个配置类,用来项目启动时 来实例化myUser类
@Configuration
public class UserConfig {
    @Bean
    public MyUser user(){
        return new MyUser();
    }
}
  • 然后修改启动类,打印myuser 实例化的code值
@SpringBootApplication
public class ConditionApplication {

    public static void main(String[] args) {
       //启动SpringBoot的应用,返回Spring的IOC容器
       ApplicationContext context = SpringApplication.run(ConditionApplication.class, args);

       Object user = context.getBean("user");
       System.out.println(user);

    }
}
  • 创建ClassCondition,实现condition接口
public class ClassCondition implements Condition {
    /**
     *
     * @param context 上下文对象。用于获取环境,IOC容器,ClassLoader对象
     * @param metadata 注解元对象。 可以用于获取注解定义的属性值
     * @return
     */
    @Override
    public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
        
        //1.需求: 导入Jedis坐标后创建Bean
        //思路:判断redis.clients.jedis.Jedis.class文件是否存在
        boolean flag = true;
        try {
            Class<?> cls = Class.forName("redis.clients.jedis.Jedis");
        } catch (ClassNotFoundException e) {
            flag = false;
        }
        return flag;

   }    
}
  • 在pom文件中引入redis依赖
 <dependency>
     <groupId>org.springframework.boot</groupId>
     <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
  • 在userconfig类中添加@conditional注解,引入ClassCondition 类
@Configuration
@Conditional(ClassCondition.class)
public class UserConfig {

    @Bean
    public MyUser user(){
        return new MyUser();
    }

}

# 自定义Condition

将类的判断定义为动态的。判断哪个字节码文件存在可以动态指定

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
// 可以实现与 @Conditional 注解相同的功能
@Conditional(ClassCondition.class)
// 定义ConditionOnClass 注解
public @interface ConditionOnClass {
   //添加属性,此处注意用数组,方便传输多个值
   String[] value();
}
  • 修改 userConfig 类
@Configuration
//@Conditional(ClassCondition.class)
@ConditionOnClass("redis.clients.jedis.Jedis")
public class UserConfig {

    @Bean
    public MyUser user(){
        return new MyUser();
    }

}
  • 修改 ClassCondition 类
public class ClassCondition implements Condition {
    /**
     *
     * @param context 上下文对象。用于获取环境,IOC容器,ClassLoader对象
     * @param metadata 注解元对象。 可以用于获取注解定义的属性值
     * @return
     */
    @Override
    public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
        
        //获取注解属性值  value
        Map<String, Object> map = metadata.getAnnotationAttributes(ConditionOnClass.class.getName());
        String[] value = (String[]) map.get("value");

        boolean flag = true;
        try {
            for (String className : value) {
                Class<?> cls = Class.forName(className);
            }
        } catch (ClassNotFoundException e) {
            flag = false;
        }
        return flag;
    }
}

# @Conditional注解的派生注解

@Component("tom")

@ConcitionOnClass(Mouse.class)
@ConcitionOnClass(name = "com.mysql.jdbc.Driver")
@ConcitionOnMissingClass("com.mysql.jdbc.Driver")

@ConditionalOnBean(Mouse.class)
@ConditionalOnBean(name = "com.itheima.bean.Mouse")

//@ConditionalOnNotWebApplication
@ConditionalOnWebApplication
public class Cat {
	
}


// prefix:配置的前缀
// name:配置的属性信息
// matchIfMissing = true:表示如果没有在application.properties设置该属性,则默认为条件符合
// havingValue:配置读取的属性值跟havingValue做比较,如果一样则返回true;否则返回false
// 如果返回值为false,则该configuration不生效;为true则生效

@ConditionalOnProperty(prefix = "swagger", name = "enabled", 
					   havingValue = "true", matchIfMissing = true)

# springboot是如何创建redisTemplate的

springboot为我们提供了很多condition的依赖,他们都放到了包:org.springframework.boot:spring-boot-autoconfigure:x.x.x,我们可以在此包中找到condition包

condition包

找到data包中redis包,可以看到RedisAutoConfiguration 类,在里面可以看到创建redisTemplate的bean

condition包

可以看到@ConditionalOnClass必须存在字节码文件RedisOperations.class,并且容器中不存在redisTemplate的时候,才能创建redisTemplate

# 切换内置web服务器

SpringBoot提供了4中内置服务器,默认使用tomcat作为内置服务器。我们可以很方便的进行切换

  • JettyWebServerFactoryCustomizer :用来配置jetty服务器
  • NettyWebServerFactoryCustomizer:用来配置netty服务器
  • TomcatWebServerFactoryCustomizer:用来配置tomcat服务器
  • UndertowWebServerFactoryCustomizer:用来配置Undertow服务器
  • EmbeddedWebServerFactoryCustomizerAutoConfiguration:用来根据条件配置不通的服务器

比如我们想使用jetty服务器替代tomcat服务器,可直接在项目中排除tomcat依赖,并引入jetty依赖即可

我们在依赖包:org.springframework.boot.autoconfigure 中找到web 包,里面有个embedded 包 内置web服务器

EmbeddedWebServerFactoryCustomizerAutoConfiguration 类:

@AutoConfiguration
@ConditionalOnNotWarDeployment
//标识当项目中配置了web环境的时候才能让这个类起作用
@ConditionalOnWebApplication
@EnableConfigurationProperties({ServerProperties.class})
public class EmbeddedWebServerFactoryCustomizerAutoConfiguration {
    public EmbeddedWebServerFactoryCustomizerAutoConfiguration() {
		
    }

    @Configuration(proxyBeanMethods = false)
    @ConditionalOnClass({HttpServer.class})
    public static class NettyWebServerFactoryCustomizerConfiguration {
        public NettyWebServerFactoryCustomizerConfiguration() {
        }

        @Bean
        public NettyWebServerFactoryCustomizer nettyWebServerFactoryCustomizer(
		                                                Environment environment, 
		                                                ServerProperties serverProperties) {
            return new NettyWebServerFactoryCustomizer(environment, serverProperties);
        }
    }

    @Configuration(proxyBeanMethods = false)
    @ConditionalOnClass({Undertow.class, SslClientAuthMode.class})
    public static class UndertowWebServerFactoryCustomizerConfiguration {
        public UndertowWebServerFactoryCustomizerConfiguration() {
        }

        @Bean
        public UndertowWebServerFactoryCustomizer undertowWebServerFactoryCustomizer(
		                                                      Environment environment, 
		                                              ServerProperties serverProperties) {
            return new UndertowWebServerFactoryCustomizer(environment, serverProperties);
        }
    }

    @Configuration(proxyBeanMethods = false)
    @ConditionalOnClass({Server.class, Loader.class, WebAppContext.class})
    public static class JettyWebServerFactoryCustomizerConfiguration {
        public JettyWebServerFactoryCustomizerConfiguration() {
        }

        @Bean
        public JettyWebServerFactoryCustomizer jettyWebServerFactoryCustomizer(
		                                                Environment environment, 
		                                         ServerProperties serverProperties) {
            return new JettyWebServerFactoryCustomizer(environment, serverProperties);
        }
    }

    @Configuration(proxyBeanMethods = false)
	//项目中必须存在tomcat.class 和UpgradeProtocol.class,才能去实例化tomcat服务
    @ConditionalOnClass({Tomcat.class, UpgradeProtocol.class})
    public static class TomcatWebServerFactoryCustomizerConfiguration {
        public TomcatWebServerFactoryCustomizerConfiguration() {
        }

        @Bean
        public TomcatWebServerFactoryCustomizer tomcatWebServerFactoryCustomizer(
		                                                  Environment environment, 
		                                          ServerProperties serverProperties) {
            return new TomcatWebServerFactoryCustomizer(environment, serverProperties);
        }
    }
}

# 自定义Starter

# ip-spring-boot-autoconfigure模块

  • 业务功能开发
public class IpCountService {
    private Map<String,Integer> ipCountMap = new HashMap<String,Integer>();

    @Autowired
    // 当前的request对象由当前的starter的工程提供自动装配
    private HttpServletRequest httpServletRequest;

    public void count(){
        // 每次调用当前操作,就记录当前访问的ip,然后累加访问次数
        // 1.获取当前操作的ip地址
        String ip = httpServletRequest.getRemoteAddr();
        System.out.println("----------------------------------" + ip);
        // 2.根据ip地址从map取值,并递增
        ipCountMap.put(ip,ipCountMap.get(ip)==null? 0+1 : ipCountMap.get(ip) + 1);
    }
	
	@Scheduled(cron = "0/5 * * * * ?")
	public void print(){
		System.out.println("           ip访问监控");
		System.out.println("+-----ip-address-----+---+");
		for (Map.Entry<String, Integer> entry : ipCountMap.entrySet()) {
			String key = entry.getKey();
			Integer value = entry.getValue();
			System.out.println(String.format("|%18s  |%5d  |",key,value));
		}
		System.out.println("+--------------------+---+");
	}
}
  • 自动配置类
@EnableScheduling
public class IpAutoCinfiguration {
    //使用@import注入bean也可以
    @Bean
    public IpCountService ipCountService(){
        return new IpCountService();
    }
}
  • 配置信息
# 配置文件位置:
# META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
com.ruoyi.system.api.factory.RemoteUserFallbackFactory
com.ruoyi.system.api.factory.RemoteLogFallbackFactory
com.ruoyi.system.api.factory.RemoteFileFallbackFactory
com.ruoyi.system.api.factory.RemoteSeedsFallbackFactory

注意

  • 从spring boot2.7开始,不再支持META-INF/spring.factories文件以及下面的配置方式:
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
cn.itcast.autoconfig.IpAutoCinfigur
  • 可以通过配置文件变更自动配置
spring:
  autoconfigure:
    exclude: 
	  - com.ruoyi.system.api.factory.RemoteFileFallbackFactory
  • 也可以通过注解调整
@EnableAutoConfiguration(excludeName="", exclude={})
//或者 继承自上面这个注解的
@SpringBootApplication(excludeName="", exclude={})
  • 安装到maven仓库:使用前切记先clean后再install到maven仓库,确保资源更新

# 定义ip-spring-boot-starter

  • 简单调用
@Autowired
//注入ipCountService
private IpCountService ipCountService;
@GetMapping("/{currentPage}/{pageSize}")
public R getPage(@PathVariable int currentPage,@PathVariable int pageSize,Book book){
    //调用
	ipCountService.count();

	IPage<Book> page = bookService.getPage(currentPage, pageSize,book);
	if (currentPage > page.getPages()){
		page = bookService.getPage((int)page.getPages(), pageSize,book);
	}
	return new R(true,page);
}
  • 拦截器定义
public class IpCountInterceptor implements HandlerInterceptor {
    @Autowired
    private IpCountService ipCountService;
    @Override
    public boolean preHandle(HttpServletRequest request, 
	                         HttpServletResponse response, 
							 Object handler) throws Exception {
								 
        ipCountService.count();
        return true;
    }
}
  • 设置核心配置类加载拦截器
@Configuration
public class SpringMvcConfig implements WebMvcConfigurer {
    @Bean
    public IpCountInterceptor ipCountInterceptor(){
        return new IpCountInterceptor();
    }

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(ipCountInterceptor()).addPathPatterns("/**");
    }
}
  • 在启动类上加上拦截器
@EnableScheduling

@Import({IpCountInterceptor.class, SpringMvcConfig.class})
public class IpAutoCinfiguration {

    @Bean
    public IpCountService ipCountService(){
        return new IpCountService();
    }
}

# 变更自动配置

# 开启yml提示功能

  • yml配置
tools:
  ip:
   cycle: 1
   cycle-reset: true
   model: "simple"
  • 配置类添加注释
@ConfigurationProperties(prefix = "tools.ip")
public class IpProperties {

    /**
     * 日志的显示周期
     */
    private Long cycle = 5L;
    /**
     * 是否周期内重置数据
     */
    private Boolean cycleReset = false;
    /**
     * 日志的输出模式  detail:详细模式,simple:极简模式
     */
    private String model = LogModel.DETAIL.value;


    public enum LogModel{
        DETAIL("detail"),
        SIMPLE("simple");
        private String value;
        LogModel(String value){
            this.value = value;
        }
        public String getValue(){
            return value;
        }
    }

    public Long getCycle() {
        return cycle;
    }

    public void setCycle(Long cycle) {
        this.cycle = cycle;
    }

    public Boolean getCycleReset() {
        return cycleReset;
    }

    public void setCycleReset(Boolean cycleReset) {
        this.cycleReset = cycleReset;
    }

    public String getModel() {
        return model;
    }

    public void setModel(String model) {
        this.model = model;
    }
}
  • 添加依赖坐标
<dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-configuration-processor</artifactId>
</dependency>

注意

用这个坐标生成spring-configuration-metadata,也就是加上这个坐标,然后clean-install,就会生成这个文件,把这个文件从target目录中找到并且提出来,放到我们的配置目录下,这个坐标就可以注释了,因为上线用不到

  • 枚举类型自定义提示功能
"hints": [
	{
	  "name": "tools.ip.model",
	  "values": [
		{
		  "value": "detail",
		  "description": "详细模式."
		},
		{
		  "value": "simple",
		  "description": "极简模式."
		}
	  ]
	}
]

# springboot监听机制

Springboot在项目启动时,会对几个监听器进行回调,我们可以实现这些监听器接口,在项目启动时完成一些操作

ApplicationRunner
CommandLineRunner
ApplicationContextInitializer
SpringApplicationRunListener
//可以将一些需要在项目启动后被执行的事情用它来执行。比如数据预热
@Component
public class MyApplicationRunner implements ApplicationRunner {
    @Override
	//项目运行时传入的args参数
    public void run(ApplicationArguments args) throws Exception {
        System.out.println("MyApplicationRunner... run");
        System.out.println(Arrays.asList(args.getSourceArgs()));
    }
}

//可以将一些需要在项目启动后被执行的事情用它来执行。比如数据预热,同ApplicationRunner
@Component
public class MyCommandLineRunner implements CommandLineRunner {
    @Override
	//项目运行时传入的args参数
    public void run(String... args) throws Exception {a
        System.out.println("MyCommandLineRunner ... run");
        System.out.println(Arrays.asList(args));
    }
}

//在图标打印后就会被执行,通常用于检测资源是否存在
public class MyApplicationContextInitializer implements ApplicationContextInitializer {
    @Override
    public void initialize(ConfigurableApplicationContext applicationContext) {
        System.out.println("MyApplicationContextInitializer...initialize");
    }
}

//在项目开头和启动完成都有日志输出,声明周期贯穿了整个启动过程,所以能做的事情就比较多了
public class MySpringApplicationRunListener implements SpringApplicationRunListener {
    public MySpringApplicationRunListener(SpringApplication application, String[] args) {
    }

    @Override
    public void starting(ConfigurableBootstrapContext bootstrapContext) {
        System.out.println("starting... 启动中");
    }

    @Override
    public void environmentPrepared(ConfigurableBootstrapContext bootstrapContext, 
	                                ConfigurableEnvironment environment) {
        System.out.println("environmentPrepared...环境对象开始准备");
    }

    @Override
    public void started(ConfigurableApplicationContext context, Duration timeTaken) {
        System.out.println("starting... 启动完成");
    }

    @Override
    public void ready(ConfigurableApplicationContext context, Duration timeTaken) {
        System.out.println("ready...准备");
    }
	
    @Override
    public void contextPrepared(ConfigurableApplicationContext context) {
        System.out.println("contextPrepared...上下文对象开始准备");
    }

    @Override
    public void contextLoaded(ConfigurableApplicationContext context) {
        System.out.println("contextLoaded...上下文对象开始加载");
    }
	
    @Override
    public void started(ConfigurableApplicationContext context) {
        System.out.println("started...上下文对象加载完成");
    }
	
    @Override
    public void running(ConfigurableApplicationContext context) {
        System.out.println("running...启动完成,开始运行");
    }

    @Override
    public void failed(ConfigurableApplicationContext context, Throwable exception) {
        System.out.println("failed...启动失败");
    }
}