Spring 开发中的常用技巧、技术,用作备忘和参考,持续更新。
Spring MVC
Spring MVC 相关。
HandlerInterceptor
Spring MVC的处理器拦截器,类似于Servlet开发中的过滤器Filter,用于对请求进行拦截和处理。
想要实现处理器拦截器,需要实现接口 HandlerInterceptor。
public interface HandlerInterceptor {
/**
* 当 HandlerMapping 决定处理对象之后,在Handler对象被 HandlerAdapter 执行前会调用拦截器的这个方法。
* 拦截器可以决定结束整个事务处理。例如可以发送HTTP错误,或者响应一些自定义的内容,从而结束事务。
* 先添加的拦截器先执行。
*
* @param request 当前HTTP请求
* @param response 当前HTTP响应
* @param handler 被选择出进行处理的处理对象
* @return 返回 true 表示交由下一个拦截器或是Handler对象进行后续事务处理。
* 返回 false 表示已经进行了响应,事务结束。这样后续拦截器的方法 postHandle 以及方法 afterCompletion 都不会执行。
*/
boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception;
/**
* 当Handler对象执行完成,但是 DispatcherServlet 在呈现 View 前,将会执行拦截器的这个方法。
* 可以额外添加 Model 通过 ModelAndView 对象参数。
* 后添加的拦截器先执行,最先添加的拦截器将会最后执行。
*
* @param request 当前HTTP请求
* @param response 当前HTTP响应
* @param handler 被选择出进行处理的处理对象
* @param modelAndView Handler 处理完成后返回的 ModelAndView,该参数不能为空
*/
void postHandle(
HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView)
throws Exception;
/**
* 当请求处理完成后,即整个事务完成,已经呈现了视图,就会调用拦截器的这个方法。
* 因此,该方法适合于清理不再需要的资源。
* 后添加的拦截器先执行,最先添加的拦截器将会最后执行。
*
* @param request 当前HTTP请求
* @param response 当前HTTP响应
* @param handler 被选择出进行处理的处理对象
* @param ex 事务处理过程中抛出的异常
*/
void afterCompletion(
HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex)
throws Exception;
}
除此之外,还有专门用于处理请求异步处理的拦截器。
public interface AsyncHandlerInterceptor extends HandlerInterceptor {
/**
* 在处理异步请求时,用于替代方法 postHandle 和方法 afterCompletion 。
* 在 Spring MVC 的处理流程中,如果发现请求是异步请求,调用完 Handler 后将马上返回,不会调用拦截器的方法 postHandle 和方法 afterCompletion 。
* 当开始异步处理后,就会调用拦截器的这个方法,注意这时请求很大可能没处理完。
* 典型的作用是用于清理 ThreadLocal 的数据。调用方法 afterConcurrentHandlingStarted 的线程仍然是请求线程,
* 但是后续处理事务的是另外的线程,原请求线程的 ThreadLocal 数据已用不上。
* @param request the current request
* @param response the current response
* @param handler the handler (or {@link HandlerMethod}) that started async
* execution, for type and/or instance examination
* @throws Exception in case of errors
*/
void afterConcurrentHandlingStarted(HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception;
}
在开发中,最常用的就是继承拦截器的适配器类 HandlerInterceptorAdapter 。
最后需要在 Spring Boot 中进行如下配置。
@Configuration
public class WebConfiguration extends WebMvcConfigurerAdapter {
@Autowired
private AuthInterceptor authInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(authInterceptor).addPathPatterns("/api/**").excludePathPatterns("/api/login");
}
}
HandlerMethodArgumentResolver
在 Handler 被调用前,将调用 HandlerMethodArgumentResolver 的相关方法,对传递给 Handler 的参数进行处理。
public interface HandlerMethodArgumentResolver {
/**
* 用于判断传递给 Handler 的参数是否适用于该 Resolver
* @param parameter Handler 处理方法的参数的信息的封装
* @return 返回 true 则会调用方法 resolveArgument 进行后续的处理;如果返回 false 则不会做进一步的处理。
*/
boolean supportsParameter(MethodParameter parameter);
/**
* 对 Request 中带有的参数、数据进行解析,得到一个最终会传递给 Handler 作为参数的对象。
* @param parameter Handler 处理方法的参数的信息的封装
* @param mavContainer 当前请求的 ModelAndView 容器
* @param webRequest 当前请求对象
* @param binderFactory 创建 WebDataBinder 的工厂类实例。可以利用该 DataBinder 进行数据的绑定。
* @return 最终传递给 Handler 处理方法的参数对象
*/
Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer,
NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception;
}
应用例子:
public class CurrentUserMethodArgumentResolver implements HandlerMethodArgumentResolver {
@Override
public boolean supportsParameter(MethodParameter parameter) {
return parameter.hasParameterAnnotation(CurrentUser.class);
}
@Override
public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
CurrentUser currentUserAnnotation = parameter.getParameterAnnotation(CurrentUser.class);
return webRequest.getAttribute(currentUserAnnotation.value(), NativeWebRequest.SCOPE_REQUEST);
}
}
当 Controller 的方法的参数上使用了 @CurrentUser 进行注解,那么该参数将会是存储在 request 属性中的某个对象。
当然,需要在 Spring Boot 中进行如下配置。
@Configuration
public class WebConfiguration extends WebMvcConfigurerAdapter {
@Autowired
private CurrentUserMethodArgumentResolver currentUserMethodArgumentResolver;
@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) {
argumentResolvers.add(currentUserMethodArgumentResolver);
super.addArgumentResolvers(argumentResolvers);
}
}
ControllerAdvice/RestControllerAdvice
ControllerAdvice/RestControllerAdvice 实现 Controller 类的特定切面能够运用到整个应用程序的所有控制器中。即可以对系统中的所有 Controller 进行统一的处理。
而这个类通过@ControllerAdvice 或是 @RestControllerAdvice 注解,通常会包含以下一种或是多钟方法,将会应用到所有 @RequestMapping 注解方法:
- @ExceptionHandler 注解标注的方法,将处理 Controller 中抛出的指定异常,配合 @ResponseStatus 的使用可以控制返回的响应码。
- @InitBinder 注解标注的方法,在其执行之前初始化数据绑定器 WebDataBinder 。
- @ModelAttribute 注解标注的方法,在其执行之前把返回值放入 Model 。
ControllerAdvice 还可以使用 @Order 定义处理顺序,默认为最低优先级,值越大,优先级越低。
RequestBodyAdvice
RequestBodyAdvice 能够在请求被转换成相应对象参数传递给 Controller 之前或之后进行一些特殊的处理。一般可以通过 RequestBodyAdvice 请求验签,用户身份验证等操作。由 RequestMappingHandlerAdapter 或是 ControllerAdvice 实现,当然大多数情况下还是由 ControllerAdvice 实现。
一般来说想要使用 RequestBodyAdvice ,只需要定义相应的 ControllerAdvice 类并实现 RequestBodyAdvice 即可。RequestBodyAdvice 有相应的适配器类,一般来说继承该适配器类即可。以下为一个用于身份验证的例子: 先定义一个注解类,方便判断是否需要进行身份验证处理。
@Target({ ElementType.METHOD, ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
public @interface UserIdentityVerify {
}
@ControllerAdvice
@Order
public class UserIdentityVerifyAdvice extends RequestBodyAdviceAdapter {
/**
* 用于判断是否调用该 RequestBodyAdvice 的方法
*
* @return true 即支持调用 ; false 则不支持调用
*/
@Override
public boolean supports(MethodParameter methodParameter, Type type, Class<? extends HttpMessageConverter<?>> aClass) {
// 判断即将被调用的 RequestMapping 方法是否有自定义身份验证注解
UserIdentityVerify userIdentityVerify = methodParameter.getMethodAnnotation(UserIdentityVerify.class);
// 如果方法上没有注解,查看该方法所在的类是否进行了注解
return Objects.nonNull(userIdentityVerify) || methodParameter.getDeclaringClass().isAnnotationPresent(UserIdentityVerify.class);
}
@Override
public HttpInputMessage beforeBodyRead(HttpInputMessage inputMessage, MethodParameter parameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) throws IOException {
InputStream in = inputMessage.getBody();
String payload = StreamUtils.copyToString(in, Constants.DEFAULT_CHARSET);
in.close();
verify(payload);
return new HttpInputMessage() {
@Override
public InputStream getBody() {
return new ByteArrayInputStream(payload.getBytes(Constants.DEFAULT_CHARSET));
}
@Override
public HttpHeaders getHeaders() {
return inputMessage.getHeaders();
}
};
}
private void verify(String payload) {
// 身份验证逻辑
}
}
上述例子用 Filter 也可以实现,但是使用 RequestBodyAdvice 的话有两个好处:
- RequestBodyAdvice 比起 Filter 在使用时不再需要额外配置。
- 在 RequestBodyAdvice 中处理请求消息时更简单。一般来说请求流只能读取一次,如果提前读取了请求中的消息,后续在进行业务处理时,再次读取请求流将会出错,所以在 Filter 中需要包装处理 Request 对象,而在 RequestBodyAdvice 不需要进行这么麻烦的处理。
ResponseBodyAdvice
ResponseBodyAdvice 能够在请求响应前进行一些特殊处理,例如对响应消息进行加密。它和 RequestBodyAdvice 一样,由 RequestMappingHandlerAdapter 或是 ControllerAdvice 实现,当然大多数情况下还是由 ControllerAdvice 实现。
一般来说想要使用 RequestBodyAdvice ,只需要定义相应的 ControllerAdvice 类并实现 ResponseBodyAdvice 即可。ResponseBodyAdvice 不像 RequestBodyAdvice 需要适配器类,因为它定义了两个方法。以下为对响应消息加密的用例:
@ControllerAdvice
public class EncodeResponseAdvice implements ResponseBodyAdvice<Object> {
private static final Logger LOGGER = LoggerFactory.getLogger(EncodeResponseAdvice.class);
@Autowired
private ObjectMapper objectMapper;
@Override
public boolean supports(MethodParameter returnType, Class converterType) {
return true;
}
@Override
public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
try {
// 头部处理
HttpHeaders headers = response.getHeaders();
headers.set("Server-Encrypt", "true");
String payload = body instanceof String ? (String) body : objectMapper.writeValueAsString(body);
return encode(payload);
} catch (Exception e) {
LOGGER.error("response encode fail!", e);
throw new RuntimeException(e);
} finally {
MvcCodeKeyContext.clear();
}
}
private String encode(String payload) {
// 加密处理
}
}
监控
Spring 监控相关
HealthIndicator
<!-- Maven 依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
Spring 的健康检查机制,可以通过访问 {host}:{port}/health 获取到项目相关的健康信息,如项目中用到的 redis 和 数据库等。
public interface HealthIndicator {
/**
* Return an indication of health.
* @return the health for
*/
Health health();
}
Spring 本身就实现了很多再项目中常用的服务的健康检测,并已经在 Spring 中进行了相关的配置,添加到了 Spring 容器中,如下所示。
一般来说,只要在 Maven 中引入了相关的依赖,如 redis,mysql 等依赖,就可以通过访问上述提到的 /health 获取项目的健康信息。
相关的配置类为 HealthIndicatorAutoConfiguration,以下为在该类中关于 Redis 健康检测的配置。
@Configuration
@ConditionalOnClass(RedisConnectionFactory.class)
@ConditionalOnBean(RedisConnectionFactory.class)
@ConditionalOnEnabledHealthIndicator("redis")
public static class RedisHealthIndicatorConfiguration extends
CompositeHealthIndicatorConfiguration<RedisHealthIndicator, RedisConnectionFactory> {
private final Map<String, RedisConnectionFactory> redisConnectionFactories;
public RedisHealthIndicatorConfiguration(
Map<String, RedisConnectionFactory> redisConnectionFactories) {
this.redisConnectionFactories = redisConnectionFactories;
}
@Bean
@ConditionalOnMissingBean(name = "redisHealthIndicator")
public HealthIndicator redisHealthIndicator() {
return createHealthIndicator(this.redisConnectionFactories);
}
}
而在 Spring 中 Redis 的健康检测类实现如下。
public class RedisHealthIndicator extends AbstractHealthIndicator {
private static final String VERSION = "version";
private static final String REDIS_VERSION = "redis_version";
private final RedisConnectionFactory redisConnectionFactory;
public RedisHealthIndicator(RedisConnectionFactory connectionFactory) {
Assert.notNull(connectionFactory, "ConnectionFactory must not be null");
this.redisConnectionFactory = connectionFactory;
}
@Override
protected void doHealthCheck(Health.Builder builder) throws Exception {
RedisConnection connection = RedisConnectionUtils
.getConnection(this.redisConnectionFactory);
try {
if (connection instanceof RedisClusterConnection) {
ClusterInfo clusterInfo = ((RedisClusterConnection) connection)
.clusterGetClusterInfo();
builder.up().withDetail("cluster_size", clusterInfo.getClusterSize())
.withDetail("slots_up", clusterInfo.getSlotsOk())
.withDetail("slots_fail", clusterInfo.getSlotsFail());
}
else {
Properties info = connection.info();
builder.up().withDetail(VERSION, info.getProperty(REDIS_VERSION));
}
}
finally {
RedisConnectionUtils.releaseConnection(connection,
this.redisConnectionFactory);
}
}
}
通过上面的代码可以看出,如果不想使用默认的 Redis 健康检测,可以自己创建一个 Bean ,并将 Bean 的名称命名为 redisHealthIndicator 即可。
想要额外添加新的健康检测,实现 HealthIndicator 即可,然后主要是根据业务需求返回一个正确的 Health 对象,详情可以参考 Health 文档 。
PublicMetrics
该机制在 Spring Boot 2.0 后有了变化,下面内容基于 Spring Boot 1.5.15 GA
<!-- Maven 依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
Spring 的度量指标机制。可以通过访问 {host}:{port}/metrics 相关指标的数值,例如项目的中 JVM 内存使用情况,线程数等指标的数值。
public interface PublicMetrics {
/**
* Return an indication of current state through metrics.
* @return the public metrics
*/
Collection<Metric<?>> metrics();
}
Spring 本身也已经实现了一些 PublicMetrics 类并已经在 Spring 中进行了相关的配置,添加到了 Spring 容器中。
想要添加新的指标,实现 PublicMetrics 即可,然后根据需求返回相应的 Metric 集合对象,关于 Metric 的详细信息可参考 Metric 文档
一种比较常见的实现是针对于本地缓存的度量统计,获取缓存的 hit 和 miss 的数据,以便观察本地缓存的使用情况。
Spring AOP
Spring AOP相关
AOP 表达式
简单说明一下关于 Spring AOP 的表达式相关内容。
maven 依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
比较常用的表达式为:
关于表达式的相关详细内容:
AspectJ指示器 | 描述 |
---|---|
arg() | 限制连接点匹配参数为指定类型的执行方法 |
@args() | 限制连接点匹配参数由指定注解标注的执行方法 |
execution() | 用于匹配是连接点的执行方法 |
this() | 限制连接点匹配AOP代理的bean引用为指定类型的类 |
target | 限制连接点匹配目标对象为指定类型的类 |
@target() | 限制连接点匹配特定的执行对象,这些对象对应的类要具有指定类型的注解 |
within() | 限制连接点匹配指定的类型 |
@within() | 限制连接点匹配指定注解所标注的类型(当使用Spring AOP时,方法定义在由指定的注解所标注的类里) |
@annotation | 限定匹配带有指定注解的连接点 |
bean | 限制了连接点只匹配特定的bean |
Spring AOP 注解
在 Spring Boot 中使用切面的功能主要还是使用注解进行配置。以下为 Spring AOP 中的常用注解。
注解 | 通知 |
---|---|
@Before | 通知方法会在目标方法调用之前执行 |
@After | 通知方法会在目标方法返回或抛出异常后调用 |
@AfterReturning | 通知方法会在目标方法返回后调用 |
@AfterThrowing | 通知方法会在目标方法抛出异常后调用 |
@Around | 通知方法会将目标方法封装起来 |
所有的这些注解都需要给定一个切点表达式,有时候可能可能会发生重复的切点表达式,这时可以使用 @Pointcut 注解声明频繁使用的切点表达式。使用例子如下:
获取、修改目标方法参数
在开发时可能会遇到需要利用切面获取目标方法的参数,甚至修改目标方法参数的需求。
对于上述需求,可以使用 @Before 和 @Around 获取到目标方法的参数,但是如果需要修改目标方法的参数,只能通过使用 @Around。
@Aspect
@Component
public class TesterAspect {
/**
* 通过 {@link JoinPoint} 获取目标方法的参数,不需要再切点表达式中指定参数,但是也不能准确的获取到参数。
*/
@Before(value = "execution(* com.bz.test.aop.Tester.test(..))")
public void beforeTest(JoinPoint joinPoint) {
System.out.println("beforeTest");
for (Object arg : joinPoint.getArgs()) {
System.out.println("param is " + arg);
}
}
/**
* 获取目标方法的参数,需要在切点表达式中指定参数。 注意如果有多个参数,将会根据目标方法的参数顺序传递给通知方法。
*
* @param p1 目标方法的参数
*/
@Before(value = "execution(* com.bz.test.aop.Tester.test(..)) && args(p1,..)")
public void beforeTest(String p1) {
System.out.println("beforeTest, param is " + p1);
}
}
使用 @Around 获取目标方法参数的手段和大概相同,接下来为利用 @Around 修改目标方法的参数。
@Aspect
@Component
public class TesterAspect {
@Around(value = "execution(* com.bz.test.aop.Tester.test(..))")
public void aroundTest(ProceedingJoinPoint joinPoint) throws Throwable {
joinPoint.proceed(changeParams(joinPoint.getArgs()));
}
private Object[] changeParams(Object[] args) {
// 更改参数
for (int i = 0; i < args.length; i++) {
Object arg = args[i];
if (arg instanceof String) {
args[i] = arg + " around change";
}
}
return args;
}
}
获取、修改目标方法的返回值
在开发时可能会遇到需要利用切面获取目标方法的返回值,甚至修改目标方法返回值的需求。
对于上述需求,可以使用 @AfterReturning 和 @Around 获取到目标方法的返回值,但是如果需要修改目标方法的返回值,只能通过使用 @Around。
@Aspect
@Component
public class TesterAspect {
/**
* 获取目标方法的返回值,需要在向注解 {@link AfterReturning} 传递 returning 值,该值为绑定返回值的参数名。
*
* @param returnValue 目标方法的返回值
*/
@AfterReturning(value = "execution(* com.bz.test.aop.Tester.test(..))", returning = "returnValue")
public void afterTest(String returnValue) {
System.out.println("After test, return " + returnValue);
}
}
接下来使用 @Around 获取并修改返回值。
@Aspect
@Component
public class TesterAspect {
@Around(value = "execution(* com.bz.test.aop.Tester.test(..))")
public Object afterTest(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
Object result = proceedingJoinPoint.proceed();
System.out.println("return value is " + result);
if (result instanceof String) {
result += " around test";
}
return result;
}
}
异常处理
Spring Boot 中的异常处理
全局异常处理
在介绍如何进行全局异常处理时,先定义一个响应数据类 StateResponse ,保证所有接口的响应信息的格式是一致的。
public class StateResponse {
public static final String SUCCESS_STATE = "success";
public static final String FAIL_STATE = "fail";
private String state = SUCCESS_STATE;
private String message;
public String getState() {
return state;
}
public void setState(String state) {
this.state = state;
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
public static StateResponse ofSuccess(String message) {
StateResponse response = new StateResponse();
response.setState(SUCCESS_STATE);
response.setMessage(message);
return response;
}
public static StateResponse ofSuccess() {
return ofSuccess(null);
}
public static StateResponse ofFail(String message) {
StateResponse response = new StateResponse();
response.setState(FAIL_STATE);
response.setMessage(message);
return response;
}
public static StateResponse ofFail() {
return ofFail(null);
}
}
如果使用 JSON 作为数据的传输格式,那么响应的数据大致如下:
{
"state": "success",
"message": "request success"
}
由于在响应信息体中,没有定义业务状态码,所以将使用 HTTP 状态码作为响应的状态表示,200 表示请求成功,400 表示请求无效,401 表示需要登陆等等,沿用 HTTP 状态码的意义。当然也可以自定义相应的状态码表示,不过注意要与接口调用者协商好。
如果在处理请求时,发生异常,那响应的信息也是需要符合上述的格式的。而对于在请求时发生的异常,可以使用 ControllerAdvice 进行全局的异常处理。
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(value = ForbiddenException.class)
@ResponseStatus(HttpStatus.FORBIDDEN)
public StateResponse forbiddenExceptionHandler(Exception e) {
return StateResponse.ofFail(e.getMessage());
}
@ExceptionHandler(value = InvalidParameterException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public StateResponse invalidParameterExceptionHandler(Exception e) {
return StateResponse.ofFail(e.getMessage());
}
@ExceptionHandler(value = NotFoundException.class)
@ResponseStatus(HttpStatus.NOT_FOUND)
public StateResponse notFoundExceptionExceptionHandler(Exception e) {
return StateResponse.ofFail(e.getMessage());
}
}
但是当系统抛出没有在 ExceptionHandler 中进行处理的异常时,Spring Boot 将响应默认定义的响应信息体,大致如下:
{
"timestamp": 1541334441252,
"status": 500,
"error": "Internal Server Error",
"exception": "java.lang.RuntimeException",
"message": "No message available",
"path": "/api/test"
}
这样既不符合我们上面定义的响应信息格式,同时也可能暴露了一些我们不想暴露的信息,而如果在 ExceptionHandler 中统一处理 RuntimeException 的话,有可能覆盖掉 Spring Boot 的一些默认处理,丢失了我们想要的一些信息。
为了解决上面提到的问题,可以让 ControllerAdvice 继承抽象类 ResponseEntityExceptionHandler 。该类对我们常要处理的一些异常情况统一在 ExceptionHandler 进行了指定捕获。
对于不同的异常,ResponseEntityExceptionHandler 都预留了一个相应的处理方法,可以通过覆盖相应的处理方法完成自定义的异常处理逻辑。
除此之外,ResponseEntityExceptionHandler 中的所有异常处理,最终都会调用方法 handleExceptionInternal ,可以通过覆盖该方法完成一些通用的逻辑处理。以下为使用示例:
@ControllerAdvice
public class GlobalExceptionHandler extends ResponseEntityExceptionHandler {
private static final Logger LOGGER = LoggerFactory.getLogger(GlobalExceptionHandler.class);
@ExceptionHandler(value = ForbiddenException.class)
public ResponseEntity<Object> forbiddenExceptionHandler(Exception ex) {
return ResponseEntity.status(HttpStatus.FORBIDDEN).body(StateResponse.ofFail(ex.getMessage()));
}
@ExceptionHandler(value = InvalidParameterException.class)
public ResponseEntity<Object> invalidParameterExceptionHandler(Exception ex) {
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(StateResponse.ofFail(ex.getMessage()));
}
@ExceptionHandler(value = NotFoundException.class)
public ResponseEntity<Object> notFoundExceptionExceptionHandler(Exception ex) {
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(StateResponse.ofFail(ex.getMessage()));
}
/**
* 如果没有被相应的 ExceptionHandler 处理的异常一般为发生了意料之外的错误所导致的,统一捕获处理,并返回 500
*/
@ExceptionHandler(Throwable.class)
public ResponseEntity<Object> unexpectedExceptionHandler(Throwable ex, HttpServletRequest request) {
LOGGER.error("unexpected exception occur, uri:[{}], params:[{}], stack trace:",
request.getRequestURI(),
Joiner.on("#").withKeyValueSeparator("=").join(request.getParameterMap()),
ex);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(StateResponse.ofFail(HttpStatus.INTERNAL_SERVER_ERROR.getReasonPhrase()));
}
/**
* 参数验证错误处理
*/
@Override
protected ResponseEntity<Object> handleMethodArgumentNotValid(MethodArgumentNotValidException ex, HttpHeaders headers, HttpStatus status, WebRequest request) {
BindingResult bindingResult = ex.getBindingResult();
String message = Joiner.on(",").join(
bindingResult.getAllErrors()
.stream()
.map(DefaultMessageSourceResolvable::getDefaultMessage)
.collect(Collectors.toList())
);
return handleExceptionInternal(ex, StateResponse.ofFail(message), headers, status, request);
}
@Override
protected ResponseEntity<Object> handleExceptionInternal(Exception ex, Object body, HttpHeaders headers, HttpStatus status, WebRequest request) {
body = Optional.ofNullable(body).orElse(StateResponse.ofFail(status.getReasonPhrase()));
return super.handleExceptionInternal(ex, body, headers, status, request);
}
}
参数校验
在开发中,常常会遇到参数的校验问题,如参数不能为空的校验。这里对在 Spring 中进行参数校验的方法进行总结。
Controller 中校验
在编写 Controller 时,可以将请求传递的参数序列化成一个对象,方便后续对参数的处理。如下:
@RestController
@RequestMapping("/api/user")
public class UserController {
@RequestMapping
public String addUser(User user) {
// 进行相关业务操作
}
}
在实际的业务需求中,会对请求传递的参数有所要求,如参数不能为空。
在 Spring MVC 中想要完成验证,首先需要在相关的数据类中添加 validation 注解。
public class User {
@NotEmpty
private String userId;
@NotEmpty
private String name;
@NotNull
private Token token;
//getter and setter
}
然后再在相关的 Controller 方法中添加 @Valid 注解,开启验证。
@RestController
@RequestMapping("/api/user")
public class UserController {
@RequestMapping
public String addUser(@Valid User user) {
// 进行相关业务操作
}
}
如果没有特别的配置,当参数验证失败时,Spring Boot 会进行统一的处理,响应 400 。
如果想在验证失败时,进行自定义操作,有两种方法。
- 当验证失败时,会抛出异常 org.springframework.web.bind.MethodArgumentNotValidException ,通过定义相关的 ExceptionHandler ,捕获该异常,然后作出自定义的操作。
- 在 @Valid 注解的参数后,跟上 BindingResult 对象参数,可以通过 BindingResult 对象获知是否发生错误和相关的错误信息。具体如下:
@RestController
@RequestMapping("/test/user")
public class UserController {
@RequestMapping
public String addUser(@Valid @RequestBody User user, BindingResult bindingResult) {
if (bindingResult.hasErrors()) {
// 相关错误处理操作
}
// 进行相关业务操作
}
}
自定义验证方法
验证操作,除了上述的是否为空外,有时还需要一些特定的验证方法,例如参数是否符合邮件地址。
对于这个问题,可以通过自定义验证完成。
进行自定义验证,首先需要构建两个类,一个是约束注解类 ,一个是约束验证器类。
以下为约束注解类:
@Documented
@Constraint(validatedBy = MailAddressValidator.class)
@Target({ElementType.METHOD, ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface MailAddressConstraint {
String message() default "invalid mail address";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
以下为约束验证器类:
public class MailAddressValidator implements ConstraintValidator<MailAddressConstraint, String> {
@Override
public void initialize(MailAddressConstraint constraintAnnotation) {
// 验证操作开始前初始化
}
@Override
public boolean isValid(String value, ConstraintValidatorContext context) {
return validMailAddress(value);
}
private boolean validMailAddress(String mailAddress){
// 邮件地址验证算法
}
}
在约束注解类的一个注解 @Constraint 中有一个 validatedBy 参数,就是用于指定对该约束进行验证的验证器。
Validator
对参数的验证不仅仅只发生在 Controller 中,在其他处理流程中亦会发生参数验证。这时可以使用 Validator 完成参数的校验。
@Service
public class MessageService {
private static final Logger LOGGER = LoggerFactory.getLogger(MessageService.class);
@Autowired
private Validator validator;
public void handleMessage(MessageVO vo) {
Set<ConstraintViolation<MessageVO>> violations = validator.validate(vo);
if (!violations.isEmpty()) {
List<String> violationMessageList = violations.stream().collect(ArrayList::new,
(list, violation) -> list.add(violation.getMessage()),
ArrayList::addAll);
String violationMessage = Joiner.on(System.getProperty("line.separator")).join(violationMessageList);
ConstraintViolationException ex = new ConstraintViolationException(violationMessage, violations);
LOGGER.error("inspection failure", ex);
return;
}
}
}
可以尝试使用校验工具类:ValidatorHelper
工具类
Spring 开发中工具类相关。
SpringUtil
当遇到要在非 Spring 容器里的对象获取到 Spring 容器中的 Bean 的需求时,可以构建一个工具类 SpringUtil 进行解决。
代码片段:SpringUtil
WebUtils
WebUtils 封装了在 Spring Web 开发中常用的方法,例如从 Request 对象中获取 Session ID,获取 Cookie 等等。详情查看 WebUtils文档
ValidatorHelper
参数校验工具类。
代码片段:ValidatorHelper