看了同事写的代码,大家竟然都开始默默模仿了。。。

我是程序汪

共 9817字,需浏览 20分钟

 · 2022-07-16

背景

事情是这样的,目前我正在参与 XXXX 项目的搭建,需要与第三方对接接口。在对方的接口中存在几个异步通知,为了接口的安全性,需要对接口的参数进行验签处理。

为了方便大家对异步通知返回参数的处理,Z 同事提出要将该验签功能进行统一封装,到时候大家只需要关注自己的业务逻辑即可。

Z同事的解决方案

Z 同事选择的是“自定义参数解析器”的解决方案,接下来我们通过代码来了解一下。

自定义注解

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.PARAMETER})
public @interface RsaVerify {
    
    /**
     * 是否启用验签功能,默认验签
     */

    boolean verifySign() default true;
}

自定义方法参数解析器

@AllArgsConstructor
@Component
//实现 HandlerMethodArgumentResolver 接口
public class RsaVerifyArgumentResolver implements HandlerMethodArgumentResolver {

    private final SecurityService securityService;

    /**
     * 此方法用来判断本次请求的接口是否需要解析参数,
     * 如果需要返回 true,然后调用下面的 resolveArgument 方法,
     *  如果不需要返回 false
     */

    @Override
    public boolean supportsParameter(MethodParameter parameter) {
        return parameter.hasParameterAnnotation(RsaVerify.class);
    }

    /**
     * 真正的解析方法,将请求中的参数值解析为某种对象
     * parameter 要解析的方法参数
     * mavContainer 当前请求的 ModelAndViewContainer(为请求提供对模型的访问)
     * webRequest 当前请求
     * WebDataBinderFactory 用于创建 WebDataBinder 的工厂
     */

    @Override
    public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
        RsaVerify parameterAnnotation = parameter.getParameterAnnotation(RsaVerify.class);
        if (!parameterAnnotation.verifySign()) {
            return mavContainer.getModel();
        }
        
        //对参数进行处理并验签的逻辑
        ......
        
        //返回处理后的实体类参数
        return ObjectMapperFactory
                .getDateTimeObjectMapper("yyyyMMddHHmmss")
                .readValue(StringUtil.queryParamsToJson(sb.toString()), parameter.getParameterType());
    }
   
}

创建配置类

@Configuration
@AllArgsConstructor
public class PayTenantWebConfig implements WebMvcConfigurer {

    private final RsaVerifyArgumentResolver rsaVerifyArgumentResolver;
    
    /**
     * 将自定义的方法参数解析器加入到配置类中
     */

    @Override
    public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
        resolvers.add(rsaVerifyArgumentResolver);
    }
}

使用

使用方法非常简单,只需要在参数上引入注解就可以了

@RestController
@Slf4j
@RequestMapping("/xxx")
public class XxxCallbackController {

    /**
     * @param params
     * @return
     */

    @PostMapping("/callback")
    public String callback(@RsaVerify CallbackReq params) {
        log.info("receive callback req={}", params);
  //业务逻辑处理
  .....
  
        return "success";
    }
}

问题

问题一

看到这,细心的朋友应该会有所疑问:既然这边用到了自定义的注解,为什么不用切面来实现,而是使用自定义的参数解析器呢?Very Good!这也是阿Q提出的疑问,同事说是因为 jackson 的反序列化动作优先级远高于切面的优先级,所以还没进入切面就已经报反序列化失败的错误了。

问题二

为什么在 controller 中注解 @RequestBody 不见了?

要回答这个问题,我们就得了解下HandlerMethodArgumentResolverComposite这个类了,以下简称CompositeSpringMVC 在启动时会将所有的参数解析器放到 Composite 中,Composite 是所有参数的一个集合。当对参数进行解析时就会从该参数解析器集合中选择一个支持对 parameter 解析的参数解析器,然后使用该解析器进行参数解析。

又因为@RequestBody所以使用的参数解析器RequestResponseBodyMethodProcessor优先级高于我们自定义的参数解析器,所以如果共用会被前者拦截解析,所以为了正常使用,我们需要将@RequestBody 注解去掉。

/**
 * Find a registered {@link HandlerMethodArgumentResolver} that supports
 * the given method parameter.
 */

@Nullable
private HandlerMethodArgumentResolver getArgumentResolver(MethodParameter parameter) {
    HandlerMethodArgumentResolver result = this.argumentResolverCache.get(parameter);
    if (result == null) {
        for (HandlerMethodArgumentResolver resolver : this.argumentResolvers) {
            if (resolver.supportsParameter(parameter)) {
                result = resolver;
                this.argumentResolverCache.put(parameter, result);
                break;
            }
        }
    }
    return result;
}

C同事的解决方案

上边 Z 同事的方案已经可以解决该问题了,但是该方案还有两个不足之处:

  • 需要每一个回调都去创建自己的 controller 层,没有一个对外的统一入口;
  • 需要在方法上添加自定义注解,侵入性比较强;

因此经过我们的商议,决定摒弃该方案,但是该方案的思想值得我们学习。接下来让我们分析一下新的解决方案:

定义业务接口类

业务接口类包含两个方法:具体业务处理的类型;业务的具体处理方法。

public interface INotifyService {
 /**
  * 处理类型
  */

 public String handleType();
 /**
  * 处理具体业务
  */

 Integer handle(String notifyBody);

}

异步通知统一入口

@AllArgsConstructor
@RestController
@RequestMapping(value = "/notify")
public class NotifyController {
 private IService service;

    @PostMapping(value = "/receive")
    public String receive(@RequestBody String body) {
        //处理通知
        Integer status = service.handle(body);
        return "success";
    }
}

在 Iservice 中做两个步骤:

  • 在 spring 启动之后,收集所有的类型为 INotifyService的类并放入map中;
  • 将参数进行处理转化,并验签处理;
private ApplicationContext applicationContext;
private Map<String,INotifyService> notifyServiceMap;

/**
 * 启动加载
 */

@PostConstruct
public void init(){
 Map<String,INotifyService> map = applicationContext.getBeansOfType(INotifyService.class);
 Collection<INotifyService> services = map.values();
 if(CollectionUtils.isEmpty(services)){
  return;
 }
 notifyServiceMap = services.stream().collect(Collectors.toMap(INotifyService::handleType, x -> x));
}

@Override
public Map<String, INotifyService> getNotifyServiceMap() {
 return notifyServiceMap;
}

@Override
public Integer handle(String body) {
 //参数处理+验签逻辑
    ......
        
 //获取具体的业务实现类
 INotifyService notifyService=notifyServiceMap.get(notifyType);
 Integer status=null;
 if(Objects.nonNull(notifyService)) {
  //执行具体业务
  try {
   status=notifyService.handle(JSON.toJSONString(requestParameter));
  } catch (Exception e) {
   e.printStackTrace();
  }
 }
 //后续逻辑处理
    ......
        
 return status;
}

业务具体实现

@Service
public class NotifySignServiceImpl implements INotifyService {

    @Override
    public String handleType() {
        return "type_sign";
    }

    @Override
    @Transactional
    public Integer handle(String notifyBody) {
        //具体的业务处理
        ......
    }

小结

  • 此方案提供统一的异步通知入口,把公共的参数处理和验签逻辑与业务逻辑剥离。
  • 利用 java 动态加载类的特性,将实现类通过类型进行收集。
  • 利用 java 多态的特性,通过不同的实现类来处理不同的业务逻辑。

看到这,相信大家已经对这两种实现方案有了一定的理解,大家可以试着在以后的项目中应用一下,体验一把!

程序汪资料链接

程序汪接的7个私活都在这里,经验整理

Java项目分享  最新整理全集,找项目不累啦 07版

堪称神级的Spring Boot手册,从基础入门到实战进阶

卧槽!字节跳动《算法中文手册》火了,完整版 PDF 开放下载!

卧槽!阿里大佬总结的《图解Java》火了,完整版PDF开放下载!

字节跳动总结的设计模式 PDF 火了,完整版开放下载!


欢迎添加程序汪个人微信 itwang009  进粉丝群或围观朋友圈


浏览 25
点赞
评论
收藏
分享

手机扫一扫分享

举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

举报