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

本文涉及的产品
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
全局流量管理 GTM,标准版 1个月
云解析 DNS,旗舰版 1个月
简介: 看了同事写的代码,我竟然开始默默的模仿了。。。

                                                                                           

背景

事情是这样的,目前我正在参与 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 多态的特性,通过不同的实现类来处理不同的业务逻辑。

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


相关文章
|
6月前
|
数据采集 算法 Java
业余爱好者想入门编程,一定远离那些只会说No的家伙,尤其程序员
业余爱好者想入门编程,一定远离那些只会说No的家伙,尤其程序员
63 2
|
人工智能 IDE 算法
【周末闲谈】新的编程方式,程序员的未来何在?
【周末闲谈】新的编程方式,程序员的未来何在?
123 0
|
设计模式 消息中间件 JavaScript
看了我写的设计模式,全公司同事都开始悄悄模仿了。。。 下
看了我写的设计模式,全公司同事都开始悄悄模仿了。。。 下
|
设计模式 算法 JavaScript
看了我写的设计模式,全公司同事都开始悄悄模仿了。。。 上
看了我写的设计模式,全公司同事都开始悄悄模仿了。。。 上
|
消息中间件 存储 JavaScript
如何写出一手让同事膜拜的漂亮代码?
如何写出一手让同事膜拜的漂亮代码?
|
设计模式 消息中间件 算法
看了我写的设计模式,全公司同事都开始悄悄模仿了。。。
看了我写的设计模式,全公司同事都开始悄悄模仿了。。。
|
机器学习/深度学习 人工智能 自然语言处理
AlphaCode 能替代人类程序员吗?网友:被替代也挺好,这样就可以少写代码多开会了
DeepMind 是最新的人工智能研究实验室。它推出了一个可以生成软件源代码的深度学习模型,成果令人印象深刻。该模型被称为 AlphaCode,基于 Transformer,与 OpenAI 在其代码生成模型中使用的架构是一样的。
169 0
AlphaCode 能替代人类程序员吗?网友:被替代也挺好,这样就可以少写代码多开会了
|
算法 数据挖掘 程序员
程序员究竟能干多少年?用数据说话!
程序员这个职业究竟可以干多少年,在中国这片神奇的土地上,很多人都说只能干到30岁,然后就需要转型。 我在很多面试中,问到应聘者未来的规划都能听到好些应聘都说程序员是个青春饭。因为,大多数程序员都认为,编程这个事只能干到30岁,最多35岁吧。
301 0
程序员究竟能干多少年?用数据说话!
|
存储 程序员 C++
如果当初学习编程时能有人给我这些忠告该多好
Cecily Carver 是多伦多的一位程序媛,和 Jennie Faber 一起创办了一个游戏制作工作室。她喜欢歌剧、舞蹈和弹钢琴。Cecily 在这篇文章分享她在编程道路上的所感所想,给出很多值得思考的编程箴言以及一些思想误区,比如在你学习编程之前思考一下你的目标、编程不是什么神秘的东西、坚持比方法更重要等,可以让我们在编程路上少走一些弯路,从而有更多的时间学习技术让自己变的越来越强大。
226 0