场景:在付款、查询、取消订单等很多和支付相关的操作时用户必须要选择支付方式,比如支付宝或微信。后端在验证时就面临一个问题,是在每个方法里都写验证代码,还是统一验证。
相信绝大多数道友都会采用统一验证的方案,最最简单的方案莫过于:
public class TController {
@PostMapping("/pay")
public JifengnanPayResult pay(@RequestBody TradePayModel model) throws Exception {
validatePaymentWay(model.getPaymentWayId());
// 其他代码
}
@PostMapping("/generate/qr/code")
public JifengnanPayResult generateQrCode(@RequestBody TradePrecreateModel model, HttpServletResponse res) throws Exception {
validatePaymentWay(model.getPaymentWayId());
// 其他代码
}
private void validatePaymentWay(Integer id) throws JifengnanPayException{
// 验证逻辑
}
}
这个方案非常简单,中规中矩,这样做对于业务代码来说够用了。但是对于想要飞升成为架构师甚至更高界面的道友来说就显得太简单了,不好跟人吹牛X。
上面方案的最大不足就是验证逻辑和其它代码耦合了。耦合,很高大尚的词汇,其实以这个例子来说就是,如果验证代码不再需要了,或者发生了剧烈的变化,其不能被简单移除,所以说验证代码和其他代码耦合了。
再细点说就是,如果验证代码不再需要了,开发人员必须一个一个的从每个调用了验证代码的方法里把其移除掉,并且所有涉及到的方法及其功能都必须重新做测试。
那么,如何达到不耦合的目的呢?这里以Spring及spring boot为基础,提供了几种解决方案。这几种方案就算把整个类都删掉对原来的代码的逻辑也没有任何影响。
1. Filter(Spring boot)
相信很多道友都知道这个东西,所以直接上代码,不多做介绍了。注意 @WebFilter 和 @ServletComponentScan 必须要加上,之所以说是spring boot的方案也是因为这两个标签。
@Order(Ordered.LOWEST_PRECEDENCE - 100)
@ServletComponentScan
@WebFilter("/trade/*")
public class PaymentWayFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
String paymentWay = request.getParameter("payment_way");
if (StringUtils.isEmpty(paymentWay)) {
response.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE);
response.getWriter().write("请选择一种支付方式");
return;
}
filterChain.doFilter(request, response);
}
}
2. Interceptor(Spring Framework)
这个和Filter类似,区别这里就不多做介绍了,还是废话不多说,直接上代码。注意@EnableWebMvc,@Configuration和@ComponentScan 这三个注解。
public class PaymentWayValidationInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String paymentWay = request.getParameter("payment_way");
if (StringUtils.isEmpty(paymentWay)) {
throw new MissingServletRequestParameterException("payment_way", "");
}
if (!paymentWay.equals(String.valueOf(PaymentWay.ALIPAY.getId()))) {
throw new ServletException("目前仅支持支付宝");
}
return true;
}
}
@EnableWebMvc
@Configuration
@ComponentScan
public class WebConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new PaymentWayValidationInterceptor());
}
}
3. Body Advice
第三种知道的人可能就没那么多了,这个是在HTTP body被解析为对象时提供给我们进行扩展的一种方法,当然这种仅适用于POST HTTP body的情况,而前面两种更适合于GET传参情况下的验证。
这个方案的出现是因为自己解析HTTP body中的数据很麻烦(要从input stream中解析出数据然后再验证),再加上现在spring框架都是自动解析数据成对象的(比如public JifengnanPayResult pay(@RequestBody TradePayModel model),HTTP body中的数据会被spring框架直接放入TradePayModel对象中),我们实在没有必要再自己解析一遍。
首先定义一个验证类继承自RequestBodyAdviceAdapter
public class PaymentWayValidationAdvice extends RequestBodyAdviceAdapter {
@Override
public boolean supports(MethodParameter methodParameter, Type targetType,
Class extends HttpMessageConverter>> converterType) {
return true;
}
@Override
public Object afterBodyRead(Object body, HttpInputMessage inputMessage, MethodParameter parameter,
Type targetType, Class extends HttpMessageConverter>> converterType) {
if (body instanceof JifengnanPayModel) {
Integer paymentWay = ((JifengnanPayModel) body).getPaymentWayId();
if (paymentWay == null) {
throw new HttpMessageNotReadableException("支付方式(payment_way)不能为空");
}
}
return body;
}
}
3.1 WebMvcConfigurationSupport(Spring Framework)
第一种方式使用了继承WebMvcConfigurationSupport 并覆盖requestMappingHandlerAdapter方法的方式,注意@Configuration 和 @Bean 两个注解。
@Configuration
public class WebConfig extends WebMvcConfigurationSupport {
@Bean
public RequestMappingHandlerAdapter requestMappingHandlerAdapter() {
RequestMappingHandlerAdapter adapter = super.requestMappingHandlerAdapter();
PaymentWayValidationAdvice advice = new PaymentWayValidationAdvice();
adapter.setRequestBodyAdvice(Collections.singletonList(advice));
return adapter;
}
}
3.2 MvcRegistrations(Spring boot)
第二种方式实现了WebMvcRegistrations,spring会扫描该种类型的bean并调用adapter中的advice。
@Configuration
public class MvcRegistrations implements WebMvcRegistrations {
public RequestMappingHandlerAdapter getRequestMappingHandlerAdapter() {
RequestMappingHandlerAdapter adapter = new RequestMappingHandlerAdapter();
adapter.setRequestBodyAdvice(Collections.singletonList(new PaymentWayValidationAdvice()));
return adapter;
}
}
3.3 @ControllerAdvice(Spring Framework)
这才是终极BOSS,前面的都是铺垫,渣渣。
3.1和3.2都是在使用各种办法让我们定义的验证类(PaymentWayValidationAdvice)被spring框架识别,但其实有一种特别简单的方式可以达到这个目的:使用注解@ControllerAdvice,也就是直接在PaymentWayValidationAdvice类上面加这个注解就行了。
@ControllerAdvice
public class PaymentWayValidationAdvice extends RequestBodyAdviceAdapter {
事实上,实现统一验证的方法可能还有更多,大家感兴趣的话可以自己去寻找。