SpringBoot

SpringBoot

Spring Boot 的自动配置是如何实现的?

使用Springboot时,我们需要先引入对应starter,Springboot启动时会自动加载相关依赖,配置相应的初始化参数;

自动装配的流程:SpringBoot通过**@EnableAutoConfiguration注解开启自动配置**,Spring Boot会自动根据项目中的依赖配置相关的Bean,例如,如果项目中引入了spring-boot-starter-web依赖

它使用 Spring 框架提供的 @Import 注解通过AutoConfigurationImportSelector类(选择器)给容器中加载所有自动配置类的jar包的META-INF/spring.factorie文件下面指定的自动配置类

如果是条件配置,满足@Conditional注解的条件时,就实例化该AutoConfiguration类中定义的Bean,并注入Spring容器,即可完成Springboot的自动装配

比如spring.factories定义的AutoConfiguration类是MyConditionalAutoConfiguration

1
2
3
4
5
# 自动配置类
org.springframework.boot.autoconfigure.EnableAutoConfiguration=com.example.MyAutoConfiguration

# 自定义条件化自动配置
org.springframework.boot.autoconfigure.condition.ConditionalOnProperty=com.example.MyConditionalAutoConfiguration

什么是spring.factories?

通常将 spring.factories 文件放置在项目的 META-INF 目录下。在大多数情况下,这个目录位于 src/main/resources 目录下。

spring.factories 文件可以将 spring-boot 项目包以外的 bean(即在 pom 文件中添加依赖中的 bean)注册到 spring-boot 项目的 spring 容器。

由于@ComponentScan 注解只能扫描 spring-boot 项目包内的 bean 并注册到 spring 容器中,因此需要@EnableAutoConfiguration 注解来注册项目包外的bean。而 spring.factories 文件,则是用来记录项目包外需要注册的bean类名

@Configuration的作用是什么

使用 @Configuration 注解进行标记,然后在类上添加 @ConditionalOnProperty 注解可以指定条件。例如:

1
2
3
4
5
6
7
8
javaCopy codeimport org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Configuration;

@Configuration
@ConditionalOnProperty(prefix = "myapp", name = "enabled", havingValue = "true", matchIfMissing = true)
public class MyConditionalAutoConfiguration {
// 自动配置内容...
}

只有当配置文件中的 myapp.enabled 属性值为 true 时,才会加载这个自动配置类。

条件注解有哪些?

  1. **@ConditionalOnBean**:当容器中存在指定的 Bean 时,才创建当前的 Bean。
  2. **@ConditionalOnMissingBean**:当容器中不存在指定的 Bean 时,才创建当前的 Bean。
  3. **@ConditionalOnClass**:当类路径下存在指定的类时,才创建当前的 Bean。
  4. **@ConditionalOnMissingClass**:当类路径下不存在指定的类时,才创建当前的 Bean。
  5. **@ConditionalOnProperty**:当指定的属性值符合条件时,才创建当前的 Bean。可以指定属性的键和值。
  6. **@ConditionalOnResource**:当类路径下存在指定的资源文件时,才创建当前的 Bean。
  7. **@ConditionalOnWebApplication**:根据应用程序的类型来决定是否创建当前的 Bean。例如,可以指定只有当应用程序是 Web 应用程序时才创建。
  8. **@ConditionalOnNotWebApplication**:与 @ConditionalOnWebApplication 相反,当应用程序不是 Web 应用程序时才创建 Bean。
  9. **@ConditionalOnExpression**:当 SpEL 表达式为 true 时,才创建当前的 Bean。
  10. **@ConditionalOnJava**:当运行在特定版本的 Java 环境下时,才创建当前的 Bean。
  11. **@ConditionalOnSingleCandidate**:当容器中只有一个指定的 Bean,或者这些 Bean 可以被自动装配时,才创建当前的 Bean。
  12. **@ConditionalOnCloudPlatform**:当应用程序运行在特定的云平台上时,才创建当前的 Bean。
  13. **@ConditionalOnRibbon**:当 Ribbon 客户端存在时,才创建当前的 Bean。
  14. **@ConditionalOnEureka**:当 Eureka 客户端存在时,才创建当前的 Bean。
  15. **@ConditionalOnZuul**:当 Zuul 客户端存在时,才创建当前的 Bean。

@SpringBootApplication注解工作流程

以下是Spring Boot启动类注解的工作流程:

  1. 初始化SpringApplication:在Spring Boot应用启动时,首先会进行SpringApplication的初始化。这个过程包括设置源(即设置Spring容器启动时依赖的初始配置类),设置WEB应用程序类型(例如SERVLET或REACTIVE),以及配置一些基本的环境变量、资源、构造器和监听器。
  2. 加载配置环境:在初始化之后,Spring Boot会根据项目的配置(如application.properties或application.yml文件)加载配置环境。这些配置信息将被用来设置Spring应用的各种参数,如数据库连接、服务器端口等。
  3. 创建上下文环境:接着,Spring Boot会创建应用上下文环境。在这个过程中,Spring Boot会根据项目中定义的Beans和自动配置项来构建Spring容器。
  4. 自动化配置:Spring Boot的一个核心特性是自动化配置,它会根据项目的依赖和配置自动配置Spring应用。例如,如果项目中包含了spring-boot-starter-web依赖,Spring Boot会自动配置嵌入式的Tomcat服务器和Spring MVC相关的组件。
  5. 启动应用:最后,通过调用SpringApplication.run(SpringBoot.class, args)方法,Spring Boot会启动内嵌的Web服务器(默认为Tomcat),并开始监听指定的端口,等待接收请求。

Spring有哪些设计模式?

单例模式

在我们的系统中,有一些对象其实我们只需要一个,比如说:线程池、缓存、对话框、注册表、日志对象、充当打印机、显卡等设备驱动程序的对象。事实上,这一类对象只能有一个实例,如果制造出多个实例就可能会导致一些问题的产生,比如:程序的行为异常、资源使用过量、或者不一致性的结果。

使用单例模式的好处 :

  • 对于频繁使用的对象,可以省略创建对象所花费的时间,这对于那些重量级对象而言,是非常可观的一笔系统开销;
  • 由于 new 操作的次数减少,因而对系统内存的使用频率也会降低,这将减轻 GC 压力,缩短 GC 停顿时间。

Spring 中 bean 的默认作用域就是 singleton(单例)的

工厂模式

Spring 使用工厂模式可以通过 BeanFactoryApplicationContext 创建 bean 对象。

两者对比:

  • BeanFactory:延迟注入(使用到某个 bean 的时候才会注入),相比于ApplicationContext 来说会占用更少的内存,程序启动速度更快。
  • ApplicationContext:容器启动的时候,不管你用没用到,一次性创建所有 bean 。BeanFactory 仅提供了最基本的依赖注入支持,ApplicationContext 扩展了 BeanFactory ,除了有BeanFactory的功能还有额外更多功能,所以一般开发人员使用ApplicationContext会更多。

ApplicationContext 的三个实现类:

  1. ClassPathXmlApplication:把上下文文件当成类路径资源。
  2. FileSystemXmlApplication:从文件系统中的 XML 文件载入上下文定义信息。
  3. XmlWebApplicationContext:从 Web 系统中的 XML 文件载入上下文定义信息。
1
2
3
4
5
6
7
8
9
10
11
12
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.FileSystemXmlApplicationContext;

public class App {
public static void main(String[] args) {
ApplicationContext context = new FileSystemXmlApplicationContext(
"C:/work/IOC Containers/springframework.applicationcontext/src/main/resources/bean-factory-config.xml");

HelloApplicationContext obj = (HelloApplicationContext) context.getBean("helloApplicationContext");
obj.getMsg();
}
}

代理模式

AOP(Aspect-Oriented Programming,面向切面编程) 能够将那些与业务无关,却为业务模块所共同调用的逻辑或责任(例如事务处理、日志管理、权限控制等)封装起来,便于减少系统的重复代码,降低模块间的耦合度,并有利于未来的可拓展性和可维护性。

Spring AOP 就是基于动态代理的,如果要代理的对象,实现了某个接口,那么 Spring AOP 会使用 JDK Proxy 去创建代理对象,而对于没有实现接口的对象,就无法使用 JDK Proxy 去进行代理了,这时候 Spring AOP 会使用 Cglib 生成一个被代理对象的子类来作为代理

适配器模式

适配器模式(Adapter Pattern) 将一个接口转换成客户希望的另一个接口,适配器模式使接口不兼容的那些类可以一起工作。

Spring AOP 中的适配器

我们知道 Spring AOP 的实现是基于代理模式,但是 Spring AOP 的增强或通知(Advice)使用到了适配器模式,与之相关的接口是AdvisorAdapter

Advice 常用的类型有:BeforeAdvice(目标方法调用前,前置通知)、AfterAdvice(目标方法调用后,后置通知)、AfterReturningAdvice(目标方法执行结束后,return 之前)等等。每个类型 Advice(通知)都有对应的拦截器:MethodBeforeAdviceInterceptorAfterReturningAdviceInterceptorThrowsAdviceInterceptor 等等。

Spring 预定义的通知要通过对应的适配器,适配成 MethodInterceptor 接口(方法拦截器)类型的对象(如:MethodBeforeAdviceAdapter 通过调用 getInterceptor 方法,将 MethodBeforeAdvice 适配成 MethodBeforeAdviceInterceptor )。

Spring MVC 中的适配器模式

在 Spring MVC 中,DispatcherServlet 根据请求信息调用 HandlerMapping,解析请求对应的 Handler。解析到对应的 Handler(也就是我们平常说的 Controller 控制器)后,开始由HandlerAdapter 适配器处理。HandlerAdapter 作为期望接口,具体的适配器实现类用于对目标类进行适配,Controller 作为需要适配的类。

模板方法模式

Spring 中 JdbcTemplateHibernateTemplate 等以 Template 结尾的对数据库操作的类,它们就使用到了模板模式。一般情况下,我们都是使用继承的方式来实现模板模式,但是 Spring 并没有使用这种方式,而是使用Callback 模式与模板方法模式配合,既达到了代码复用的效果,同时增加了灵活性。

装饰者模式

装饰者模式可以动态地给对象添加一些额外的属性或行为。相比于使用继承,装饰者模式更加灵活。简单点儿说就是当我们需要修改原有的功能,但我们又不愿直接去修改原有的代码时,设计一个 Decorator 套在原有代码外面。其实在 JDK 中就有很多地方用到了装饰者模式,比如 InputStream家族,InputStream 类下有 FileInputStream (读取文件)、BufferedInputStream (增加缓存,使读取文件速度大大提升)等子类都在不修改InputStream 代码的情况下扩展了它的功能。

Spring 中配置 DataSource 的时候,DataSource 可能是不同的数据库和数据源。我们能否根据客户的需求在少修改原有类的代码下动态切换不同的数据源?这个时候就要用到装饰者模式(这一点我自己还没太理解具体原理)。Spring 中用到的包装器模式在类名上含有 Wrapper或者 Decorator。这些类基本上都是动态地给一个对象添加一些额外的职责

SpringBoot常用注解有哪些

@SpringBootApplication

这里先单独拎出@SpringBootApplication 注解说一下,虽然我们一般不会主动去使用它。

Guide:这个注解是 Spring Boot 项目的基石,创建 SpringBoot 项目之后会默认在主类加上。

1
2
3
4
5
6
@SpringBootApplication
public class SpringSecurityJwtGuideApplication {
public static void main(java.lang.String[] args) {
SpringApplication.run(SpringSecurityJwtGuideApplication.class, args);
}
}

我们可以把 @SpringBootApplication看作是 @SpringBootConfiguration@EnableAutoConfiguration@ComponentScan 注解的集合。
@EnableAutoconfiguration :启用SpringBoot的自动配置机制

@ComponentScan :扫描被 @Component(@Service,@controller )注解的 bean,注解默认会扫描该类所在的包下所有的类。

@SpringBootConfiguration: 组合了@Configuration注解,实现配置文件的功能

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package org.springframework.boot.autoconfigure;
@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 {
......
}

package org.springframework.boot;
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Configuration
public @interface SpringBootConfiguration {

}

工作原理如下:

  1. 当Spring Boot应用启动时,会扫描主程序类上的@SpringBootApplication注解。
  2. 根据@SpringBootConfiguration注解,将当前类作为配置类。
  3. 根据@ComponentScan注解,扫描指定包下的类,并将其注册为Spring容器中的Bean。
  4. 根据@EnableAutoConfiguration注解,Spring Boot会自动根据项目中的依赖配置相关的Bean。例如,如果项目中引入了spring-boot-starter-web依赖,那么Spring Boot会自动配置Tomcat和Spring MVC等相关的Bean。
  5. Spring Boot会将所有配置好的Bean注入到Spring容器中,完成自动配置。
  6. 最后,Spring Boot会调用主程序类的main方法,启动应用。

@RestController

@RestController注解是@Controller@ResponseBody的合集,表示这是个控制器 bean,并且是将函数的返回值直接填入 HTTP 响应体中,是 REST 风格的控制器。

单独使用 @Controller 不加 @ResponseBody的话一般是用在要返回一个视图的情况,这种情况属于比较传统的 Spring MVC 的应用,对应于前后端不分离的情况。@Controller +@ResponseBody 返回 JSON 或 XML 形式数据

@RequestMapping

@RequestMapping 是 Spring MVC 中的一个注解,用于映射 HTTP 请求到控制器的处理方法。它可以应用于类级别或方法级别,提供灵活的配置选项来处理不同的请求路径、HTTP 方法、请求参数等。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@RestController
@RequestMapping("/api")
public class MyController {

// 处理 GET 请求 /api/hello
@RequestMapping(value = "/hello", method = RequestMethod.GET)
public String sayHello() {
return "Hello, World!";
}

// 处理 POST 请求 /api/hello 并接收一个参数
@RequestMapping(value = "/hello", method = RequestMethod.POST)
public String sayHelloPost(@RequestParam String name) {
return "Hello, " + name + "!";
}

// 处理 /api/greet,路径参数和查询参数的结合
@RequestMapping(value = "/greet/{name}", method = RequestMethod.GET)
public String greet(@PathVariable String name, @RequestParam(defaultValue = "guest") String title) {
return "Hello, " + title + " " + name + "!";
}
}

@Scope

声明 Spring Bean 的作用域,使用方法:

1
2
3
4
5
@Bean
@Scope("singleton")
public Person personSingleton() {
return new Person();
}

四种常见的 Spring Bean 的作用域:

  • singleton : 唯一 bean 实例,Spring 中的 bean 默认都是单例的。
  • prototype : 每次请求都会创建一个新的 bean 实例。
  • request : 每一次 HTTP 请求都会产生一个新的 bean,该 bean 仅在当前 HTTP request 内有效。
  • session : 每一个 HTTP Session 会产生一个新的 bean,该 bean 仅在当前 HTTP session 内有效。

@Configuration

一般用来声明配置类,可以使用 @Component注解替代,不过使用@Configuration注解声明配置类更加语义化。

1
2
3
4
5
6
7
@Configuration
public class AppConfig {
@Bean
public TransferService transferService() {
return new TransferServiceImpl();
}
}

处理常见的 HTTP 请求类型

5 种常见的请求类型:

  • GET:请求从服务器获取特定资源。举个例子:GET /users(获取所有学生)
  • POST:在服务器上创建一个新的资源。举个例子:POST /users(创建学生)
  • PUT:更新服务器上的资源(客户端提供更新后的整个资源)。举个例子:PUT /users/12(更新编号为 12 的学生)
  • DELETE:从服务器删除特定的资源。举个例子:DELETE /users/12(删除编号为 12 的学生)
  • PATCH:更新服务器上的资源(客户端提供更改的属性,可以看做作是部分更新),使用的比较少,这里就不举例子了。

GET 请求

1
@GetMapping("users")` 等价于`@RequestMapping(value="/users",method=RequestMethod.GET)
1
2
3
4
@GetMapping("/users")
public ResponseEntity<List<User>> getAllUsers() {
return userRepository.findAll();
}

POST 请求

1
@PostMapping("users")` 等价于`@RequestMapping(value="/users",method=RequestMethod.POST)

关于@RequestBody注解的使用,在下面的“前后端传值”这块会讲到。

1
2
3
4
@PostMapping("/users")
public ResponseEntity<User> createUser(@Valid @RequestBody UserCreateRequest userCreateRequest) {
return userRepository.save(userCreateRequest);
}

PUT 请求

1
@PutMapping("/users/{userId}")` 等价于`@RequestMapping(value="/users/{userId}",method=RequestMethod.PUT)
1
2
3
4
5
@PutMapping("/users/{userId}")
public ResponseEntity<User> updateUser(@PathVariable(value = "userId") Long userId,
@Valid @RequestBody UserUpdateRequest userUpdateRequest) {
......
}

DELETE 请求

1
@DeleteMapping("/users/{userId}")`等价于`@RequestMapping(value="/users/{userId}",method=RequestMethod.DELETE)
1
2
3
4
@DeleteMapping("/users/{userId}")
public ResponseEntity deleteUser(@PathVariable(value = "userId") Long userId){
......
}

PATCH 请求

一般实际项目中,我们都是 PUT 不够用了之后才用 PATCH 请求去更新数据。

1
2
3
4
5
@PatchMapping("/profile")
public ResponseEntity updateStudent(@RequestBody StudentUpdateRequest studentUpdateRequest) {
studentRepository.updateDetail(studentUpdateRequest);
return ResponseEntity.ok().build();
}

前后端传值

@PathVariable 和 @RequestParam

@PathVariable用于获取路径参数,即路径中的占位符部分,@RequestParam用于从请求参数中获取参数值,即 URL 中的查询参数或者表单参数。

举个简单的例子:

1
2
3
4
5
6
@GetMapping("/klasses/{klassId}/teachers")
public List<Teacher> getKlassRelatedTeachers(
@PathVariable("klassId") Long klassId,
@RequestParam(value = "type", required = false) String type ) {
...
}

如果我们请求的 url 是:/klasses/123456/teachers?type=web

那么我们服务获取到的数据就是:klassId=123456,type=web

@RequestBody

用于读取 Request 请求(可能是 POST,PUT,DELETE,GET 请求)的 body 部分并且Content-Type 为 application/json 格式的数据,接收到数据之后会自动将数据绑定到 Java 对象上去。系统会使用HttpMessageConverter或者自定义的HttpMessageConverter将请求的 body 中的 json 字符串转换为 java 对象。

我用一个简单的例子来给演示一下基本使用!

我们有一个注册的接口:

1
2
3
4
5
@PostMapping("/sign-up")
public ResponseEntity signUp(@RequestBody @Valid UserRegisterRequest userRegisterRequest) {
userService.save(userRegisterRequest);
return ResponseEntity.ok().build();
}

UserRegisterRequest对象:

1
2
3
4
5
6
7
8
9
10
11
@Data
@AllArgsConstructor
@NoArgsConstructor
public class UserRegisterRequest {
@NotBlank
private String userName;
@NotBlank
private String password;
@NotBlank
private String fullName;
}

我们发送 post 请求到这个接口,并且 body 携带 JSON 数据:

1
{ "userName": "coder", "fullName": "shuangkou", "password": "123456" }

这样我们的后端就可以直接把 json 格式的数据映射到我们的 UserRegisterRequest 类上。

image-20240319221351739

需要注意的是:**一个请求方法只可以有一个@RequestBody,但是可以有多个@RequestParam@PathVariable**。 如果你的方法必须要用两个 @RequestBody来接受数据的话,大概率是你的数据库设计或者系统设计出问题了!

读取配置信息

很多时候我们需要将一些常用的配置信息比如阿里云 oss、发送短信、微信认证的相关配置信息等等放到配置文件中。

下面我们来看一下 Spring 为我们提供了哪些方式帮助我们从配置文件中读取这些配置信息。

我们的数据源application.yml内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
wuhan2020: 2020年初武汉爆发了新型冠状病毒,疫情严重,但是,我相信一切都会过去!武汉加油!中国加油!

my-profile:
name: Guide哥
email: koushuangbwcx@163.com

library:
location: 湖北武汉加油中国加油
books:
- name: 天才基本法
description: 二十二岁的林朝夕在父亲确诊阿尔茨海默病这天,得知自己暗恋多年的校园男神裴之即将出国深造的消息——对方考取的学校,恰是父亲当年为她放弃的那所。
- name: 时间的秩序
description: 为什么我们记得过去,而非未来?时间“流逝”意味着什么?是我们存在于时间之内,还是时间存在于我们之中?卡洛·罗韦利用诗意的文字,邀请我们思考这一亘古难题——时间的本质。
- name: 了不起的我
description: 如何养成一个新习惯?如何让心智变得更成熟?如何拥有高质量的关系? 如何走出人生的艰难时

@Value(常用)

使用 @Value("${property}") 读取比较简单的配置信息:

1
2
@Value("${wuhan2020}")
String wuhan2020;

@ConfigurationProperties(常用)

通过@ConfigurationProperties读取配置信息并与 bean 绑定。其中prefix为配置信息的前缀

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Component
@ConfigurationProperties(prefix = "library")
class LibraryProperties {
@NotEmpty
private String location;
private List<Book> books;

@Setter
@Getter
@ToString
static class Book {
String name;
String description;
}
省略getter/setter
......
}

@PropertySource(不常用)

@PropertySource读取指定 properties 文件

1
2
3
4
5
6
7
8
9
10
@Component
@PropertySource("classpath:website.properties")

class WebSite {
@Value("${url}")
private String url;

省略getter/setter
......
}

参数校验

数据的校验的重要性就不用说了,即使在前端对数据进行校验的情况下,我们还是要对传入后端的数据再进行一遍校验,避免用户绕过浏览器直接通过一些 HTTP 工具直接向后端请求一些违法数据。

JSR(Java Specification Requests) 是一套 JavaBean 参数校验的标准,它定义了很多常用的校验注解,我们可以直接将这些注解加在我们 JavaBean 的属性上面,这样就可以在需要校验的时候进行校验了,非常方便!

校验的时候我们实际用的是 Hibernate Validator 框架。Hibernate Validator 是 Hibernate 团队最初的数据校验框架,Hibernate Validator 4.x 是 Bean Validation 1.0(JSR 303)的参考实现,Hibernate Validator 5.x 是 Bean Validation 1.1(JSR 349)的参考实现,目前最新版的 Hibernate Validator 6.x 是 Bean Validation 2.0(JSR 380)的参考实现。

SpringBoot 项目的 spring-boot-starter-web 依赖中已经有 hibernate-validator 包,不需要引用相关依赖。如下图所示(通过 idea 插件—Maven Helper 生成):

:更新版本的 spring-boot-starter-web 依赖中不再有 hibernate-validator 包(如 2.3.11.RELEASE),需要自己引入 spring-boot-starter-validation 依赖。

image-20240319222919988

需要注意的是:所有的注解,推荐使用 JSR 注解,即javax.validation.constraints,而不是org.hibernate.validator.constraints

一些常用的字段验证的注解

  • @NotEmpty 被注释的字符串的不能为 null 也不能为空
  • @NotBlank 被注释的字符串非 null,并且必须包含一个非空白字符
  • @Null 被注释的元素必须为 null
  • @NotNull 被注释的元素必须不为 null
  • @AssertTrue 被注释的元素必须为 true
  • @AssertFalse 被注释的元素必须为 false
  • @Pattern(regex=,flag=)被注释的元素必须符合指定的正则表达式
  • @Email 被注释的元素必须是 Email 格式。
  • @Min(value)被注释的元素必须是一个数字,其值必须大于等于指定的最小值
  • @Max(value)被注释的元素必须是一个数字,其值必须小于等于指定的最大值
  • @DecimalMin(value)被注释的元素必须是一个数字,其值必须大于等于指定的最小值
  • @DecimalMax(value) 被注释的元素必须是一个数字,其值必须小于等于指定的最大值
  • @Size(max=, min=)被注释的元素的大小必须在指定的范围内
  • @Digits(integer, fraction)被注释的元素必须是一个数字,其值必须在可接受的范围内
  • @Past被注释的元素必须是一个过去的日期
  • @Future 被注释的元素必须是一个将来的日期
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Person {

@NotNull(message = "classId 不能为空")
private String classId;

@Size(max = 33)
@NotNull(message = "name 不能为空")
private String name;

@Pattern(regexp = "((^Man$|^Woman$|^UGM$))", message = "sex 值不在可选范围")
@NotNull(message = "sex 不能为空")
private String sex;

@Email(message = "email 格式不正确")
@NotNull(message = "email 不能为空")
private String email;

}

我们在需要验证的参数上加上了@Valid注解,如果验证失败,它将抛出MethodArgumentNotValidException

1
2
3
4
5
6
7
8
@RestController
@RequestMapping("/api")
public class PersonController {
@PostMapping("/person")
public ResponseEntity<Person> getPerson(@RequestBody @Valid Person person) {
return ResponseEntity.ok().body(person);
}
}

验证请求参数@Validated

使用PathVariables 和 RequestParameters一定不要忘记在类上加上 @Validated 注解了,这个参数可以告诉 Spring 去校验方法参数。

1
2
3
4
5
6
7
8
9
@RestController
@RequestMapping("/api")
@Validated
public class PersonController {
@GetMapping("/person/{id}")
public ResponseEntity<Integer> getPersonByID(@Valid @PathVariable("id") @Max(value = 5,message = "超过 id 的范围了") Integer id) {
return ResponseEntity.ok().body(id);
}
}

全局处理 Controller 层异常

介绍一下我们 Spring 项目必备的全局处理 Controller 层异常。

相关注解:

  1. @ControllerAdvice :注解定义全局异常处理类
  2. @ExceptionHandler :注解声明异常处理方法

1. 新建异常信息实体类

非必要的类,主要用于包装异常信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class ErrorResponse {
private String errorTypeName;
private String message;

public ErrorResponse(Exception e) {
this(e.getClass().getName(), e.getMessage());
}

public ErrorResponse(String errorTypeName, String message) {
this.errorTypeName = errorTypeName;
this.message = message;
}
......省略getter/setter方法
}

2. 自定义异常类型

一般我们处理的都是 RuntimeException ,所以如果你需要自定义异常类型的话直接集成这个类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class ResourceNotFoundException extends RuntimeException {
private String message;

public ResourceNotFoundException() {
super();
}

public ResourceNotFoundException(String message) {
super(message);
this.message = message;
}

@Override
public String getMessage() {
return message;
}

public void setMessage(String message) {
this.message = message;
}
}

3. 新建异常处理类

我们只需要在类上加上@ControllerAdvice注解这个类就成为了全局异常处理类,当然你也可以通过 assignableTypes指定特定的 Controller 类,让异常处理类只处理特定类抛出的异常。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@ControllerAdvice(assignableTypes = {ExceptionController.class})
@ResponseBody
public class GlobalExceptionHandler {

ErrorResponse illegalArgumentResponse = new ErrorResponse(new IllegalArgumentException("参数错误!"));
ErrorResponse resourseNotFoundResponse = new ErrorResponse(new ResourceNotFoundException("Sorry, the resourse not found!"));

@ExceptionHandler(value = Exception.class)
// 拦截所有异常, 这里只是为了演示,一般情况下一个方法特定处理一种异常
public ResponseEntity<ErrorResponse> exceptionHandler(Exception e) {

if (e instanceof IllegalArgumentException) {
return ResponseEntity.status(400).body(illegalArgumentResponse);
} else if (e instanceof ResourceNotFoundException) {
return ResponseEntity.status(404).body(resourseNotFoundResponse);
}
return null;
}
}

4. controller模拟抛出异常

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@RestController
@RequestMapping("/api")
public class ExceptionController {

@GetMapping("/illegalArgumentException")
public void throwException() {
throw new IllegalArgumentException();
}

@GetMapping("/resourceNotFoundException")
public void throwException2() {
throw new ResourceNotFoundException();
}
}

使用 Get 请求 localhost:8080/api/resourceNotFoundException,服务端返回的 JSON 数据如下:

1
2
3
4
{    
"message": "Sorry, the resourse not found!",
"errorTypeName": "com.twuc.webApp.exception.ResourceNotFoundException"
}

5. 编写测试类

MockMvc 由org.springframework.boot.test包提供,实现了对Http请求的模拟,一般用于我们测试 controller 层。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@AutoConfigureMockMvc
@SpringBootTest
public class ExceptionTest {
@Autowired
MockMvc mockMvc;

@Test
void should_return_400_if_param_not_valid() throws Exception {
mockMvc.perform(get("/api/illegalArgumentException"))
.andExpect(status().is(400))
.andExpect(jsonPath("$.message").value("参数错误!"));
}

@Test
void should_return_404_if_resourse_not_found() throws Exception {
mockMvc.perform(get("/api/resourceNotFoundException"))
.andExpect(status().is(404))
.andExpect(jsonPath("$.message").value("Sorry, the resourse not found!"));
}
}

@ResponseStatus

@ResponseStatus是Spring框架中的一个注解,用于在控制器方法或自定义异常类上指定默认的HTTP响应状态码。

1
2
3
4
5
6
7
8
@ResponseStatus(code = HttpStatus.NOT_FOUND)
public class ResourseNotFoundException2 extends RuntimeException {
public ResourseNotFoundException2() {
}
public ResourseNotFoundException2(String message) {
super(message);
}
}
1
2
3
4
5
6
7
8
@RestController
@RequestMapping("/api")
public class ResponseStatusExceptionController {
@GetMapping("/resourceNotFoundException2")
public void throwException3() {
throw new ResourseNotFoundException2("Sorry, the resourse not found!");
}
}

使用 Get 请求 localhost:8080/api/resourceNotFoundException2,服务端返回的 JSON 数据如下:

1
2
3
4
5
6
7
{    
"timestamp": "2019-08-21T07:11:43.744+0000",
"status": 404,
"error": "Not Found",
"message": "Sorry, the resourse not found!",
"path": "/api/resourceNotFoundException2"
}

ResponseStatusException类

ResponseStatusException类是位于org.springframework.web.server.ResponseStatusException包中的类。它是Spring框架中用于表示特定HTTP状态码的异常类。

这种通过 ResponseStatus注解简单处理异常的方法是的好处是比较简单,但是一般我们不会这样做,通过ResponseStatusException会更加方便,可以避免我们额外的异常类。

1
2
3
4
@GetMapping("/resourceNotFoundException2")
public void throwException3() {
throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Sorry, the resourse not found!", new ResourceNotFoundException());
}

使用 Get 请求 localhost:8080/api/resourceNotFoundException2 ,服务端返回的 JSON 数据和使用 ResponseStatus 实现的效果一样

ResponseStatusException 提供了三个构造方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public ResponseStatusException(HttpStatus status) {
this(status, null, null);
}

public ResponseStatusException(HttpStatus status, @Nullable String reason) {
this(status, reason, null);
}

public ResponseStatusException(HttpStatus status, @Nullable String reason, @Nullable Throwable cause) {
super(null, cause);
Assert.notNull(status, "HttpStatus is required");
this.status = status;
this.reason = reason;
}

构造函数中的参数解释如下:

status :http status

reason :response 的消息内容

cause :抛出的异常

格式化 json 数据

@JsonFormat一般用来格式化 json 数据。

比如:

1
2
@JsonFormat(shape=JsonFormat.Shape.STRING, pattern="yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", timezone="GMT")
private Date date;

扁平化对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Getter
@Setter
@ToString
public class Account {
private Location location;
private PersonInfo personInfo;

@Getter
@Setter
@ToString
public static class Location {
private String provinceName;
private String countyName;
}
@Getter
@Setter
@ToString
public static class PersonInfo {
private String userName;
private String fullName;
}
}

未扁平化之前:

1
2
3
4
5
6
7
8
9
10
{
"location": {
"provinceName": "湖北",
"countyName": "武汉"
},
"personInfo": {
"userName": "coder1234",
"fullName": "shaungkou"
}
}

使用@JsonUnwrapped 扁平对象之后:

1
2
3
4
5
6
7
8
9
10
@Getter
@Setter
@ToString
public class Account {
@JsonUnwrapped
private Location location;
@JsonUnwrapped
private PersonInfo personInfo;
......
}
1
2
3
4
5
6
{
"provinceName": "湖北",
"countyName": "武汉",
"userName": "coder1234",
"fullName": "shaungkou"
}

测试相关

@ActiveProfiles一般作用于测试类上, 用于声明生效的 Spring 配置文件。

1
2
3
4
5
6
@SpringBootTest(webEnvironment = RANDOM_PORT)
@ActiveProfiles("test")
@Slf4j
public abstract class TestBase {
......
}

@Test声明一个方法为测试方法

@Transactional被声明的测试方法的数据会回滚,避免污染测试数据。

@WithMockUser Spring Security 提供的,用来模拟一个真实用户,并且可以赋予权限。

1
2
3
4
5
6
@Test
@Transactional
@WithMockUser(username = "user-id-18163138155", authorities = "ROLE_TEACHER")
void should_import_student_success() throws Exception {
......
}

SpringBoot
http://example.com/2024/01/16/Spring+MyBatis/SpringBoot/
作者
PALE13
发布于
2024年1月16日
许可协议