Spring Boot 框架知识整理
Spring Boot 发展
Spring 框架是一个轻量级的企业级开发的一站式解决方案。
Spring 是一个轻量级的控制反转(IoC)和面向切面编程(AOP)的容器框架。
IoC (Inversion of Control) 控制反转
控制反转是一种通过描述(XML 或注解)并通过第三方去生产或获取待定对象的方式。在 Spring 中实现控制反转的是 IoC 容器,其实现方法是依赖注入(DI)。
AOP (Aspect Oriented Programming) 面向切面编程
为了解耦。AOP 可以让一组类共享相同的行为(OOP 只能通过继承类实现接口,AOP 弥补了 OOP 的不足)。
DI (Dependency Injection) 依赖注入
Spring 框架四大原则
- 使用 POJO (Plain Ordinary Java Object, Java 简单对象) 进行轻量级和最小侵入式开发
- 通过依赖注入和基于接口编程实现松耦合
- 通过 AOP 和默认习惯进行声明式编程
- 使用 AOP 和模板(template)减少模式化代码
下图为 Spring 框架的架构图:

Spring Boot 是对各种 Java Web 开发技术的整合。
下图展示了 Spring Boot 在整个 Spring 生态中的位置:

Spring Boot 初步使用
Spring Boot 项目创建
参考文章:https://blog.csdn.net/ght886/article/details/91367067
启动类样例:
@SpringBootApplication
public class RainfallSystemApplication {
public static void main(String[] args) {
SpringApplication.run(RainfallSystemApplication.class, args);
}
}
pom.xml 初始文件
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.4.1</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>org.nwpu</groupId>
<artifactId>rainfall-system</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>rainfall-system</name>
<description>Flood runoff forecasting system's backend for Spring Boot.</description>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
Spring Boot 注解和配置
注解 @SpringBootApplication
@SpringBootApplication 注解实际上封装了以下三个注解:
- @SpringBootConfiguration 配置类注解
- @EnableAutoConfiguration 启用自动配置注解
- @ComponentScan 组件扫描注解
关于注解 @ServletComponentScan 和 @ComponentScan:
参考文章:https://blog.csdn.net/m0_37739193/article/details/85097477
@ComponentScan(“{package name}”) 自动扫描包名下所有使用 @Service、@Component、@Repository 和 @Controller 的类,并注册为 @Bean 。
VO(View Object):视图对象,用于展示层,它的作用是把某个指定页面(或组件)的所有数据封装起来。
DTO(Data Transfer Object):数据传输对象,这个概念来源于J2EE的设计模式,原来的目的是为了EJB的分布式应用提供粗粒度的数据实体,以减少分布式调用的次数,从而提高分布式调用的性能和降低网络负载,但在这里,我泛指用于展示层与服务层之间的数据传输对象。
DO(Domain Object):领域对象,就是从现实世界中抽象出来的有形或无形的业务实体。
PO(Persistent Object):持久化对象,它跟持久层(通常是关系型数据库)的数据结构形成一一对应的映射关系,如果持久层是关系型数据库,那么,数据表中的每个字段(或若干个)就对应PO的一个(或若干个)属性。
Spring MVC 视图解析器相关配置
新增 JSP 和 JSTL 的 Maven 依赖配置 -
pom.xml
<dependency> <groupId>org.apache.tomcat.embed</groupId> <artifactId>tomcat-embed-jasper</artifactId> <scope>provided</scope> </dependency> <dependency> <groupId>javax.servlet</groupId> <artifactId>jstl</artifactId> <scope>provided</scope> </dependency>
定义视图前后缀配置 -
application.properties
server.port =8090 spring.mvc.view.prefix=/WEB-INF/jsp/ spring.mvc.view.suffix=.jsp
新建控制器
IndexController.java
这里定义了一个映射为 /index 的路径,然后方法返回了“index”,这样它就与之前配置的前缀和后缀结合起来寻找对应的 jsp 文件,为此还需要开发一个对应的
index.jsp
文件。package com.springboot.chapter2.main; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; @Controller public class IndexController { @RequestMapping("/index") public String index() { return "index"; } }
自定义注解 @interface 和 @AliasFor 使用
@interface 参考文章:Java 注释 @interface 的用法
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(excludeFilters = { @Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),
@Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) })
public @interface SpringBootApplication {
@AliasFor(annotation = EnableAutoConfiguration.class)
Class<?>[] exclude() default {};
@AliasFor(annotation = EnableAutoConfiguration.class)
String[] excludeName() default {};
@AliasFor(annotation = ComponentScan.class, attribute = "basePackages")
String[] scanBasePackages() default {};
@AliasFor(annotation = ComponentScan.class, attribute = "nameGenerator")
Class<? extends BeanNameGenerator> nameGenerator() default BeanNameGenerator.class;
@AliasFor(annotation = Configuration.class)
boolean proxyBeanMethods() default true;
}
@AliasFor 注解的几种使用方式
1. 在同一个注解中显示使用,将注解中的多个属性互相设置别名
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Mapping
public @interface RequestMapping {
@AliasFor("path")
String[] value() default {};
@AliasFor("value")
String[] path() default {};
//...
}
为什么要给 value 属性和 path 属相互相设置别名也是有原因的。在 Spring 中给 value 属性设置值是可以省略属性的,比如可以写成:
RequestMapping("/foo")
这样写比较简洁,但是这样可读性不高,并不知道 value 属性代表什么意思。如果给这个属相设置一个 path 别名的话我们就知道这个是在设置路径。
但是要注意一点,@AliasFor 标签有一些使用限制:
- 互为别名的属性属性值类型,默认值,都是相同的;
- 互为别名的注解必须成对出现,比如 value 属性添加了 @AliasFor(“path”),那么 path 属性就必须添加 @AliasFor(“value”);
- 互为别名的属性必须定义默认值。
如果违反了别名的定义,在使用过程中就会报错。
2. 给元注解中的属性设定别名
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(excludeFilters = { @Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),
@Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) })
public @interface SpringBootApplication {
@AliasFor(annotation = EnableAutoConfiguration.class)
Class<?>[] exclude() default {};
@AliasFor(annotation = EnableAutoConfiguration.class)
String[] excludeName() default {};
@AliasFor(annotation = ComponentScan.class, attribute = "basePackages")
String[] scanBasePackages() default {};
//...
}
@SpringBootApplication 这个注解是由其他几个注解组合而成的。下面的代码就是在给 @ComponentScan 注解的 basePackages 属性设置别名 scanBasePackages。如果不设置 attribute 属性的话就是在给元注解的同名属性设置别名。
@AliasFor(annotation = ComponentScan.class, attribute = "basePackages")
String[] scanBasePackages() default {};
这种使用方式的好处是可以将几个注解的功能组合成一个新的注解。
@AliasFor 的实现代码
@AliasFor 的具体实现在 AnnotationUtils.findAnnotation 中。
依赖注入和 @Autowired
依赖注入(Dependency Injection, DI)即为定义 Bean 之间的依赖。
@Autowired 可以对类成员变量、方法及构造函数进行标注,完成自动装配的工作。 通过 @Autowired 的使用来消除 set, get 方法。在使用 @Autowired 之前,我们这样对一个 Bean 配置属性:
<property name="属性名" value=" 属性值"/>
通过这种方式来,配置比较繁琐,而且代码比较多。在 Spring 2.5 引入了 @Autowired 注释。下面用案例来具体说明:
UserRepository.java
package com.proc.bean.repository;
public interface UserRepository {
void save();
}
这里定义了一个 UserRepository 接口,其中定义了一个 save() 方法。
UserRepositoryImps.java
package com.proc.bean.repository;
import org.springframework.stereotype.Repository;
@Repository("userRepository")
public class UserRepositoryImps implements UserRepository{
@Override
public void save() {
System.out.println("UserRepositoryImps save");
}
}
定义一个 UserRepository 接口的实现类,并实现 save() 方法,在这里指定了该 Bean 在 IoC 中标识符名称为 userRepository。
UserService.java
package com.proc.bean.service;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import com.proc.bean.repository.UserRepository;
@Service
public class UserService {
@Autowired
private UserRepository userRepository;
public void save() {
userRepository.save();
}
}
这里需要一个 UserRepository 类型的属性,通过 @Autowired 自动装配方式,从 IoC 容器中去查找到,并返回给该属性。
applicationContext.xml
<context:component-scan base-package="com.proc.bean" />
测试代码:
ApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml");
UserService userService = (UserService) ctx.getBean("userService");
userService.save();
输出结果:UserRepositoryImps save
在使用 @Autowired 时,首先在容器中查询对应类型的 Bean
- 如果查询结果刚好为一个,就将该 Bean 装配给 @Autowired 指定的数据
- 如果查询的结果不止一个,那么 @Autowired 会根据名称来查找
- 如果查询的结果为空,那么会抛出异常。解决方法时,使用 required=false
举例说明,在上面例子中,我们再定一个类来实现 UserRepository.java
接口
package com.proc.bean.repository;
import org.springframework.stereotype.Repository;
@Repository
public class UserJdbcImps implements UserRepository {
@Override
public void save() {
System.out.println("UserJdbcImps save");
}
}
这时在启动容器后,在容器中有两个 UserRepository 类型的实例,一个名称为 userRepository,另一个为 userJdbcImps。
输出结果:UserRepositoryImps save。这里由于查询到有两个该类型的实例,那么采用名称匹配方式,在容器中查找名称为 userRepository 的实例,并自动装配给该参数。
如果这里想要装载 userJdbcImps 的实例,除了将字段 userRepository 名称改成 userJdbcImps 外,还可以使用 @Qualifier 标记,来指定需要装配 Bean 的名称,代码这样写:
@Autowired
@Qualifier("userJdbcImps")
private UserRepository userRepository;
输出结果:UserJdbcImps save
也可以在
UserJdbcImps.java
实现类中添加 @Primary 注解,表示当发现有多个同样类型的 Bean 时,优先使用此实现类进行注入。
约定编程——Spring AOP
Spring AOP (Aspect Oriented Programming, 面向切面编程) 是一种约定流程的编程。按照一定的规则,就可以将代码织入事先约定的流程中。AOP 最为典型的实际应用就是数据库事务的管理。
Spring AOP 可以处理一些无法使用 OOP 实现的业务逻辑。其次,通过约定,可以将一些业务逻辑织入流程中,并且可以将一些通用的逻辑抽取出来,然后给予默认实现,这样只需要完成部分的功能,使得开发者的代码更加简短,同时可维护性也得到提高。
AOP 术语和流程
- 连接点(join point):对应的是具体被拦截的对象,因为 Spring 只能支持方法,所以被拦截的对象往往就是指特定的方法,AOP 将通过动态代理技术把它织入对应的流程中。
- 切点(point cut):有时候,我们的切面不单单应用于单个方法,也可能是多个类的不同方法,这时,可以通过正则式和指示器的规则去定义,从而适配连接点。切点就是提供这样一个功能的概念。
- 通知(advice):就是按照约定的流程下的方法,分为前置通知(before advice)、后置通知(after advice)、环绕通知(around advice)、事后返回通知(afterReturning advice)和异常通知(afterThrowing advice),它会根据约定织入流程中,需要弄明白它们在流程中的顺序和运行的条件。
- 目标对象(target):即被代理对象。
- 引入(introduction):是指引入新的类和其方法,增强现有 Bean 的功能。
- 织入(weaving):它是一个通过动态代理技术,为原有服务对象生成代理对象,然后将与切点定义匹配的连接点拦截,并按约定将各类通知织入约定流程的过程。
- 切面(aspect):是一个可以定义切点、各类通知和引入的内容,Spring AOP 将通过它的信息来增强 Bean 的功能或者将对应的方法织入流程。

使用 AOP 开发
确定连接点
UserServiceImpl.java
package com.springboot.chapter4.aspect.service.impl;
/**** imports ****/
@Service
public class UserServiceImpl implements UserService {
@Override
public void printUser(User user) {
if (user == null) {
throw new RuntimeException("检查用户参数是否为空......");
}
System.out.print("id = " + user.getId());
System.out.print("\tusername = " + user.getUsername());
System.out.println("\tnote = " + user.getNote());
}
}
切点定义
MyAspect.java
package com.springboot.chapter4.aspect;
/**** imports ****/
@Aspect
public class MyAspect {
@Pointcut("execution(*com.springboot.chapter4.aspect.service.impl.UserServiceImpl.printUser(..))")
public void pointCut() {
}
@Before("pointCut()")
public void before() {
System.out.println("before......");
}
@After("pointCut()")
public void after() {
System.out.println("after......");
}
@AfterReturning("pointCut()")
public void afterReturning() {
System.out.println("afterReturning......");
}
@AfterThrowing("pointCut()")
public void afterThrowing() {
System.out.println("afterThrowing......");
}
}
测试 AOP
UserController.java
package com.springboot.chapter4.aspect.controller;
/**** imports ****/
// 定义控制器
@Controller
// 定义类请求路径
@RequestMapping("/user")
public class UserController {
// 注入用户服务
@Autowired
private UserService userService = null;
// 定义请求
@RequestMapping("/print")
// 转换为 JSON
@ResponseBody
public User printUser(Long id, String userName, String note) {
User user = new User();
user.setId(id);
user.setUsername(userName);
user.setNote(note);
userService.printUser(user); // 若 user = nul1,则执行 afterThrowing 方法
return user;// 加入断点
}
}
Chapter4Application.java
package com.springboot.chapter4.main;
/**** imports ****/
// 指定扫描包
@SpringBootApplication(scanBasePackages = {"com.springboot.chapter4.aspect"})
public class Chapter4Application {
// 定义切面
@Bean(name = "myAspect")
public MyAspect initMyAspect() {
return new MyAspect();
}
// 启动切面
public static void main(String[]args) {
SpringApplication.run(Chapter4Application.class, args);
}
}
打印日志:
before……
id = 1 username = user_name_1 note = 2323
after……
afterReturning……
环绕通知
MyAspect.java
加入环绕通知:
@Around("pointCut()")
public void around(ProceedingJoinPoint jp) throws Throwable {
System.out.println("around before......");
// 回调目标对象的原有方法
jp.proceed();
System.out.println("around after......");
}
多个切面
创建切面实例
定义多个切面类后,在启动类中创建实例:
// 指定扫描包
@SpringBootApplication(scanBasePackages = {"com.springboot.chapter4.aspect"})
public class Chapter4Application {
// 定义切面
@Bean(name = "myAspect2")
public MyAspect2 initMyAspect2() {
return new MyAspect2();
}
// 定义切面
@Bean(name = "myAspect1")
public MyAspect1 initMyAspect1() {
return new MyAspect1();
}
//定义切面
@Bean(name = "myAspect3")
public MyAspect3 initMyAspect3() {
return new MyAspect3();
}
// 启动 Spring Boot
public static void main(String[]args) {
SpringApplication.run(Chapter4Application.class,args);
}
}
指定多个切面的顺序
@Aspect
@Order(1) // 数字越小,优先级越高
public class MyAspect1 {
...
}
访问数据库
MyBatis 框架
MyBatis 的官方定义为:MyBatis 是支持定制化 SQL、存储过程以及高级映射的优秀的持久层框架。MyBatis 避免了几乎所有的 JDBC 代码和手动设置参数以及获取结果集。MyBatis 可以对配置和原生 Map 使用简单的 XML 或注解,将接口和 Java 的 POJO (Plain Old Java Object, 普通的Java对象) 映射成数据库中的记录。
MyBatis 的配置文件包括两个大的部分,一是基础配置文件,一个是映射文件。在 MyBatis 中也可以使用注解来实现映射,只是由于功能和可读性的限制,在实际的企业中使用得比较少。MyBatis 社区为了整合 Spring 自己开发了相应的开发包,因此在 Spring Boot 中,我们可以依赖 MyBatis 社区提供的 starter。例如,在Maven中加入依赖的包,如代码所示:
<!-- 引入关于 MyBatis 的 starter -->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>1.3.1</version>
</dependency>
详见本人博客:“MyBatis 学习笔记”。
数据库事务处理
执行 SQL 事务流程

Spring 数据库事务约定

当 Spring 的上下文开始调用被 @Transactional 标注的类或者方法时,Spring 就会产生 AOP 的功能。请注意事务的底层需要启用 AOP 功能,这是 Spring 事务的底层实现,后面我们会看到一些陷阱。那么当它启动事务时,就会根据事务定义器内的配置去设置事务,首先是根据传播行为去确定事务的策略,然后是隔离级别、超时时间、只读等内容的设置,只是这步设置事务并不需要开发者完成,而是 Spring 事务拦截器根据 @Transactional 配置的内容来完成的。
在上述场景中,Spring 通过对注解 @Transactional 属性配置去设置数据库事务,跟着 Spring 就会开始调用开发者编写的业务代码。执行开发者的业务代码,可能发生异常,也可能不发生异常。在 Spring 数据库事务的流程中,它会根据是否发生异常采取不同的策略。
如果都没有发生异常,Spring 数据库拦截器就会帮助我们提交事务,这点也并不需要我们干预。如果发生异常,就要判断一次事务定义器内的配置,如果事务定义器已经约定了该类型的异常不回滚事务就提交事务,如果没有任何配置或者不是配置不回滚事务的异常,则会回滚事务,并且将异常抛出,这步也是由事务拦截器完成的。
无论发生异常与否,Spring 都会释放事务资源,这样就可以保证数据库连接池正常可用了,这也是由 Spring 事务拦截器完成的内容。
从流程中我们可以看到开发者在整个流程中只需要完成业务逻辑即可,其他的使用 Spring 事务机制和其配置即可,这样就可以把 try… catch… finally…、数据库连接管理和事务提交回滚的代码交由 Spring 拦截器完成,而只需要完成业务代码即可,所以经常看到如下所示的简洁代码。
// ......
public class UserServiceImpl implements UserService {
@Autowired
private UserDao userDao = null;
@Override
@Transactional
public int insertUser(User user) {
return userDao.insertUser(user);
}
// ......
}
隔离级别
- 未提交读(read uncommitted),允许一个事务读取另外一个事务没有提交的数据。优点在于并发能力高,最大坏处是出现脏读。实际应用中采用的不多。
- 读写提交(read committed),指一个事务只能读取另外一个事务已经提交的数据,不能读取未提交的数据。不足之处为出现不可重复读。
- 可重复读(repeatable read),目标是克服读写提交中出现的不可重复读现象。不足之处为出现幻读(指的是一个事务在前后两次查询同一个范围的时候,后一次查询看到了前一次查询没有看到的数据行)。
- 串行化(serializable),要求所有的 SQL 都会按顺序执行。

对于 Oracle 默认的隔离级别为读写提交, MySQL 则是可重复读。
传播行为
在 Spring 中,当一个方法调用另外一个方法时,可以让事务采取不同的策略工作,如新建事务或者挂起当前事务等,这便是事务的传播行为。
传播行为 | 含义 |
---|---|
PROPAGATION_REQUIRED | 表示当前方法必须运行在事务中。如果当前事务存在,方法将会在该事务中运行。否则,会启动一个新的事务 |
PROPAGATION SUPPORTS | 表示当前方法不需要事务上下文,但是如果存在当前事务的话,那么该方法会在这个事务中运行 |
PROPAGATION_MANDATORY | 表示该方法必须在事务中运行,如果当前事务不存在,则会抛出一个异常 |
PROPAGATION_REQUIRED_NEW | 表示当前方法必须运行在它自己的事务中。一个新的事务将被启动。如果存在当前事务,在该方法执行期间,当前事务会被挂起。如果使用 JTATransactionManager 的话,则需要访问 TransactionManager |
PROPAGATION_NOT_SUPPORTED | 表示该方法不应该运行在事务中。如果存在当前事务,在该方法运行期间,当前事务将被挂起。如果使用JTATransactionManager 的话,则需要访问 TransactionManager |
PROPAGATON_NEVER | 表示当前方法不应该运行在事务上下文中。如果当前正有一个事务在运行,则会抛出异常 |
PROPAGATON_NIESTED | 表示如果当前已经存在一个事务,那么该方法将会在嵌套事务中运行。嵌套的事务可以独立于当前事务进行单独地提交或回流。如果当前事务不存在,那么其行为与 PROPAGATION_REQUIRED 一样。注意各厂商对这种传播行为的支持是有所差异的。可以参考资源管理器的文档来确认它们是否支持嵌套事务 |
测试 REQUIRED、REQUIRES_NEW 和 NESTED 3种常用传播行为
- REQUIRED
内外方法同一个事务。内部抛异常,外部回滚。外部抛异常,内部回滚。 - REQUIRES_NEW
内外方法不是同一个事务。内部抛异常,外部不回滚。外部抛异常,内部不回滚。 - NESTED
内外方法是嵌套事务。内部抛异常,外部不回滚。外部抛异常,内部回滚。
以 REQUIRED 举例:
UserBatchServiceImpl.java
package com.springboot.chapter6.service.impl;
/**** imports ****/
@Service
public class UserBatchServiceImpl implements UserBatchService {
@Autowired
private UserService userService = null;
@Override
@Transactional(isolation = Isolation.READ_COMMITTED, propagation = Propagation.REQUIRED)
public int insertUsers(List<User> userList) {
int count = 0;
for(User user : userList) {
// 调用子方法,将使用 @Transactional 定义的传播行为
count += userService.insertUser(user);
}
return count;
}
}
UserController.java
@Autowired
private UserBatchService userBatchService = null;
@RequestMapping("/insertUsers")
@ResponseBody
public Map<String, Object> insertUsers(String userName1, String note1, String userName2, String note2) {
User user1 = new User();
user1.setUserName(userName1);
user1.setNote(note1);
User user2 = new User();
user2.setUserName(userName2);
user2.setNote(note2);
List<User> userList = new ArrayList<>();
userList.add(user1);
userList.add(user2);
// 结果会回填主键,返回插入条数
int inserts = userBatchService.insertUsers(userList);
Map<String, Object> result = new HashMap<>();
result.put("success", inserts > 0);
result.put("user", userList);
return result;
}
Participating in existing transaction
Creating a new SqlSession
……
Releasing transactional SqlSession […]
Participating in existing transaction
Creating a new SqlSession
……
Releasing transactional SqlSession […]
接着修改 insertUser
方法,分别测试 REQUIRES_NEW 和 NESTED,输出日志不过多展示。
@Override
@Transactional(isolation = Isolation.READ_COMMITTED, propagation = Propagation.REQUIRES_NEW)
public int insertUser(User user) {
return userDao.insertUser(user);
}
/**********/
@Override
@Transactional(isolation = Isolation.READ_COMMITTED, propagation = Propagation.NESTED)
public int insertUser(User user) {
return userDao.insertUser(user);
}
使用 Redis
Redis 是一种运行在内存的数据库,支持 7 种数据类型的存储。Redis 是一个开源、使用 ANSIC 语言编写、遵守 BSD 协议、支持网络、可基于内存亦可持久化的日志型、键值数据库,并提供多种语言的 API。Redis 是基于内存的,所以运行速度很快,大约是关系数据库几倍到几十倍的速度。在测试中,Redis 可以在 1s 内完成 10 万次的读写,性能十分高效。如果我们将常用的数据存储在 Redis 中,用来代替关系数据库的查询访问,网站性能将可以得到大幅提高。
RedisTemplate
创建 RedisTemplate
@Bean(name = redisTemplate)
public RedisTemplate<Object, Object> initRedisTemplate() {
RedisTemplate<Object, Object> redisTemplate = new RedisTemplate<>();
// RedisTemplate 会自动初始化 StringRedisSerializer,所以这里直接获取
RedisSerializer stringRedisSerializer = redisTemplate.getStringSerializer();
// 设置字符串序列化器,这样 Spring 就会把 Redis 的 key 当作字符串处理了
redisTemplate.setKeySerializer(stringRedisSerializer);
redisTemplate.setHashKeySerializer(stringRedisSerializer);
redisTemplate.setHashValueSerializer(stringRedisSerializer);
redisTemplate.setConnectionFactory(initConnectionFactory());
return redisTemplate;
}
使用 SessionCallBack 接口
// 让 RedisTemplate 回调,在同一连接下执行多个 Redis 命令
public void useSessionCallback(RedisTemplate redisTemplate) {
redisTemplate.execute(new SessionCallback() {
@Override
public Object execute(RedisOperation ro) throws DataAccessException {
ro.opsForValue().set("key1", "value1");
ro.opsForHash().put("hash", "field", "hvalue");
return null;
}
});
}
// 也可以使用 Lambda 表达式改写代码
public void useSessionCallback(RedisTemplate redisTemplate) {
redisTemplate.execute((RedisOperation ro) -> {
ro.opsForValue().set("key1", "value1");
ro.opsForHash().put("hash", "field", "hvalue");
return null;
});
}
在 Spring Boot 中配置和使用 Redis
配置文件 application.yml
spring:
redis:
# 配置连接池属性
jedis:
pool:
min-idle: 5
max-active: 10
max-idle: 10
max-wait: 2000
# 配置 Redis 服务器属性
port: 6379
host: 192.168.11.131
password: 123456
# Redis 连接超时时间,单位主运秒
timeout: 1000
RedisTemplate 会默认使用 JdkSerializationRedisSerializer 进行序列化键值,这样便能够存储到 Redis 服务器中。Redis 服务器存入的便是一个经过序列化后的特殊字符串,有时候对于跟踪并不是很友好 。如果我们在 Redis 只是使用字符串 ,那么使用其自动生成的 StringRedisTemplate 即可,但是这样就只能支持字符串了,并不能支持 Java 对象的存储。为了克服这个问题,可以通过设置 RedisTemplate 的序列化器来处理。
自定义序列化(推荐使用 StringRedisTemplate)
为 Redis 客户端查看操作数据, redisTemplate 需要进行序列化设置, 默认配置的 jdk 序列化会导致在客户端查看不了数据(仍可使用内在函数存取修改, 只是查看不了), 为避免这种情况发生, 使用 StringRedisTemplate 或自行配置序列化, 自行配置可参考如下代码:
/**** imports ****/
@Configuration
public class MyRedisConfig {
@Bean(name = "redisTemplate")
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory){
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
////参照StringRedisTemplate内部实现指定序列化器
redisTemplate.setConnectionFactory(redisConnectionFactory);
redisTemplate.setKeySerializer(keySerializer());
redisTemplate.setHashKeySerializer(keySerializer());
redisTemplate.setValueSerializer(valueSerializer());
redisTemplate.setHashValueSerializer(valueSerializer());
return redisTemplate;
}
private RedisSerializer<String> keySerializer(){
return new StringRedisSerializer();
}
//使用Jackson序列化器
private RedisSerializer<Object> valueSerializer(){
return new GenericJackson2JsonRedisSerializer();
}
}
简单测试代码 test:
@RunWith(SpringRunner.class)
@SpringBootTest
public class MyConfigRedisTemplateTest {
@Autowired
// 在 MyRedisConfig 文件中配置了 redisTemplate 的序列化之后,客户端也能正确显示键值对了
private RedisTemplate redisTemplate;
@Test
public void test(){
redisTemplate.opsForValue().set("wujinxing", "lige");
System.out.println(redisTemplate.opsForValue().get("wujinxing"));
Map<String, Object> map = new HashMap<>();
for (int i=0; i<10; i++){
User user = new User();
user.setId(i);
user.setName(String.format("测试%d", i));
user.setAge(i+10);
map.put(String.valueOf(i),user);
}
redisTemplate.opsForHash().putAll("测试", map);
BoundHashOperations hashOps = redisTemplate.boundHashOps("测试");
Map map1 = hashOps.entries();
System.out.println(map1);
}
static class User implements Serializable {
private int id;
private String name;
private long age;
// 省略getter, setter, toString...
}
}
操作 Redis 数据类型(String, hash, set 等)
常见操作(均在 Controller 上使用, 仅做测试, 实际项目应在 Service 层使用 Redis):
/**** imports ****/
@Controller
@RequestMapping("/redis")
public class RedisController {
private static final Logger LOGGER = LoggerFactory.getLogger(RedisController.class);
@Autowired
private RedisTemplate redisTemplate = null ;
@Autowired
private StringRedisTemplate stringRedisTemplate = null ;
@RequestMapping("/stringAndHash")
@ResponseBody
public Map<String, Object> testStringAndHash() {
redisTemplate.opsForValue().set("key1", "value1");
// 注意这里使用了 JDK 的序列化器,所以 Redis 保存时不是整数,不能运算
redisTemplate.opsForValue().set("int_key", "1");
stringRedisTemplate.opsForValue().set("int", "1");
// 使用运算
stringRedisTemplate.opsForValue().increment("int", 1);
// 获取底层 Jedis 连接
Jedis jedis = (Jedis) stringRedisTemplate.getConnectionFactory().getConnection().getNativeConnection();
// 减1操作,这个命令 RedisTemplate 不支持,所以我先获取底层的连接再操作
jedis.decr("int");
Map<String, String> hash = new HashMap<String, String>();
hash.put("field1", "value1");
hash.put("field2", "value2");
// 存入一个散列数据类型
stringRedisTemplate.opsForHash().putAll("hash", hash);
// 新增一个字段
stringRedisTemplate.opsForHash().put("hash", "field3", "value3");
// 绑定散列操作的 key , 这样可 以连续对同一个散列数据类型进行操作
BoundHashOperations hashOps = stringRedisTemplate.boundHashOps("hash");
// 删除两个字段
hashOps.delete("field1", "field2");
// 新增一个字段
hashOps.put("filed4", "value5");
Map<String, Object> map = new HashMap<String, Object>();
map.put("success", true);
return map;
}
@RequestMapping("/list")
@ResponseBody
public Map<String, Object> testList(){
//链表从左到右的顺序为v10, v8, v6, v4, v2
stringRedisTemplate.opsForList().leftPushAll("list1", "v2","v4","v6","v8","v10");
//链表从左到右的顺序为v1, v3, v5, v7, v9
stringRedisTemplate.opsForList().rightPushAll("list2", "v1","v3","v5","v7","v9");
//绑定list2操作链表
BoundListOperations listOps = stringRedisTemplate.boundListOps("list2");
Object result1 = listOps.rightPop();//从右边弹出一个成员
LOGGER.info("list2的最右边元素为: "+result1.toString());
Object result2 = listOps.index(1); //获取定位元素, 下标从0开始
LOGGER.info("list2下标为1的元素为"+result2.toString());
listOps.leftPush("v0"); //从左边插入链表
Long size = listOps.size();//求链表长
LOGGER.info("list2的长度为: "+size);
List element = listOps.range(0, size-2); //求链表区间成员
LOGGER.info("list2从0到size-2的元素依次为: "+element.toString());
Map<String, Object> map = new HashMap<>();
map.put("success", true);
return map;
}
@RequestMapping("/set")
@ResponseBody
public Map<String, Object> testSet(){
//重复的元素不会被插入
stringRedisTemplate.opsForSet().add("set1", "v1","v1","v3","v5","v7","v9");
stringRedisTemplate.opsForSet().add("set2", "v2","v4","v6","v5","v10","v10");
//绑定sert1集合操作
BoundSetOperations setOps = stringRedisTemplate.boundSetOps("set1");
setOps.add("v11", "v13");
setOps.remove("v1", "v3");
Set set = setOps.members();//返回所有元素
LOGGER.info("集合中所有元素: "+set.toString());
Long size = setOps.size();//求成员数
LOGGER.info("集合长度: "+String.valueOf(size));
Set inner = setOps.intersect("set2"); //求交集
setOps.intersectAndStore("set2", "set1_set2");//求交集并用新的集合保存
LOGGER.info("集合的交集: "+inner.toString());
Set diff = setOps.diff("set2"); //求差集
setOps.diffAndStore("set2","set1-set2"); //求差集并用新的集合保存
LOGGER.info("集合的差集: "+diff.toString());
Set union = setOps.union("set2"); //求并集
setOps.unionAndStore("set2", "set1=set2"); //求并集并用新的集合保存
LOGGER.info("集合的并集: "+union.toString());
Map<String, Object> map = new HashMap<>();
map.put("success", true);
return map;
}
/**
* redis操作有序集合
* @return
*/
@RequestMapping("/zset")
@ResponseBody
public Map<String, Object> testZSet(){
Set<ZSetOperations.TypedTuple<String>> typedTupleSet = new HashSet<>();
for(int i=1; i<=9; i++){
//分数
double score = i*0.1;
//创建一个TypedTuple对象, 存入值和分数
ZSetOperations.TypedTuple typedTuple = new DefaultTypedTuple<String>("value" + i, score);
typedTupleSet.add(typedTuple);
}
LOGGER.info("新建的set: "+typedTupleSet.toString());
//往有序集合插入元素
stringRedisTemplate.opsForZSet().add("zset1", typedTupleSet);
//绑定zset1有序集合操作
BoundZSetOperations<String, String> zSetOps = stringRedisTemplate.boundZSetOps("zset1");
zSetOps.add("value10", 0.26);
Set<String> setRange = zSetOps.range(1,6);
LOGGER.info("下标下1-6的set: " + setRange.toString());
//按分数排序获取有序集合
Set<String> setScore = zSetOps.rangeByScore(0.2, 0.6);
LOGGER.info("按分数排序获取有序集合: "+ setScore.toString());
//定义值范围
RedisZSetCommands.Range range = new RedisZSetCommands.Range();
range.gt("value3"); //大于value3
//range.gte("value3"); //大于等于value3
//range.lt("value8"); //小于value8
range.lte("value8"); //小于等于value8
//按值排序, 注意这个排序是按字符串排序
Set<String> setLex = zSetOps.rangeByLex(range);
LOGGER.info("按值排序: "+setLex.toString());
zSetOps.remove("value9", "value2"); //删除元素
Double score = zSetOps.score("value8"); //求分数
LOGGER.info("求value8的分数: "+score);
//在下标区间 按分数排序, 同时返回value和score
Set<ZSetOperations.TypedTuple<String>> rangeSet = zSetOps.rangeWithScores(1,6);
LOGGER.info("在下标区间 按分数排序, 同时返回value和score: "+rangeSet.toString());
//在下标区间 按分数排序, 同时返回value和score
Set<ZSetOperations.TypedTuple<String>> scoreSet = zSetOps.rangeByScoreWithScores(1,6);
LOGGER.info("在下标区间 按分数排序, 同时返回value和score: "+scoreSet.toString());
//按从大到小排序
Set<String> reverseSet = zSetOps.reverseRange(2, 8);
LOGGER.info("按从大到小排序: "+reverseSet.toString());
Map<String, Object> map = new HashMap<>();
map.put("success", true);
return map;
}
@RequestMapping("/multi")
@ResponseBody
public Map<String, Object> testMulti(){
stringRedisTemplate.opsForValue().set("key1", "value1");
/*List list = (List) stringRedisTemplate.execute((RedisOperations operations)->{
operations.watch("key1");
operations.multi();
operations.opsForValue().set("key2", "value2");
//operations.opsForValue().increment("key1", 1);
//获取的值将为null, 因为redis知识把命令放入队列
Object value2 = operations.opsForValue().get("key2");
System.out.println("命令在队列, 所以value2为null [ " + value2 + " ] ");
operations.opsForValue().set("key3", "value3");
Object value3 = operations.opsForValue().get("key3");
System.out.println("命令在队列, 所以value3为null [ " + value3 + " ] ");
//执行exce()命令,将先判断key1是否在监控后被修改过, 如果是则不执行事务, 否则就执行事务
return operations.exec();
});
System.out.println(list);*/
Map<String, Object> map = new HashMap<>();
map.put("success", true);
return map;
}
}
Service 层使用 Redis
@Service
public class CityServiceImpl implements CityService {
private static final Logger LOGGER = LoggerFactory.getLogger(CityServiceImpl.class);
@Autowired
private CityMapper cityMapper;
@Autowired
private RedisTemplate redisTemplate;
/**
* 获取城市逻辑:
* 如果缓存存在,从缓存中获取城市信息
* 如果缓存不存在,从 DB 中获取城市信息,然后插入缓存
*/
@Override
public City findCityById(Long id){
//从缓存中获取城市信息
String key = "city_"+id;
ValueOperations<String,City> operations = redisTemplate.opsForValue();
//缓存存在
boolean hasKey = redisTemplate.hasKey(key);
if(hasKey){
City city = operations.get(key);
LOGGER.info("CityServiceImpl.findCityById() : 从缓存中获取了城市 >> " + city.toString());
return city;
}
//从DB中获取城市
City city = cityMapper.findById(id);
//插入缓存
operations.set(key,city,10,TimeUnit.SECONDS); //缓存的时间仅有十秒钟
LOGGER.info("CityServiceImpl.findCityById() : 城市插入缓存 >> " + city.toString());
LOGGER.info("刚才加入redis的数据是: "+operations.get(key));
return city;
}
@Override
public Long saveCity(City city) {
return cityMapper.saveCity(city);
}
/**
* 更新城市逻辑:
* 如果缓存存在,删除
* 如果缓存不存在,不操作
*/
@Override
public Long updateCity(City city) {
Long ret = cityMapper.updateCity(city);
//缓存存在,删除缓存
String key = "city_" + city.getId();
boolean hasKey = redisTemplate.hasKey(key);
if (hasKey){
redisTemplate.delete(key);
LOGGER.info("CityServiceImpl.updateCity() : 从缓存中删除城市 >> " + city.toString());
}
return ret;
}
@Override
public Long deleteCity(Long id) {
Long ret = cityMapper.deleteCity(id);
String key = "city_" + id;
boolean hasKey = redisTemplate.hasKey(key);
if(hasKey){
redisTemplate.delete(key);
LOGGER.info("CityServiceImpl.deleteCity() : 从缓存中删除城市 ID >> " + id);
}
return ret;
}
}
Spring Boot 20 道面试题
1、什么是 Spring Boot?
Spring Boot 是 Spring 开源组织下的子项目,是 Spring 组件一站式解决方案,主要是简化了使用 Spring 的难度,简省了繁重的配置,提供了各种启动器,开发者能快速上手。
更多 Spring Boot 详细介绍请看这篇文章《什么是Spring Boot?》。
2、为什么要用 Spring Boot?
Spring Boot 优点非常多,如:
- 独立运行
- 简化配置
- 自动配置
- 无代码生成和XML配置
- 应用监控
- 上手容易
- …
Spring Boot 集这么多优点于一身,还有理由不使用它呢?
3、Spring Boot 的核心配置文件有哪几个?它们的区别是什么?
Spring Boot 的核心配置文件是 application 和 bootstrap 配置文件。
application 配置文件这个容易理解,主要用于 Spring Boot 项目的自动化配置。
bootstrap 配置文件有以下几个应用场景。
- 使用 Spring Cloud Config 配置中心时,这时需要在 bootstrap 配置文件中添加连接到配置中心的配置属性来加载外部配置中心的配置信息;
- 一些固定的不能被覆盖的属性;
- 一些加密/解密的场景;
具体请看这篇文章《Spring Boot 核心配置文件详解》。
4、Spring Boot 的配置文件有哪几种格式?它们有什么区别?
.properties 和 .yml,它们的区别主要是书写格式不同。
1).properties
app.user.name = javastack
2).yml
app:
user:
name: javastack
另外,.yml 格式不支持 @PropertySource
注解导入配置。
5 1docker exec -it myredis bash2cd /usr/local/bin3./redis-cli4# or5docker exec -it myredis redis-clibash
启动类上面的注解是@SpringBootApplication,它也是 Spring Boot 的核心注解,主要组合包含了以下 3 个注解:
@SpringBootConfiguration:组合了 @Configuration 注解,实现配置文件的功能。
@EnableAutoConfiguration:打开自动配置的功能,也可以关闭某个自动配置的选项,如关闭数据源自动配置功能: @SpringBootApplication(exclude = { DataSourceAutoConfiguration.class })。
@ComponentScan:Spring组件扫描。
6、开启 Spring Boot 特性有哪几种方式?
1)继承spring-boot-starter-parent项目
2)导入spring-boot-dependencies项目依赖
具体请参考这篇文章《Spring Boot开启的2种方式》。
7、Spring Boot 需要独立的容器运行吗?
可以不需要,内置了 Tomcat/ Jetty 等容器。
8、运行 Spring Boot 有哪几种方式?
1)打包用命令或者放到容器中运行
2)用 Maven/ Gradle 插件运行
3)直接执行 main 方法运行
9、Spring Boot 自动配置原理是什么?
注解 @EnableAutoConfiguration, @Configuration, @ConditionalOnClass 就是自动配置的核心,首先它得是一个配置文件,其次根据类路径下是否有这个类去自动配置。
具体看这篇文章《Spring Boot自动配置原理、实战》。
10、Spring Boot 的目录结构是怎样的?
cn
+- javastack
+- MyApplication.java
|
+- customer
| +- Customer.java
| +- CustomerController.java
| +- CustomerService.java
| +- CustomerRepository.java
|
+- order
+- Order.java
+- OrderController.java
+- OrderService.java
+- OrderRepository.java
这个目录结构是主流及推荐的做法,而在主入口类上加上 @SpringBootApplication 注解来开启 Spring Boot 的各项能力,如自动配置、组件扫描等。具体看这篇文章《Spring Boot 主类及目录结构介绍》。
11、你如何理解 Spring Boot 中的 Starters?
Starters可以理解为启动器,它包含了一系列可以集成到应用里面的依赖包,你可以一站式集成 Spring 及其他技术,而不需要到处找示例代码和依赖包。如你想使用 Spring JPA 访问数据库,只要加入 spring-boot-starter-data-jpa 启动器依赖就能使用了。
Starters包含了许多项目中需要用到的依赖,它们能快速持续的运行,都是一系列得到支持的管理传递性依赖。具体请看这篇文章《Spring Boot Starters启动器》。
12、如何在 Spring Boot 启动的时候运行一些特定的代码?
可以实现接口 ApplicationRunner 或者 CommandLineRunner,这两个接口实现方式一样,它们都只提供了一个 run 方法,具体请看这篇文章《Spring Boot Runner启动器》。
13、Spring Boot 有哪几种读取配置的方式?
Spring Boot 可以通过 @PropertySource,@Value,@Environment, @ConfigurationProperties 来绑定变量,具体请看这篇文章《Spring Boot读取配置的几种方式》。
14、Spring Boot 支持哪些日志框架?推荐和默认的日志框架是哪个?
Spring Boot 支持 Java Util Logging, Log4j2, Lockback 作为日志框架,如果你使用 Starters 启动器,Spring Boot 将使用 Logback 作为默认日志框架,具体请看这篇文章《Spring Boot日志集成》。
15、SpringBoot 实现热部署有哪几种方式?
主要有两种方式:
- Spring Loaded
- Spring-boot-devtools
Spring-boot-devtools 使用方式可以参考这篇文章《Spring Boot实现热部署》。
16、你如何理解 Spring Boot 配置加载顺序?
在 Spring Boot 里面,可以使用以下几种方式来加载配置。
1)properties文件;
2)YAML文件;
3)系统环境变量;
4)命令行参数;
等等……
具体请看这篇文章《Spring Boot 配置加载顺序详解》。
17、Spring Boot 如何定义多套不同环境配置?
提供多套配置文件,如:
applcation.properties
application-dev.properties
application-test.properties
application-prod.properties
运行时指定具体的配置文件,具体请看这篇文章《Spring Boot Profile 不同环境配置》。
18、Spring Boot 可以兼容老 Spring 项目吗,如何做?
可以兼容,使用 @ImportResource
注解导入老 Spring 项目配置文件。
19、保护 Spring Boot 应用有哪些方法?
- 在生产中使用HTTPS
- 使用Snyk检查你的依赖关系
- 升级到最新版本
- 启用CSRF保护
- 使用内容安全策略防止XSS攻击
- …
更多请看这篇文章《10 种保护 Spring Boot 应用的绝佳方法》。
20、Spring Boot 2.X 有什么新特性?与 1.X 有什么区别?
- 配置变更
- JDK 版本升级
- 第三方类库升级
- 响应式 Spring 编程支持
- HTTP/2 支持
- 配置属性绑定
- 更多改进与加强…
具体请看这篇文章《Spring Boot 2.x 新特性总结及迁移指南》。