Spring Boot 开发常用技术技巧总结

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 的话有两个好处:

  1. RequestBodyAdvice 比起 Filter 在使用时不再需要额外配置。
  2. 在 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 容器中,如下所示。
HealthIndicator相关实现
一般来说,只要在 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>

比较常用的表达式为:
AOP常用表达式

关于表达式的相关详细内容:

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 注解声明频繁使用的切点表达式。使用例子如下:
@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 。
如果想在验证失败时,进行自定义操作,有两种方法。

  1. 当验证失败时,会抛出异常 org.springframework.web.bind.MethodArgumentNotValidException ,通过定义相关的 ExceptionHandler ,捕获该异常,然后作出自定义的操作。
  2. 在 @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

转载于:https://my.oschina.net/bingzhong/blog/1934311

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值