Spring 必知必会的技术

API 请求参数读取

@RequestParam

用来加载 URL 中?之后的参数

@GetMapping("/notice")
public boolean isNeedToNoticeUser(@RequestParam("email") String email) {
return userNoticeService.isNeedToNoticeUser(email);
}

@RequestBody

用来加载 POST/PUT 请求的复杂请求体

@PostMapping("/status")
public Response jobStatus(@RequestBody @Validated SenderJobDTO jobDTO) {
ConsumerJobVO vo = senderJobService.jobStatus(jobDTO);
return Response.ok().data(vo);
}

@PathVariable

用来加载 URL 路径中的参数

@GetMapping("/user/{id}")
@ResponseBody()
public User findUserById(@PathVariable("id") String id){
return userRepo.findById(id);
}

@MatrixVariable

API 的参数通过;分割。比如:这个请求/books/reviews;isbn=1234;topN=5; 就可以如下面这样,使用@MatrixVariable来加载 URL 中用;分割的参数

@GetMapping("/books/reviews")
@ResponseBody()
public List<BookReview> getBookReviews(
@MatrixVariable String isbn, @MatrixVariable Integer topN) {
return bookReviewsLogic.getTopNReviewsByIsbn(isbn, topN);
}

@RequestHeader

用来加载请求头中的数据

@GetMapping("/user")
@ResponseBody()
public List<User> getUserList(@RequestHeader("Authorization") String authToken) {
return userRepo.findAll();
}

@CookieValue

服务端读取 Cookie 数据

@GetMapping("/user")
@ResponseBody()
public List<User> getUserList(@CookieValue(name = "SessionId") String sessionId) {
return userRepo.findAll();
}

参数配置

常用的有两种方式:@Value 和@ConfigurationProperties

Feature@ConfigurationProperties@Value
Relaxed bindingYesLimited (see note below)
Meta-data supportYesNo
SpEL evaluationNoYes

@Value

首先需要在配置类上增加@Configuration

带默认值的

@Value("${kafka.producer.retry:1}")
private int retry;

使用 Spring Expression Language (SpEL)

@Value("#{'${actuator.send.business.type:eims,item}'.split(',')}")
List<String> specifyType;

@ConfigurationProperties

该方式可以将相应的配置封装在具体类内,对代码组织和封装友好,推进使用这种方式

@ConfigurationProperties(prefix = "my.server")
public class MyServerProperties {
private String name="test";
@NotNull
private String bootstrapServers;
private Host host;
// getters/setters ...
// 嵌入式结构
public static class Host {
private String ip;
private int port;
// getters/setters ...
}
}
my.server:
name: test
bootstrap_servers: xxxxx:9092
host:
ip: xxx
port: 8081

扩展:

为了让 spring 容器可以识别该配置类,有多种方式,这里我推荐在启动类上添加注解@ConfigurationPropertiesScan({"top.trumandu.config"})

配置参数

Spring(relaxed binding )使用一些宽松的绑定属性规则。因此,以下变体都将绑定到 hostName 属性上:

mail.hostName=localhost
mail.hostname=localhost
mail.host_name=localhost
mail.host-name=localhost
mail.HOST_NAME=localhost

为了让 ide 可以识别我们自定义的配置文件,可以添加 spring-boot-configuration-processor,这样就可以自动生成additional-spring-configuration-metadata.json, 进而进行提示。

配置文件优先级

classpath root > classpath/config > current directory > current directory/config/ > current directory/../config

日志

默认使用 Logback,通常我们只用如下配置即可快速使用.

logging:
level:
root: "warn"
org.springframework.web: "debug"
org.hibernate: "error"

当无法满足一些日志场景时,Logback 扩展可以通过在src/main/resources 创建logback-spring.xml

<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<!--<jmxConfigurator/> -->
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>
%d{ISO8601} %-5level [%thread] %logger{0}: %msg%n
</pattern>
</encoder>
</appender>
<appender name="FILE"
class="ch.qos.logback.core.rolling.RollingFileAppender">
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>logs/info.%d{yyyy-MM-dd}.log</fileNamePattern>
<maxHistory>7</maxHistory>
</rollingPolicy>
<encoder>
<pattern>
%d{ISO8601} %-5level [%thread] %logger{0}: %msg%n
</pattern>
</encoder>
</appender>
<appender name="ERROR_FILE"
class="ch.qos.logback.core.rolling.RollingFileAppender">
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>logs/error.%d{yyyy-MM-dd}.log</fileNamePattern>
<maxHistory>7</maxHistory>
</rollingPolicy>
<encoder>
<pattern>
%d{ISO8601} %-5level [%thread] %logger{0}: %msg%n
</pattern>
</encoder>
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>error</level>
</filter>
</appender>
<logger name="top.trumandu" level="info" />
<logger name="org.apache" level="error" />
<logger name="org.I0Itec.zkclient" level="error" />
<root level="info">
<appender-ref ref="STDOUT" />
<appender-ref ref="FILE" />
<appender-ref ref="ERROR_FILE" />
</root>
</configuration>

Spring 容器启动以后运行任务

可以实现ApplicationRunnerCommandLineRunner 接口

@Component
public class MyCommandLineRunner implements CommandLineRunner {
@Override
public void run(String... args) {
// Do something...
}
}

优先级可以通过@Ordered 实现

定时 job

首先启动定时注解配置

@EnableScheduling

通过以下代码可以快速实现定时 job

@Component
public class ManualGcJob {
// 每两秒运行一次
@Scheduled(cron = "0/2 * * * * ?")
public void scheduleJvmGc(){
System.out.println("manual gc");
}
}

不仅通过 corn 表达式,也可以通过 fixedDelay 和 fixedRate 配置

支持配置文件配置参数

@Scheduled(fixedDelayString = "${category.fixed.delay:300000}", initialDelayString = "${category.fixed.delay:300000}")
public void updateCategory() {
System.out.println("update category");
}

程序硬编码指定

@Scheduled(fixedDelay = 24 * 60 * 60 * 1000, initialDelay = 24 * 60 * 60 * 1000)
public void scheduleJvmGc() {
System.out.println("manual gc");
}

fixedDelay 和 fixedRate 区别:fixedDelay 需要等上一个任务完成后,再延迟相应时间才运行。

spring boot scheduling 相关配置如下:

spring:
task:
scheduling:
thread-name-prefix: "scheduling-"
pool:
size: 2

springboot 默认定时调度线程池只有一个,强烈建议使用的时候更新该 pool.size 大小

RestTemplate

如果使用 RestTemplate,强烈建议配置超时时间,编码等配置

@Configuration
public class RestTemplateConfig {
/**
* 第三方请求要求的默认编码
*/
private final Charset thirdRequest = StandardCharsets.UTF_8;
@Bean
public RestTemplate restTemplate(ClientHttpRequestFactory factory) {
RestTemplate restTemplate = new RestTemplate(factory);
// 处理请求中文乱码问题
List<HttpMessageConverter<?>> messageConverters = restTemplate.getMessageConverters();
for (HttpMessageConverter<?> messageConverter : messageConverters) {
if (messageConverter instanceof StringHttpMessageConverter) {
((StringHttpMessageConverter) messageConverter).setDefaultCharset(thirdRequest);
}
if (messageConverter instanceof MappingJackson2HttpMessageConverter) {
((MappingJackson2HttpMessageConverter) messageConverter).setDefaultCharset(thirdRequest);
}
if (messageConverter instanceof AllEncompassingFormHttpMessageConverter) {
((AllEncompassingFormHttpMessageConverter) messageConverter).setCharset(thirdRequest);
}
}
return restTemplate;
}
@Bean
public ClientHttpRequestFactory simpleClientHttpRequestFactory() {
SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory();
factory.setConnectTimeout(15000);
factory.setReadTimeout(5000);
return factory;
}
}

@Async 异步执行

在 spring 中可以使用@Async来声明方法异步执行,前提需要启用异步配置@EnableAsync

使用该功能需要注意三个问题:

  1. 同一个类中方法调用不会有异步化,也就是说该功能无效。
//无效代码案例
@Service
public class PurchaseService {
public void purchase(){
sendEmail();
}
@Async
public void sendEmail(){
// Asynchronous code
}
}
//正确代码
@Service
public class EmailService {
@Async
public void sendEmail(){
// Asynchronous code
}
}
@Service
public class PurchaseService {
public void purchase(){
emailService.sendEmail();
}
@Autowired
private EmailService emailService;
}
  1. 调用声明的异步方法无法捕获运行异常,需要特殊处理才可以。
//错误案例
@Service
public class EmailService {
@Async
public void sendEmail() throws Exception{
throw new Exception("Oops, cannot send email!");
}
}
@Service
public class PurchaseService {
@Autowired
private EmailService emailService;
public void purchase(){
try{
emailService.sendEmail();
}catch (Exception e){
System.out.println("Caught exception: " + e.getMessage());
}
}
}
//正确案例
@Service
public class EmailService {
@Async
public Future<String> sendEmail() throws Exception{
throw new Exception("Oops, cannot send email!");
}
}
@Service
public class PurchaseService {
@Autowired
private EmailService emailService;
public void purchase(){
try{
Future<String> future = emailService.sendEmail();
String result = future.get();
System.out.println("Result: " + result);
}catch (Exception e){
System.out.println("Caught exception: " + e.getMessage());
}
}
}
  1. 最佳实践要初始化线程池配置,避免线程阻塞无法运行问题。 优选推荐通过配置文件,这样可以随时修改。
spring:
task:
execution:
thread-name-prefix: email-sync-task
pool:
core-size: 10
max-size: 20
queue-capacity: 10
keep-alive: 10s

或者

@Bean
public ThreadPoolTaskExecutor taskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(2);
executor.setMaxPoolSize(2);
executor.setQueueCapacity(500);
executor.setThreadNamePrefix("MyAsyncThread-");
executor.setRejectedExecutionHandler((r, executor1) -> log.warn("Task rejected, thread pool is full and queue is also full"));
executor.initialize();
return executor;
}
  1. @Async 和@Transcational 会导致事务不一致

Validated 参数校验

@Valid@Validated 都是 Spring 框架中用于启用数据验证的注解

@Valid:只能用于单一的 Bean Validation 校验,无法进行分组校验。@Validated:不仅支持基本校验,还支持通过分组来进行不同场景下的定制化校验。

Valid 使用案例:

public class UserDto {
@NotNull(message = "Name is required")
private String name;
@Email(message = "Invalid email address")
private String email;
}
// 在 Controller 中使用 @Valid
@PostMapping("/user")
public ResponseEntity<String> createUser(@Valid @RequestBody UserDto user, BindingResult result) {
if (result.hasErrors()) {
return ResponseEntity.badRequest().body(result.getFieldErrors().toString());
}
return ResponseEntity.ok("User created successfully");
}

Validated 使用案例:

public class UserDto {
@NotNull(message = "Name is required", groups = Create.class)
private String name;
@NotNull(message = "Email is required", groups = {Create.class, Update.class})
@Email(message = "Invalid email format", groups = {Create.class, Update.class})
private String email;
@NotNull(message = "Password is required for new users", groups = Create.class)
@Size(min = 8, message = "Password must be at least 8 characters", groups = Create.class)
private String password;
public interface Create {}
public interface Update {}
}
// 在 Controller 中使用 @Validated 来指定分组
@PostMapping("/createUser")
public ResponseEntity<String> createUser(@Validated(UserDto.Create.class) @RequestBody UserDto user, BindingResult result) {
if (result.hasErrors()) {
return ResponseEntity.badRequest().body(result.getFieldErrors().toString());
}
return ResponseEntity.ok("User created successfully");
}
@PutMapping("/updateUser")
public ResponseEntity<String> updateUser(@Validated(UserDto.Update.class) @RequestBody UserDto user, BindingResult result) {
if (result.hasErrors()) {
return ResponseEntity.badRequest().body(result.getFieldErrors().toString());
}
return ResponseEntity.ok("User updated successfully");
}

Create 场景:用户在创建时,name 和 password 都必须被验证,而 email 需要验证格式。 Update 场景:在更新时,只验证 email 和 name,且不需要验证 password。

参考

  1. 11 个 Spring 最常用的功能扩展点,手摸手实战
  2. Spring Boot 中的 6 种 API 请求参数读取方式