达摩院--Spring和Spring Boot注解大全
最近深圳房价大涨,一个在深圳的学姐也加入了买房的队伍,什么现在还不买,一天涨一两百万,可怜的小宋现在连一个厕所都买不起🤣,还是老老实实继续写博客吧。
本章小宋会带大家去了解使用Spring和Spring Boot的详细注解,这个章节还是花了小宋比较长的时间,下面要讲的基本上也就是大家平常工作时会使用的场景。
注解大全
@SpringBootAplication
首先讲一下SpringBoot的基石注解@SpringBootAplication,这个注解使用在启动类上。
@SpringBootApplication
public class XiaoSongApplication {
public static void main(java.lang.String[] args) {
SpringApplication.run(XiaoSongApplication .class, args);
}
}
我们可以把 @SpringBootApplication看作是 @Configuration、@EnableAutoConfiguration、@ComponentScan 注解的集合。
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 {
}
作用如下:
- @EnableAutoConfiguration:启用 SpringBoot 的自动配置机制
- @ComponentScan: 扫描被@Component (@Service,@Controller)注解的 bean,注解默认会扫描该类所在的包下所有的类。
- @Configuration:允许在 Spring 上下文中注册额外的 bean 或导入其他配置类
Spring Bean注解
这里小宋详细讲一下Spring Bean相关的一些注解。
@Autowired
@Autowired 自动装配Bean,注解会自动导入对象到类中,被注入进的类同样要被 Spring 容器管理比如:Service 类注入到 Controller 类中。
@Service
public class UserService {
......
}
@RestController
@RequestMapping("/users")
public class UserController {
@Autowired
private UserService userService;
......
}
@Component,@Repository,@Service, @Controller注解
大家一般使用 @Autowired 注解让 Spring 容器帮我们自动装配 bean。要想把类标识成可用于 @Autowired 注解自动装配的 bean 的类,可以采用以下注解实现:
- @Component :通用的注解,可标注任意类为 Spring 组件。如果一个 Bean 不知道属于哪个层,可以使用@Component 注解标注。
- @Repository : 对应持久层即 Dao 层,主要用于数据库相关操作。
- @Service : 对应服务层,主要涉及一些复杂的逻辑,需要用到 Dao 层。
- @Controller : 对应 Spring MVC 控制层,主要用于接受用户请求并调用 Service 层返回数据给前端页面。
@Resource
spring不但支持自己定义的@Autowired注解,还支持几个由JSR-250规范定义的注解,它们分别是@Resource、@PostConstruct以及@PreDestroy。
@Resource的作用相当于@Autowired,只不过@Autowired按byType自动注入,而@Resource默认按 byName自动注入罢了。@Resource有两个属性是比较重要的,分是name和type,Spring将@Resource注解的name属性解析为bean的名字,而type属性则解析为bean的类型。所以如果使用name属性,则使用byName的自动注入策略,而使用type属性时则使用byType自动注入策略。如果既不指定name也不指定type属性,这时将通过反射机制使用byName自动注入策略。
@Resource装配顺序
1. 如果同时指定了name和type,则从Spring上下文中找到唯一匹配的bean进行装配,找不到则抛出异常
2. 如果指定了name,则从上下文中查找名称(id)匹配的bean进行装配,找不到则抛出异常
3. 如果指定了type,则从上下文中找到类型匹配的唯一bean进行装配,找不到或者找到多个,都会抛出异常
4. 如果既没有指定name,又没有指定type,则自动按照byName方式进行装配;如果没有匹配,则回退为一个原始类型进行匹配,如果匹配则自动装配;
@Resource 和 @Autowired区别
-
@Autowired与@Resource都可以用来装配bean. 都是用来实现依赖注入的.
-
@Autowired默认按类型装配(这个注解是属于spring的,spring提供),@AutoWried按byType自动注入,默认情况下必须要求依赖对象必须存在,如果要允许null值,可以设置它的required属性为false,如:@Autowired(required=false) ,如果我们想使用名称装配可以结合@Qualifier注解进行使用
-
@Resource(这个注解属于J2EE的,jdk提供),@Resource默认按byName自动注入,默认按照名称进行装配,名称可以通过name属性进行指定,如果没有指定name属性,当注解写在字段上时,默认取字段名进行按照名称查找,如果注解写在setter方法上默认取属性名进行装配。当找不到与名称匹配的bean时才按照类型进行装配。但是需要注意的是,如果name属性一旦指定,就只会按照名称进行装配。
推荐使用:@Resource注解在字段上,这样就不用写setter方法了,并且这个注解是属于J2EE的,减少了与spring的耦合。这样代码看起就比较优雅。
@RestController
@RestController注解是@Controller和@ResponseBody的合集,表示这是个控制器 bean,并且是将函数的返回值去直接填入 HTTP 响应体中,是 REST 风格的控制器。
现在都是前后端分离,说实话我已经很久没有用过@Controller。当然如果你的项目太老了的话,还是有可能。
单独使用 @Controller 不加 @ResponseBody的话一般使用在要返回一个视图的情况,这种情况属于比较传统的 Spring MVC 的应用,对应于前后端不分离的情况。@Controller +@ResponseBody 返回 JSON 或 XML 形式数据
@Scope
@Scope注解用于声明Spring Bean的作用域:
@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 请求都会产生一个新的 bean,该 bean 仅在当前 HTTP session 内有效。
@Configuration
一般用来声明配置类,可以使用 @Component注解替代,不过使用@Configuration注解声明配置类更加语义化。
@Configuration
public class AppConfig {
@Bean
public TransferService transferService() {
return new TransferServiceImpl();
}
}
处理常见的HTTP请求注解
下面我们来看看常见的5种http请求类型:
- GET :请求从服务器获取特定资源。举个例子:GET /users(获取所有学生)
- POST :在服务器上创建一个新的资源。举个例子:POST /users(创建学生)
- PUT :更新服务器上的资源(客户端提供更新后的整个资源)。举个例子:PUT /users/12(更新编号为 12 的学生)
- DELETE :从服务器删除特定的资源。举个例子:DELETE /users/12(删除编号为 12 的学生)
- PATCH :更新服务器上的资源(客户端提供更改的属性,可以看做作是部分更新),使用的比较少,下面就不举例子了
GET请求
@GetMapping("/list") == @RequestMapping(value="/list",method=RequestMethod.GET)
@GetMapping("/list")
public Result<Map<String,Object>> getList() {
IPage<CreditsDetailPageVo> pageList=iCreditsDetailService.pageList(new Page<Map<String,Object>>(pageNo, pageSize),map);
map.put("pageList",pageList);
result.setResult(map);
result.setSuccess(true);
return result;
}
POST请求
@PostMapping(“users”) == @RequestMapping(value="/users",method=RequestMethod.POST)
@PostMapping("/users")
public ResponseEntity<User> createUser(@Valid @RequestBody UserCreateRequest userCreateRequest) {
return userRespository.save(user);
}
PUT请求
@PutMapping("/users/{userId}") == @RequestMapping(value="/users/{userId}",method=RequestMethod.PUT)
@PutMapping("/users/{userId}")
public ResponseEntity<User> updateUser(@PathVariable(value = "userId") Long userId,
@Valid @RequestBody UserUpdateRequest userUpdateRequest) {
......
}
DELETE请求
@DeleteMapping("/users/{userId}") == @RequestMapping(value="/users/{userId}",method=RequestMethod.DELETE)
@DeleteMapping("/users/{userId}")
public ResponseEntity deleteUser(@PathVariable(value = "userId") Long userId){
......
}
PATCH请求
一般在项目中,我们都是 PUT 不够用了之后才用 PATCH 请求去更新数据。
@PatchMapping("/profile")
public ResponseEntity updateStudent(@RequestBody StudentUpdateRequest studentUpdateRequest) {
studentRepository.updateDetail(studentUpdateRequest);
return ResponseEntity.ok().build();
}
前后端传值注解
掌握前后端的传值,是我们开始 CRUD 的第一步!
@PathVariable 和 @RequestParam
@PathVariable用于获取路径参数,@RequestParam请求参数可用于获取查询参数。
eg:
@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 对象。
比如下面这个注册的接口:
@PostMapping("/sign-up")
public ResponseEntity signUp(@RequestBody @Valid UserRegisterRequest userRegisterRequest) {
userService.save(userRegisterRequest);
return ResponseEntity.ok().build();
}
UserRegisterRequest类:
@Data
@AllArgsConstructor
@NoArgsConstructor
public class UserRegisterRequest {
@NotBlank
private String userName;
@NotBlank
private String password;
@NotBlank
private String fullName;
}
我们发送 post 请求到这个接口,并且 body 携带 JSON 数据:
{"userName":"coder","fullName":"shuangkou","password":"123456"}
然后后端直接把 json 格式的数据映射到我们的 UserRegisterRequest 类上。
注意:一个请求方法只可以有一个@RequestBody,但是可以有多个@RequestParam和@PathVariable。 如果你的方法必须要用两个 @RequestBody来接受数据的话,大概率是你的数据库设计或者系统设计出问题了!
读取配置信息注解
很多时候我们需要将一些常用的配置信息比如阿里云 oss 配置、发送短信的相关信息配置等等放到配置文件中。
下面我们来看一下 Spring 为我们提供了哪些方式帮助我们从配置文件中读取这些配置信息。
配置文件内容如下:
server:
port: 8080
tomcat:
max-swallow-size: -1
servlet:
context-path: /vyun
compression:
enabled: true
mime-types: application/javascript,application/json,application/xml,text/html,text/xml,text/plain,text/css,image/*
management:
endpoints:
web:
exposure:
include: metrics,httptrace
spring:
servlet:
multipart:
max-file-size: 1024MB
max-request-size: 1024MB
## quartz定时任务,采用数据库方式
quartz:
job-store-type: jdbc
#json 时间戳统一转换
jackson:
date-format: yyyy-MM-dd HH:mm:ss
time-zone: GMT+8
#接口返回的数据中不含value为null的key
#default-property-inclusion: non_null
使用 @Value("${property}") 读取
@Value
@Value我们常用来读取比较简单的配置信息
@Value("${server.port}")
int port;
@ConfigurationProperties
使用@ConfigurationProperties读取配置信息并与 bean 绑定。
@Component
@ConfigurationProperties(prefix = "spring.servlet.multipart")
public class MultipartProperties {
@NotEmpty
private String maxFileSize; // 配置文件中是max-file-size, 转驼峰命名便可以绑定成
private String maxRequestSize;
省略getter/setter
......
}
你可以像使用普通的 Spring bean 一样,将其注入到类中使用。
@PropertySource
这个注解不怎么常用,但还是讲一下。
@PropertySource读取指定 properties 文件
@Component
@PropertySource("classpath:config.properties")
public class Config {
@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)的参考实现。
相关依赖
如果开发普通的Java程序的话,依赖入下:
<dependency>
<groupId>org.hibernate.validator</groupId>
<artifactId>hibernate-validator</artifactId>
<version>6.0.9.Final</version>
</dependency>
<dependency>
<groupId>javax.el</groupId>
<artifactId>javax.el-api</artifactId>
<version>3.0.0</version>
</dependency>
<dependency>
<groupId>org.glassfish.web</groupId>
<artifactId>javax.el</artifactId>
<version>2.2.6</version>
</dependency>
SpringBoot 项目的 spring-boot-starter-web 依赖中已经有 hibernate-validator 包,除了这个依赖,下面的演示还用到了 lombok ,所以不要忘记添加上相关依赖。如下:
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

注意一下: 所有的注解,推荐使用 JSR 注解,即javax.validation.constraints,而不是org.hibernate.validator.constraints
实体类
@Data
@AllArgsConstructor
@NoArgsConstructor
publicclass 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;
}
一些常用的字段验证的注解
JSR提供的校验注解:
- @Null 被注释的元素必须为 null
- @NotNull 被注释的元素必须不为 null
- @AssertTrue 被注释的元素必须为 true
- @AssertFalse 被注释的元素必须为 false
- @Min(value) 被注释的元素必须是一个数字,其值必须大于等于指定的最小值
- @Max(value) 被注释的元素必须是一个数字,其值必须小于等于指定的最大值
- @DecimalMin(value) 被注释的元素必须是一个数字,其值必须大于等于指定的最小值
- @DecimalMax(value) 被注释的元素必须是一个数字,其值必须小于等于指定的最大值
- @Size(max=, min=) 被注释的元素的大小必须在指定的范围内
- @Digits (integer, fraction) 被注释的元素必须是一个数字,其值必须在可接受的范围内
- @Past 被注释的元素必须是一个过去的日期
- @Future 被注释的元素必须是一个将来的日期
- @Pattern(regex=,flag=) 被注释的元素必须符合指定的正则表达式
正则表达式说明:- ^string : 匹配以 string 开头的字符串
- string$ :匹配以 string 结尾的字符串
- ^string$ :精确匹配 string 字符串
- ((Man$|^Woman$|UGM$)) : 值只能在 Man,Woman,UGM 这三个值中选择
Hibernate Validator提供的校验注解:
- @NotBlank(message =) 验证字符串非null,且长度必须大于0
- @Email 被注释的元素必须是电子邮箱地址
- @Length(min=,max=) 被注释的字符串的大小必须在指定的范围内
- @NotEmpty 被注释的字符串的必须非空
- @Range(min=,max=,message=) 被注释的元素必须在合适的范围内
验证Controller的输入
验证请求体(RequestBody)
Controller:
我们在需要验证的参数上加上了@Valid注解,如果验证失败,它将抛出MethodArgumentNotValidException。默认情况下,Spring会将此异常转换为HTTP Status 400(错误请求)。
@RestController
@RequestMapping("/api")
publicclass PersonController {
@PostMapping("/person")
public ResponseEntity<Person> getPerson(@RequestBody @Valid Person person) {
return ResponseEntity.ok().body(person);
}
}
ExceptionHandler:
自定义异常处理器可以帮助我们捕获异常,并进行一些简单的处理。
@ControllerAdvice(assignableTypes = {PersonController.class})
publicclass GlobalExceptionHandler {
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<Map<String, String>> handleValidationExceptions(
MethodArgumentNotValidException ex) {
Map<String, String> errors = new HashMap<>();
ex.getBindingResult().getAllErrors().forEach((error) -> {
String fieldName = ((FieldError) error).getField();
String errorMessage = error.getDefaultMessage();
errors.put(fieldName, errorMessage);
});
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(errors);
}
}
通过测试验证:
下面我通过 MockMvc 模拟请求 Controller 的方式来验证是否生效,也可以通过 Postman 工具来验证。
- 所有参数输入正确的情况:
@RunWith(SpringRunner.class) @SpringBootTest @AutoConfigureMockMvc publicclass PersonControllerTest { @Autowired private MockMvc mockMvc; @Autowired private ObjectMapper objectMapper; @Test public void should_get_person_correctly() throws Exception { Person person = new Person(); person.setName("SnailClimb"); person.setSex("Man"); person.setClassId("82938390"); person.setEmail("Snailclimb@qq.com"); mockMvc.perform(post("/api/person") .contentType(MediaType.APPLICATION_JSON_UTF8) .content(objectMapper.writeValueAsString(person))) .andExpect(MockMvcResultMatchers.jsonPath("name").value("SnailClimb")) .andExpect(MockMvcResultMatchers.jsonPath("classId").value("82938390")) .andExpect(MockMvcResultMatchers.jsonPath("sex").value("Man")) .andExpect(MockMvcResultMatchers.jsonPath("email").value("Snailclimb@qq.com")); } - 验证出现参数不合法的情况抛出异常并且可以正确被捕获。
使用 Postman 验证结果如下:@Test public void should_check_person_value() throws Exception { Person person = new Person(); person.setSex("Man22"); person.setClassId("82938390"); person.setEmail("SnailClimb"); mockMvc.perform(post("/api/person") .contentType(MediaType.APPLICATION_JSON_UTF8) .content(objectMapper.writeValueAsString(person))) .andExpect(MockMvcResultMatchers.jsonPath("sex").value("sex 值不在可选范围")) .andExpect(MockMvcResultMatchers.jsonPath("name").value("name 不能为空")) .andExpect(MockMvcResultMatchers.jsonPath("email").value("email 格式不正确")); }
![在这里插入图片描述]()
验证请求参数(Path Variables 和 Request Parameters)
Controller:
一定一定不要忘记在类上加上 Validated 注解了,这个参数可以告诉 Spring 去校验方法参数。
@RestController
@RequestMapping("/api")
@Validated
publicclass PersonController {
@GetMapping("/person/{id}")
public ResponseEntity<Integer> getPersonByID(@Valid @PathVariable("id") @Max(value = 5,message = "超过 id 的范围了") Integer id) {
return ResponseEntity.ok().body(id);
}
@PutMapping("/person")
public ResponseEntity<String> getPersonByName(@Valid @RequestParam("name") @Size(max = 6,message = "超过 name 的范围了") String name) {
return ResponseEntity.ok().body(name);
}
}
ExceptionHandler:
@ExceptionHandler(ConstraintViolationException.class)
ResponseEntity<String> handleConstraintViolationException(ConstraintViolationException e) {
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(e.getMessage());
}
通过测试验证:
@Test
public void should_check_param_value() throws Exception {
mockMvc.perform(get("/api/person/6")
.contentType(MediaType.APPLICATION_JSON_UTF8))
.andExpect(status().isBadRequest())
.andExpect(content().string("getPersonByID.id: 超过 id 的范围了"));
}
@Test
public void should_check_param_value2() throws Exception {
mockMvc.perform(put("/api/person")
.param("name","snailclimbsnailclimb")
.contentType(MediaType.APPLICATION_JSON_UTF8))
.andExpect(status().isBadRequest())
.andExpect(content().string("getPersonByName.name: 超过 name 的范围了"));
}
验证 Service 的输入
我们还可以验证任何Spring组件的输入,而不是验证控制器级别的输入,我们可以使用 @Validated 和 @Valid 注释的组合来实现这一需求。
一定一定不要忘记在类上加上 Validated 注解了,这个参数可以告诉 Spring 去校验方法参数。
@Service
@Validated
public class PersonService {
public void validatePerson(@Valid Person person){
// do something
}
}
测试:
@RunWith(SpringRunner.class)
@SpringBootTest
@AutoConfigureMockMvc
publicclass PersonServiceTest {
@Autowired
private PersonService service;
@Test(expected = ConstraintViolationException.class)
public void should_throw_exception_when_person_is_not_valid() {
Person person = new Person();
person.setSex("Man22");
person.setClassId("82938390");
person.setEmail("SnailClimb");
service.validatePerson(person);
}
}
Validator 编程方式手动进行参数验证
某些场景下可能会需要我们手动校验并获得校验结果。
@Test
public void check_person_manually() {
ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
Validator validator = factory.getValidator();
Person person = new Person();
person.setSex("Man22");
person.setClassId("82938390");
person.setEmail("SnailClimb");
Set<ConstraintViolation<Person>> violations = validator.validate(person);
//output:
//email 格式不正确
//name 不能为空
//sex 值不在可选范围
for (ConstraintViolation<Person> constraintViolation : violations) {
System.out.println(constraintViolation.getMessage());
}
}
上面我们是通过 Validator 工厂类获得的 Validator 示例,当然你也可以通过 @Autowired 直接注入的方式。但是在非 Spring Component 类中使用这种方式的话,只能通过工厂类来获得 Validator。
@Autowired
Validator validate
自定义 Validator(实用)
如果自带的校验注解无法满足你的需求的话,你还可以自定义实现注解。
案例一:校验特定字段的值是否在可选范围
比如我们现在多了这样一个需求:Person类多了一个 region 字段,region 字段只能是China、China-Taiwan、China-HongKong这三个中的一个。
-
第一步需要创建一个注解:
@Target({FIELD}) @Retention(RUNTIME) @Constraint(validatedBy = RegionValidator.class) @Documented public@interface Region { String message() default "Region 值不在可选范围内"; Class<?>[] groups() default {}; Class<? extends Payload>[] payload() default {}; } -
第二步你需要实现 ConstraintValidator接口,并重写isValid 方法:
import javax.validation.ConstraintValidator; import javax.validation.ConstraintValidatorContext; import java.util.HashSet; publicclass RegionValidator implements ConstraintValidator<Region, String> { @Override public boolean isValid(String value, ConstraintValidatorContext context) { HashSet<Object> regions = new HashSet<>(); regions.add("China"); regions.add("China-Taiwan"); regions.add("China-HongKong"); return regions.contains(value); } }
现在你就可以使用这个注解:
@Region
private String region;
案例二:校验电话号码
校验我们的电话号码是否合法,这个可以通过正则表达式来做,相关的正则表达式都可以在网上搜到,你甚至可以搜索到针对特定运营商电话号码段的正则表达式。
PhoneNumber.java:
import javax.validation.Constraint;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
importstatic java.lang.annotation.ElementType.FIELD;
importstatic java.lang.annotation.ElementType.PARAMETER;
importstatic java.lang.annotation.RetentionPolicy.RUNTIME;
@Documented
@Constraint(validatedBy = PhoneNumberValidator.class)
@Target({FIELD, PARAMETER})
@Retention(RUNTIME)
public@interface PhoneNumber {
String message() default "Invalid phone number";
Class[] groups() default {};
Class[] payload() default {};
}
PhoneNumberValidator.java:
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
publicclass PhoneNumberValidator implements ConstraintValidator<PhoneNumber,String> {
@Override
public boolean isValid(String phoneField, ConstraintValidatorContext context) {
if (phoneField == null) {
// can be null
return true;
}
return phoneField.matches("^1(3[0-9]|4[57]|5[0-35-9]|8[0-9]|70)\\d{8}$") && phoneField.length() > 8 && phoneField.length() < 14;
}
}
现在就可以使用这个注解了。
@PhoneNumber(message = "phoneNumber 格式不正确")
@NotNull(message = "phoneNumber 不能为空")
private String phoneNumber;
使用验证组
某些场景下我们需要使用到验证组,这样说可能不太清楚,说简单点就是对对象操作的不同方法有不同的验证规则,示例如下(这个就我目前经历的项目来说使用的比较少,因为本身这个在代码层面理解起来是比较麻烦的,然后写起来也比较麻烦)。
先创建两个接口
public interface AddPersonGroup {
}
public interface DeletePersonGroup {
}
接着使用如下:
@NotNull(groups = DeletePersonGroup.class)
@Null(groups = AddPersonGroup.class)
private String group;
@Service
@Validated
publicclass PersonService {
public void validatePerson(@Valid Person person) {
// do something
}
@Validated(AddPersonGroup.class)
public void validatePersonGroupForAdd(@Valid Person person) {
// do something
}
@Validated(DeletePersonGroup.class)
public void validatePersonGroupForDelete(@Valid Person person) {
// do something
}
}
测试:
@Test(expected = ConstraintViolationException.class)
public void should_check_person_with_groups() {
Person person = new Person();
person.setSex("Man22");
person.setClassId("82938390");
person.setEmail("SnailClimb");
person.setGroup("group1");
service.validatePersonGroupForAdd(person);
}
@Test(expected = ConstraintViolationException.class)
public void should_check_person_with_groups2() {
Person person = new Person();
person.setSex("Man22");
person.setClassId("82938390");
person.setEmail("SnailClimb");
service.validatePersonGroupForDelete(person);
}
使用验证组这种方式的时候一定要小心,这是一种反模式,还会造成代码逻辑性变差。
@NotNull vs @Column(nullable = false)(重要)
在使用 JPA 操作数据的时候会经常碰到 @Column(nullable = false) 这种类型的约束,那么它和 @NotNull 有何区别呢?搞清楚这个还是很重要的!
- @NotNull是 JSR 303 Bean验证注解,它与数据库约束本身无关。
- @Column(nullable = false) : 是JPA声明列为非空的方法。
总结来说就是即前者用于验证,而后者则用于指示数据库创建表的时候对表的约束。
SpringBoot 几种常见处理异常的方式
这里讲一下spring boot 3种常见的处理异常的方式。
一.使用 @RestControllerAdvice 和 @ExceptionHandler 处理全局异常
Spring 项目必备的全局处理 Controller 层异常。
这是目前很常用的一种方式,非常推荐。测试中用到了 Junit 5,如果你新建项目验证下面的代码的话,记得添加上相关依赖。
1. 新建异常信息实体类
非必要的类,主要用于包装异常信息。
/**
* @author song
*/
public class ErrorResponse {
private String message;
private String errorTypeName;
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 ,所以如果你需要自定义异常类型的话直接集成这个类就可以了。
/**
* @author song
* 自定义异常类型
*/
public class ResourceNotFoundException extends RuntimeException {
private String message;
public ResourceNotFoundException() {
super();
}
public ResourceNotFoundException(String message) {
super(message);
this.message = message;
}
public ResourceNotFoundException(Throwable cause){
super(cause);
}
public ResourceNotFoundException(String message,Throwable cause){
super(message,cause);
}
@Override
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
3. 新建异常处理类
相关的3个注解如下:
- @ControllerAdvice :注解定义全局异常处理类
- @RestControllerAdvice :注解定义全局异常处理类
- @ExceptionHandler :注解声明异常处理方法
ControllerAdvice 和 RestControllerAdvice 区别也就是和 Controller 与 RestController 一样。现在基本都是Spring boot 和rest风格开发用的是@RestControllerAdvice。还可以用assignableTypes指定特定的 Controller 类
/**
* @author song
*/
@RestControllerAdvice
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模拟抛出异常
/**
* @author song
*/
@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(curl -i -s -X GET url),服务端返回的 JSON 数据如下:
{
"message": "Sorry, the resourse not found!",
"errorTypeName": "com.saijia.common.exception.ResourceNotFoundException"
}
5. 编写测试类
MockMvc 由org.springframework.boot.test包提供,实现了对Http请求的模拟,一般用于我们测试 controller 层。
/**
* @author song
*/
@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!"));
}
}
二.使用 @ExceptionHandler 处理 Controller 级别的异常
上面也说了使用@ControllerAdvice注解 可以通过 assignableTypes指定特定的类,让异常处理类只处理特定类抛出的异常。实际上这种方式现在使用的比较少了,这里还是举一下例子。
/**
* @author shuang.kou
*/
@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;
}
}
controller模拟抛出异常
/**
* @author song
*/
@RestController
@RequestMapping("/api")
public class ExceptionController {
@GetMapping("/illegalArgumentException")
public void throwException() {
throw new IllegalArgumentException();
}
@GetMapping("/resourceNotFoundException")
public void throwException2() {
throw new ResourceNotFoundException();
}
}
三.ResponseStatusException
研究 ResponseStatusException 我们先来看看,通过 ResponseStatus注解简单处理异常的方法(将异常映射为状态码)。
@ResponseStatus(code = HttpStatus.NOT_FOUND)
public class ResourseNotFoundException2 extends RuntimeException {
public ResourseNotFoundException2() {
}
public ResourseNotFoundException2(String message) {
super(message);
}
}
Controller 层抛出异常测试
@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 数据如下:
{
"timestamp": "2020-12-08T07:11:43.744+0000",
"status": 404,
"error": "Not Found",
"message": "Sorry, the resourse not found!",
"path": "/api/resourceNotFoundException2"
}
通过 ResponseStatus注解简单处理异常的方法的好处是比较简单,但是一般我们不会这样做,通过ResponseStatusException会更加方便,可以避免我们额外的异常类。
@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 实现的效果一样:
{
"timestamp": "2020-12-08T07:20:53.017+0000",
"status": 404,
"error": "Not Found",
"message": "Sorry, the resourse not found!",
"path": "/api/resourceNotFoundException2"
}
ResponseStatusException 提供了三个构造方法:
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 :抛出的异常
JPA注解和简单操作
这里小宋讲解一下Spring Data Jpa的相关知识。
1.相关依赖
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
2.配置数据库连接信息和JPA配置
下面的配置中要先去单独说一下 spring.jpa.hibernate.ddl-auto=create这个配置选项。
该属性常用的选项包含4种:
- create:每次重新启动项目都会重新创建新表结构,会导致数据丢失
- create-drop:每次启动项目创建表结构,关闭项目删除表结构
- update:每次启动项目会更新表结构
- validate:验证表结构,不对数据库进行任何更改
一定要不要在生产环境使用 ddl 自动生成表结构,一般推荐手写 SQL 语句配合 Flyway 来做这些事情。
spring.datasource.url=jdbc:mysql://localhost:3306/springboot_jpa?useSSL=false&serverTimezone=CTT
spring.datasource.username=root
spring.datasource.password=123456
# 打印出 sql 语句
spring.jpa.show-sql=true
spring.jpa.hibernate.ddl-auto=create
spring.jpa.open-in-view=false
# 创建的表的 ENGINE 为 InnoDB
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQL55Dialect
3.实体类
我们为这个类添加 @Entity 注解代表它是数据库持久化类,并配置主键 id。
import lombok.Data;
import lombok.NoArgsConstructor;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
@Entity
@Data
@NoArgsConstructor
publicclass Person {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(unique = true)
private String name;
private Integer age;
public Person(String name, Integer age) {
this.name = name;
this.age = age;
}
}
3.1 创建表
@Entity:声明一个类对应一个数据库实体。
@Table:设置表名
@NoArgsConstructor:无参构造方法
@Entity
@Data
@NoArgsConstructor
publicclass Person {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(unique = true)
private String name;
private Integer age;
public Person(String name, Integer age) {
this.name = name;
this.age = age;
}
3.2 创建主键
@Id:声明一个字段为主键。
使用@Id声明之后,我们还需要定义主键的生成策略。我们可以使用 @GeneratedValue 指定主键生成策略。
1.通过 @GeneratedValue直接使用 JPA 内置提供的四种主键生成策略来指定主键生成策略。
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
JPA使用枚举定义了4种常见的主键生成策略:
public enum GenerationType {
/**
* 使用一个特定的数据库表格来保存主键
* 持久化引擎通过关系数据库的一张特定的表格来生成主键,
*/
TABLE,
/**
*在某些数据库中,不支持主键自增长,比如Oracle、PostgreSQL其提供了一种叫做"序列(sequence)"的机制生成主键
*/
SEQUENCE,
/**
* 主键自增长
*/
IDENTITY,
/**
*把主键生成策略交给持久化引擎(persistence engine),
*持久化引擎会根据数据库在以上三种主键生成 策略中选择其中一种
*/
AUTO
}
@GeneratedValue注解默认使用的策略是GenerationType.AUTO
public @interface GeneratedValue {
GenerationType strategy() default AUTO;
String generator() default "";
}
一般使用 MySQL 数据库的话,使用GenerationType.IDENTITY策略比较普遍一点(分布式系统的话需要另外考虑使用分布式 ID)。
2.通过 @GenericGenerator声明一个主键策略,然后 @GeneratedValue使用这个策略
@Id
@GeneratedValue(generator = "IdentityIdGenerator")
@GenericGenerator(name = "IdentityIdGenerator", strategy = "identity")
private Long id;
等同于:
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
jpa 提供的主键生成策略有如下一些:
public class DefaultIdentifierGeneratorFactory
implements MutableIdentifierGeneratorFactory, Serializable, ServiceRegistryAwareService {
@SuppressWarnings("deprecation")
public DefaultIdentifierGeneratorFactory() {
register( "uuid2", UUIDGenerator.class );
register( "guid", GUIDGenerator.class ); // can be done with UUIDGenerator + strategy
register( "uuid", UUIDHexGenerator.class ); // "deprecated" for new use
register( "uuid.hex", UUIDHexGenerator.class ); // uuid.hex is deprecated
register( "assigned", Assigned.class );
register( "identity", IdentityGenerator.class );
register( "select", SelectGenerator.class );
register( "sequence", SequenceStyleGenerator.class );
register( "seqhilo", SequenceHiLoGenerator.class );
register( "increment", IncrementGenerator.class );
register( "foreign", ForeignGenerator.class );
register( "sequence-identity", SequenceIdentityGenerator.class );
register( "enhanced-sequence", SequenceStyleGenerator.class );
register( "enhanced-table", TableGenerator.class );
}
public void register(String strategy, Class generatorClass) {
LOG.debugf( "Registering IdentifierGenerator strategy [%s] -> [%s]", strategy, generatorClass.getName() );
final Class previous = generatorStrategyToClassNameMap.put( strategy, generatorClass );
if ( previous != null ) {
LOG.debugf( " - overriding [%s]", previous.getName() );
}
}
}
3.3 设置字段类型
@Column:声明字段。
示例:
设置属性 userName 对应的数据库字段名为 user_name,长度为 32,非空
@Column(name = "user_name", nullable = false, length=32)
private String userName;
设置字段类型并且加默认值,这个还是挺常用的。
@Column(columnDefinition = "tinyint(1) default 1")
private Boolean enabled;
3.4 指定不持久化特定字段
@Transient :声明不需要与数据库映射的字段,在保存的时候不需要保存进数据库 。
如果我们想让secrect 这个字段不被持久化,可以使用 @Transient关键字声明。
@Entity(name="USER")
public class User {
......
@Transient
private String secrect; // not persistent because of @Transient
}
除了 @Transient关键字声明, 还可以采用下面3种方法:
static String secrect; // not persistent because of static
final String secrect = “Satish”; // not persistent because of final
transient String secrect; // not persistent because of transient
但是一般注解的方式使用的比较多
3.5 声明大字段
@Lob:声明某个字段为大字段。
@Lob
private String content;
@Lob
//指定 Lob 类型数据的获取策略, FetchType.EAGER 表示非延迟 加载,而 FetchType. LAZY 表示延迟加载 ;
@Basic(fetch = FetchType.EAGER)
//columnDefinition 属性指定数据表对应的 Lob 字段类型
@Column(name = "content", columnDefinition = "LONGTEXT NOT NULL")
private String content;
3.6 创建枚举类型的字段
public enum Gender {
MALE("男性"),
FEMALE("女性");
private String value;
Gender(String str){
value=str;
}
}
可以通过@Enumerated注解去使用枚举类型的字段
@Entity
@Table(name = "role")
public class Role {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private String description;
@Enumerated(EnumType.STRING)
private Gender gender;
省略getter/setter......
}
数据库里面对应存储的是 MAIL/FEMAIL。
如何验证我们已经成功,运行项目后,查看控制台是否打印出创建表的 sql 语句,并且数据库中表真的被创建出来的话,说明你成功了。
控制台打印出来的 sql 语句类似下面这样:
droptableifexists person
CREATETABLE`person` (
`id`bigint(20) NOTNULL AUTO_INCREMENT,
`age`int(11) DEFAULTNULL,
`name`varchar(255) DEFAULTNULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDBDEFAULTCHARSET=utf8;
altertable person addconstraint UK_p0wr4vfyr2lyifm8avi67mqw5 unique (name)
4.创建操作数据库的 Repository 接口
@Repository
public interface PersonRepository extends JpaRepository<Person, Long> {
}
首先这个接口加了 @Repository 注解,代表它和数据库操作有关。另外,它继承了 JpaRepository<Person, Long>接口,而JpaRepository<Person, Long>源码如下:
@NoRepositoryBean
publicinterface JpaRepository<T, ID> extends PagingAndSortingRepository<T, ID>, QueryByExampleExecutor<T> {
List<T> findAll();
List<T> findAll(Sort var1);
List<T> findAllById(Iterable<ID> var1);
<S extends T> List<S> saveAll(Iterable<S> var1);
void flush();
<S extends T> S saveAndFlush(S var1);
void deleteInBatch(Iterable<T> var1);
void deleteAllInBatch();
T getOne(ID var1);
<S extends T> List<S> findAll(Example<S> var1);
<S extends T> List<S> findAll(Example<S> var1, Sort var2);
}
所以当我们继承了JpaRepository<T, ID> ,也就可以去调用 JPA 为我们提供好的增删改查、分页查询以及根据条件查询等方法。
4.1 JPA自带方法用例
4.1.1 增删改查
- 增(保存用户到数据库)
Person person = new Person("SnailClimb", 23);
personRepository.save(person);
save()方法对应 sql 语句是: insert into person (age, name) values (23,"snailclimb")
- 查(根据id查找用户)
Optional<Person> personOptional = personRepository.findById(id);
findById()方法对应 sql 语句就是:select * from person p where p.id = id
- 删(根据id删除用户)
personRepository.deleteById(id);
deleteById()方法对应 sql 语句就是:delete from person where id=id
- 改(根据id更新用户信息)
更新操作也是通过save()方法实现。
Person person = new Person("SnailClimb", 23);
Person savedPerson = personRepository.save(person);
// 更新 person 对象的姓名
savedPerson.setName("UpdatedName");
personRepository.save(savedPerson);
这里是先新增再去更新,也可以先查出一个用户的信息再去更新。
这里最后的save()方法相当于 sql 语句:update person set name="UpdatedName" where id=id
4.1.2 条件查询
下面这些方法是根据 JPA 提供的语法自定义的,你需要将下面这些方法写到 PersonRepository 中。
假如我们想要根据 Name 来查找 Person ,你可以这样:
Optional<Person> findByName(String name);
如果你想要找到年龄大于某个值的人,你可以这样:
List<Person> findByAgeGreaterThan(int age);
4.2 JPA自定义Sql语句
很多时候我们自定义 sql 语句会非常有用。
根据 name 来查找 Person:
@Query("select p from Person p where p.name = :name")
Optional<Person> findByNameCustomeQuery(@Param("name") String name);
Person 部分属性查询,避免 select *操作:
@Query("select p.name from Person p where p.id = :id")
String findPersonNameById(@Param("id") Long id);
根据 id 更新Person name:
@Modifying
@Transactional
@Query("update Person p set p.name = ?1 where p.id = ?2")
void updatePersonNameById(String name, Long id);
根据 id 删除Person name:
@Modifying
@Transactional
@Query("delete from Person where id = ?1")
void deletePersonNameById(Long id);
4.3 创建异步方法
如果我们需要创建异步方法的话,也比较方便。
异步方法在调用时立即返回,然后会被提交给TaskExecutor执行。当然你也可以选择得出结果后才返回给客户端。
@Async
Future<User> findByName(String name);
@Async
CompletableFuture<User> findByName(String name);
5.测试
@SpringBootTest
@RunWith(SpringRunner.class)
publicclass PersonRepositoryTest {
@Autowired
private PersonRepository personRepository;
private Long id;
/**
* 保存person到数据库
*/
@Before
public void setUp() {
assertNotNull(personRepository);
Person person = new Person("SnailClimb", 23);
Person savedPerson = personRepository.saveAndFlush(person);// 更新 person 对象的姓名
savedPerson.setName("UpdatedName");
personRepository.save(savedPerson);
id = savedPerson.getId();
}
/**
* 使用 JPA 自带的方法查找 person
*/
@Test
public void should_get_person() {
Optional<Person> personOptional = personRepository.findById(id);
assertTrue(personOptional.isPresent());
assertEquals("SnailClimb", personOptional.get().getName());
assertEquals(Integer.valueOf(23), personOptional.get().getAge());
List<Person> personList = personRepository.findByAgeGreaterThan(18);
assertEquals(1, personList.size());
// 清空数据库
personRepository.deleteAll();
}
/**
* 自定义 query sql 查询语句查找 person
*/
@Test
public void should_get_person_use_custom_query() {
// 查找所有字段
Optional<Person> personOptional = personRepository.findByNameCustomeQuery("SnailClimb");
assertTrue(personOptional.isPresent());
assertEquals(Integer.valueOf(23), personOptional.get().getAge());
// 查找部分字段
String personName = personRepository.findPersonNameById(id);
assertEquals("SnailClimb", personName);
System.out.println(id);
// 更新
personRepository.updatePersonNameById("UpdatedName", id);
Optional<Person> updatedName = personRepository.findByNameCustomeQuery("UpdatedName");
assertTrue(updatedName.isPresent());
// 清空数据库
personRepository.deleteAll();
}
}
JPA连表查询和分页
这里我们继续讲JPA如何实现连表和分页,因为JPA可以在repository层自定义sql所以也不难。
1.实体类
创建三个实体类。
@Entity
@Data
@NoArgsConstructor
publicclass Company {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(unique = true)
private String companyName;
private String description;
public Company(String name, String description) {
this.companyName = name;
this.description = description;
}
}
@Entity
@Data
@NoArgsConstructor
@AllArgsConstructor
publicclass School {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(unique = true)
private String name;
private String description;
}
@Entity
@Data
@NoArgsConstructor
publicclass Person {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(unique = true)
private String name;
private Integer age;
private Long schoolId;
private Long companyId;
public Person(String name, Integer age) {
this.name = name;
this.age = age;
}
}
2. 自定义Sql实现连表查询
假如我们当前要通过 person 表的 id 来查询 Person 的话,我们知道 Person 的信息一共分布在Company、School、Person这三张表中,所以,我们如果要把 Person 的信息都查询出来的话是需要进行连表查询的。
首先我们需要创建一个包含我们需要的 Person 信息的 DTO 对象,我们简单第将其命名为 UserDTO,用于保存和传输我们想要的信息。
@Data
@NoArgsConstructor
@Builder(toBuilder = true)
@AllArgsConstructor
publicclass UserDTO {
private String name;
privateint age;
private String companyName;
private String schoolName;
}
写一个方法查询Person的基本信息
/**
* 连表查询
*/
@Query(value = "select new com.saijia.modules.live.entity.UserDTO(p.name,p.age,c.companyName,s.name) " +
"from Person p left join Company c on p.companyId=c.id " +
"left join School s on p.schoolId=s.id " +
"where p.id=:personId")
Optional<UserDTO> getUserInformation(@Param("personId") Long personId);
2. 自定义 SQL 语句连表查询并实现分页操作
查询当前所有的人员信息并实现分页,可以按照下面这种方式.为了实现分页,我们在@Query注解中还添加了 countQuery 属性
@Query(value = "select new com.saijia.modules.live.entity.UserDTO(p.name,p.age,c.companyName,s.name) " +
"from Person p left join Company c on p.companyId=c.id " +
"left join School s on p.schoolId=s.id ",
countQuery = "select count(p.id) " +
"from Person p left join Company c on p.companyId=c.id " +
"left join School s on p.schoolId=s.id ")
Page<UserDTO> getUserInformationList(Pageable pageable);
使用方法
//分页选项
PageRequest pageRequest = PageRequest.of(0, 3, Sort.Direction.DESC, "age");
Page<UserDTO> userInformationList = personRepository.getUserInformationList(pageRequest);
//查询结果总数
System.out.println(userInformationList.getTotalElements());// 6
//按照当前分页大小,总页数
System.out.println(userInformationList.getTotalPages());// 2
System.out.println(userInformationList.getContent());
3. IN的操作查询
在 sql 语句中加入我们需要筛选出符合几个条件中的一个的情况下,可以使用 IN 查询,对应到 JPA 中也非常简单。比如下面的方法就实现了,根据名字过滤需要的人员信息。
@Query(value = "select new com.saijia.modules.live.entity.UserDTO(p.name,p.age,c.companyName,s.name) " +
"from Person p left join Company c on p.companyId=c.id " +
"left join School s on p.schoolId=s.id " +
"where p.name IN :peopleList")
List<UserDTO> filterUserInfo(List peopleList);
实际使用:
List<String> personList=new ArrayList<>(Arrays.asList("person1","person2"));
List<UserDTO> userDTOS = personRepository.filterUserInfo(personList);
4. BETWEEN操作查询
查询满足某个范围的值。比如下面的方法就实现查询满足某个年龄范围的人员的信息。
@Query(value = "select new com.saijia.modules.live.entity.UserDTO(p.name,p.age,c.companyName,s.name) " +
"from Person p left join Company c on p.companyId=c.id " +
"left join School s on p.schoolId=s.id " +
"where p.age between :small and :big")
List<UserDTO> filterUserInfoByAge(int small,int big);
实际使用:
List<UserDTO> userDTOS = personRepository.filterUserInfoByAge(19,20);
5. 测试
@SpringBootTest
@RunWith(SpringRunner.class)
publicclass PersonRepositoryTest2 {
@Autowired
private PersonRepository personRepository;
@Sql(scripts = {"classpath:/init.sql"})
@Test
public void find_person_age_older_than_18() {
List<Person> personList = personRepository.findByAgeGreaterThan(18);
assertEquals(1, personList.size());
}
@Sql(scripts = {"classpath:/init.sql"})
@Test
public void should_get_user_info() {
Optional<UserDTO> userInformation = personRepository.getUserInformation(1L);
System.out.println(userInformation.get().toString());
}
@Sql(scripts = {"classpath:/init.sql"})
@Test
public void should_get_user_info_list() {
PageRequest pageRequest = PageRequest.of(0, 3, Sort.Direction.DESC, "age");
Page<UserDTO> userInformationList = personRepository.getUserInformationList(pageRequest);
//查询结果总数
System.out.println(userInformationList.getTotalElements());// 6
//按照当前分页大小,总页数
System.out.println(userInformationList.getTotalPages());// 2
System.out.println(userInformationList.getContent());
}
@Sql(scripts = {"classpath:/init.sql"})
@Test
public void should_filter_user_info() {
List<String> personList=new ArrayList<>(Arrays.asList("person1","person2"));
List<UserDTO> userDTOS = personRepository.filterUserInfo(personList);
System.out.println(userDTOS);
}
@Sql(scripts = {"classpath:/init.sql"})
@Test
public void should_filter_user_info_by_age() {
List<UserDTO> userDTOS = personRepository.filterUserInfoByAge(19,20);
System.out.println(userDTOS);
}
}
事务@Transactional注解使用详解
日常编码中,当我们要使用事务的时候,只需要在要开启事务的方法上加上@Transactional注解即可。
@Transactional(rollbackFor = Exception.class)
public void delete() {
......
}
1.@Transactional 的作用范围
@Transactional 注解一般用在可以作用在类或者方法上。
-
方法 :推荐将注解使用于方法上,不过需要注意的是:该注解只能应用到 public 方法上,否则不生效。
-
类 :如果这个注解使用在类上的话,表明该注解对该类中所有的 public 方法都生效,所有该类的public 方法都配置相同的事务属性信息。
-
接口 :不推荐在接口上使用。
但是当类配置了@Transactional,方法也配置了@Transactional,方法的事务会覆盖类的事务配置信息。
2.@Transactional 的常用配置参数
首先我们来看一下@Transactional的源码,如下(里面包含了基本事务属性的配置):
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface Transactional {
@AliasFor("transactionManager")
String value() default "";
@AliasFor("value")
String transactionManager() default "";
Propagation propagation() default Propagation.REQUIRED;
Isolation isolation() default Isolation.DEFAULT;
int timeout() default TransactionDefinition.TIMEOUT_DEFAULT;
boolean readOnly() default false;
Class<? extends Throwable>[] rollbackFor() default {};
String[] rollbackForClassName() default {};
Class<? extends Throwable>[] noRollbackFor() default {};
String[] noRollbackForClassName() default {};
}
@Transactional 的常用配置参数总结(列5 个我平时比较常用的):
| 属性名 | 说明 |
|---|---|
| propagation | 事务的传播行为,默认值为 Propagation.REQUIRED |
| isolation | 事务的隔离级别,默认值采用 Isolation.DEFAULT |
| timeout | 事务的超时时间,默认值为-1(不会超时)。如果超过该时间限制但事务还没有完成,则自动回滚事务。 |
| readOnly | 指定事务是否为只读事务,默认值为 false。如果要去忽略那些不需要事务的方法,比如读取数据,可以设置 read-only 为 true。 |
| rollbackFor | 用于指定能够触发事务回滚的异常类型,并且可以指定多个异常类型。 |
这里还是说一下propagation传播行为:
- Propagation.REQUIRED:如果当前存在事务,则加入该事务,如果当前不存在事务,则创建一个新的事务。( 也就是说如果A方法和B方法都添加了注解,在默认传播模式下,A方法内部调用B方法,会把两个方法的事务合并为一个事务 )
- Propagation.SUPPORTS:如果当前存在事务,则加入该事务;如果当前不存在事务,则以非事务的方式继续运行。
- Propagation.MANDATORY:如果当前存在事务,则加入该事务;如果当前不存在事务,则抛出异常。
- Propagation.REQUIRES_NEW:重新创建一个新的事务,如果当前存在事务,暂停当前的事务。( 当类A中的 a 方法用默认Propagation.REQUIRED模式,类B中的 b方法加上采用 Propagation.REQUIRES_NEW模式,然后在 a 方法中调用 b方法操作数据库,然而 a方法抛出异常后,b方法并没有进行回滚,因为Propagation.REQUIRES_NEW会暂停 a方法的事务 )
- Propagation.NOT_SUPPORTED:以非事务的方式运行,如果当前存在事务,暂停当前的事务。
- Propagation.NEVER:以非事务的方式运行,如果当前存在事务,则抛出异常。
- Propagation.NESTED :和 Propagation.REQUIRED 效果一样。
isolation 事务的隔离级别:
- TransactionDefinition.ISOLATION_DEFAULT: 使用后端数据库默认的隔离级别,Mysql 默认采用的 REPEATABLE_READ隔离级别 Oracle 默认采用的 READ_COMMITTED隔离级别.
- TransactionDefinition.ISOLATION_READ_UNCOMMITTED: 最低的隔离级别,允许读取尚未提交的数据变更,可能会导致脏读、幻读或不可重复读
- TransactionDefinition.ISOLATION_READ_COMMITTED: 允许读取并发事务已经提交的数据,可以阻止脏读,但是幻读或不可重复读仍有可能发生
- TransactionDefinition.ISOLATION_REPEATABLE_READ: 对同一字段的多次读取结果都是一致的,除非数据是被本身事务自己所修改,可以阻止脏读和不可重复读,但幻读仍有可能发生。
- TransactionDefinition.ISOLATION_SERIALIZABLE: 最高的隔离级别,完全服从ACID的隔离级别。所有的事务依次逐个执行,这样事务之间就完全不可能产生干扰,也就是说,该级别可以防止脏读、不可重复读以及幻读。但是这将严重影响程序的性能。通常情况下也不会用到该级别。
对于事务一些相关的基础知识还不太了解的同学可以去看一下 Spring系列之事务博客的相关知识。
在开始的时候我举了一个方法使用@Transactional注解的例子,就有用到rollbackFor配置参数,我们知道 Exception 分为运行时异常 RuntimeException 和非运行时异常。在@Transactional注解中如果不配置rollbackFor属性,那么事物只会在遇到RuntimeException的时候才会回滚,加上rollbackFor=Exception.class,可以让事物在遇到非运行时异常时也回滚。
3.@Transactional 事务注解原理
我们在面试中问到 AOP 的时候可能会被问的一个问题。
基本都知道,@Transactional 的工作机制是基于 AOP 实现的,AOP 又是使用动态代理实现的。如果目标对象实现了接口,默认情况下会采用 JDK 的动态代理,如果目标对象没有实现了接口,会使用 CGLIB 动态代理。
多说一下,createAopProxy() 方法 决定了是使用 JDK 还是 Cglib 来做动态代理,源码如下:
public class DefaultAopProxyFactory implements AopProxyFactory, Serializable {
@Override
public AopProxy createAopProxy(AdvisedSupport config) throws AopConfigException {
if (config.isOptimize() || config.isProxyTargetClass() || hasNoUserSuppliedProxyInterfaces(config)) {
Class<?> targetClass = config.getTargetClass();
if (targetClass == null) {
throw new AopConfigException("TargetSource cannot determine target class: " +
"Either an interface or a target is required for proxy creation.");
}
if (targetClass.isInterface() || Proxy.isProxyClass(targetClass)) {
return new JdkDynamicAopProxy(config);
}
return new ObjenesisCglibAopProxy(config);
}
else {
return new JdkDynamicAopProxy(config);
}
}
.......
}
如果一个类或者一个类中的 public 方法上被标注@Transactional 注解的话,Spring 容器就会在启动的时候为其创建一个代理类,在调用被@Transactional 注解的 public 方法的时候,实际调用的是,TransactionInterceptor 类中的 invoke()方法。这个方法的作用就是在目标方法之前开启事务,方法执行过程中如果遇到异常的时候回滚事务,方法调用完成之后提交事务。
TransactionInterceptor 类中的 invoke()方法内部实际调用的是 TransactionAspectSupport 类的 invokeWithinTransaction()方法。
由于新版本的 Spring 对这部分重写很大,而且用到了很多响应式编程的知识,这里就不列源码了。
4.Spring AOP自调用问题
若同一类中的其他没有 @Transactional 注解的方法内部调用有 @Transactional 注解的方法,有@Transactional 注解的方法的事务会失效。
这是由于Spring AOP代理的原因造成的,因为只有当 @Transactional 注解的方法在类以外被调用的时候,Spring 事务管理才生效。
MyService 类中的method1()调用method2()就会导致method2()的事务失效。
@Service
public class MyService {
private void method1() {
method2();
//......
}
@Transactional
public void method2() {
//......
}
}
解决办法: 避免同一类中自调用或者使用 AspectJ 取代 Spring AOP 代理。
5. @Transactional 的使用注意事项
- @Transactional 注解只有作用到 public 方法上事务才生效,不推荐在接口上使用;
- 避免同一个类中调用 @Transactional 注解的方法,这样会导致事务失效;
- 正确的设置 @Transactional 的 rollbackFor 和 propagation 属性,否则事务可能会回滚失败
6.@Transactional 失效场景
我们结合具体的代码分析一下,哪些场景下@Transactional 注解会失效
6.1 @Transactional 应用在非public修饰的方法上
如果Transactional注解应用在非public 修饰的方法上,Transactional将会失效。

protected TransactionAttribute computeTransactionAttribute(Method method,
Class<?> targetClass) {
// Don't allow no-public methods as required.
if (allowPublicMethodsOnly() && !Modifier.isPublic(method.getModifiers())) {
returnnull;
}
此方法会检查目标方法的修饰符是否为 public,不是 public则不会获取@Transactional 的属性配置信息。
这里注意一下:protected、private 修饰的方法上使用 @Transactional 注解,虽然事务无效,但不会有任何报错,这是我们容易犯错的一点。
6.2 @Transactional 注解属性 propagation 设置错误
这种失效是由于配置错误,若是错误的配置以下三种 propagation,事务将不会发生回滚。
-
TransactionDefinition.PROPAGATION_SUPPORTS:如果当前存在事务,则加入该事务;如果当前没有事务,则以非事务的方式继续运行。
-
TransactionDefinition.PROPAGATION_NOT_SUPPORTED:以非事务方式运行,如果当前存在事务,则把当前事务挂起。
-
TransactionDefinition.PROPAGATION_NEVER:以非事务方式运行,如果当前存在事务,则抛出异常。
6.3 @Transactional 注解属性 rollbackFor 设置错误
rollbackFor 可以指定能够触发事务回滚的异常类型。Spring默认抛出了未检查unchecked异常(继承自 RuntimeException 的异常)或者 Error才回滚事务;其他异常不会触发回滚事务。如果在事务中抛出其他类型的异常,但却期望 Spring 能够回滚事务,就需要指定 rollbackFor属性。

// 希望自定义的异常可以进行回滚
@Transactional(propagation= Propagation.REQUIRED,rollbackFor= MyException.class
若在目标方法中抛出的异常是 rollbackFor 指定的异常的子类,事务同样会回滚。Spring 源码如下:
private int getDepth(Class<?> exceptionClass, int depth) {
if (exceptionClass.getName().contains(this.exceptionName)) {
// Found it!
return depth;
}
// If we've gone as far as we can go and haven't found it...
if (exceptionClass == Throwable.class) {
return -1;
}
return getDepth(exceptionClass.getSuperclass(), depth + 1);
}
6.4 同一个类中方法调用,导致@Transactional失效
开发中避免不了会对同一个类里面的方法调用,比如有一个类Test,它的一个方法A,A再调用本类的方法B(不论方法B是用public还是private修饰),但方法A没有声明注解事务,而B方法有。则外部调用方法A之后,方法B的事务是不会起作用的。这也是经常犯错误的一个地方。
那为啥会出现这种情况?其实这还是由于使用Spring AOP代理造成的,因为只有当事务方法被当前类以外的代码调用时,才会由Spring生成的代理对象来管理。
//@Transactional
@GetMapping("/test")
private Integer A() throws Exception {
CityInfoDict cityInfoDict = new CityInfoDict();
cityInfoDict.setCityName("2");
/**
* B 插入字段为 3的数据
*/
this.insertB();
/**
* A 插入字段为 2的数据
*/
int insert = cityInfoDictMapper.insert(cityInfoDict);
return insert;
}
@Transactional()
public Integer insertB() throws Exception {
CityInfoDict cityInfoDict = new CityInfoDict();
cityInfoDict.setCityName("3");
cityInfoDict.setParentCityId(3);
return cityInfoDictMapper.insert(cityInfoDict);
}
6.5 异常被你的 catch“吃了”导致@Transactional失效
这种情况其实是最常见的 @Transactional 注解失效场景
@Transactional
private Integer A() throws Exception {
int insert = 0;
try {
CityInfoDict cityInfoDict = new CityInfoDict();
cityInfoDict.setCityName("2");
cityInfoDict.setParentCityId(2);
/**
* A 插入字段为 2的数据
*/
insert = cityInfoDictMapper.insert(cityInfoDict);
/**
* B 插入字段为 3的数据
*/
b.insertB();
} catch (Exception e) {
e.printStackTrace();
}
}
如果B方法内部抛了异常,而A方法此时try catch了B方法的异常,那这个事务还能正常回滚吗?
不能回滚。
会抛出异常:
org.springframework.transaction.UnexpectedRollbackException: Transaction rolled back because it has been marked as rollback-only
因为当ServiceB中抛出了一个异常以后,ServiceB标识当前事务需要rollback。但是ServiceA中由于你手动的捕获这个异常并进行处理,ServiceA认为当前事务应该正常commit。此时就出现了前后不一致,也就是因为这样,抛出了前面的UnexpectedRollbackException异常。
spring的事务是在调用业务方法之前开始的,业务方法执行完毕之后才执行commit or rollback,事务是否执行取决于是否抛出runtime异常。如果抛出runtime exception 并在你的业务方法中没有catch到的话,事务会回滚。
在业务方法中一般不需要catch异常,如果非要catch一定要抛出throw new RuntimeException(),或者注解中指定抛异常类型@Transactional(rollbackFor=Exception.class),否则会导致事务失效,数据commit造成数据不一致,所以有些时候 try catch反倒会画蛇添足。
6.6 数据库引擎不支持事务
这种情况出现的概率并不高,事务能否生效,看数据库引擎是否支持事务。常用的MySQL数据库默认使用支持事务的innodb引擎。但是数据库引擎切换成不支持事务的myisam,那事务就从根本上失效了。
json数据处理注解
讲一下关于json数据处理的一些相关注解
1. 过滤json数据
@JsonIgnoreProperties 作用在类上用于过滤掉特定字段不返回或者不解析。
//生成json时将userRoles属性过滤
@JsonIgnoreProperties({"userRoles"})
public class User {
private String userName;
private String fullName;
private String password;
@JsonIgnore
private List<UserRole> userRoles = new ArrayList<>();
}
@JsonIgnore一般用于类的属性上,作用和上面的@JsonIgnoreProperties 一样。
public class User {
private String userName;
private String fullName;
private String password;
//生成json时将userRoles属性过滤
@JsonIgnore
private List<UserRole> userRoles = new ArrayList<>();
}
2. 格式化json数据
@JsonFormat一般用来格式化 json 数据:
@JsonFormat(shape=JsonFormat.Shape.STRING, pattern="yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", timezone="GMT")
private Date date;
3.扁平化对象
@Getter
@Setter
@ToString
public class Account {
@JsonUnwrapped
private Location location;
@JsonUnwrapped
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;
}
}
未扁平化之前:
{
"location": {
"provinceName":"湖北",
"countyName":"武汉"
},
"personInfo": {
"userName": "coder1234",
"fullName": "shaungkou"
}
}
使用@JsonUnwrapped 扁平对象之后:
@Getter
@Setter
@ToString
public class Account {
@JsonUnwrapped
private Location location;
@JsonUnwrapped
private PersonInfo personInfo;
......
}
{
"provinceName":"湖北",
"countyName":"武汉",
"userName": "coder1234",
"fullName": "shaungkou"
}
测试注解
@ActiveProfiles一般作用于测试类上, 用于声明生效的 Spring 配置文件。
@SpringBootTest(webEnvironment = RANDOM_PORT)
@ActiveProfiles("test")
@Slf4j
public abstract class TestBase {
......
}
@Test声明一个方法为测试方法
@Transactional被声明的测试方法的数据会回滚,避免污染测试数据。
@WithMockUser Spring Security 提供的,用来模拟一个真实用户,并且可以赋予权限。
@Test
@Transactional
@WithMockUser(username = "user-id-18163138155", authorities = "ROLE_TEACHER")
void should_import_student_success() throws Exception {
......
}
讲到这里本章对Spring和Spring Boot注解的讲解也就结束了,如果想了解更多知识可以在对应的专栏中看系列文章,谢谢大家的观看,希望能给各位同学带来帮助。如果觉得博主写的还可以的,可以点赞收藏。 😉



浙公网安备 33010602011771号