背景
事情是这样的,目前我正在参与 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
这个类了,以下简称Composite
。SpringMVC
在启动时会将所有的参数解析器放到 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 多态的特性,通过不同的实现类来处理不同的业务逻辑。
看到这,相信大家已经对这两种实现方案有了一定的理解,大家可以试着在以后的项目中应用一下,体验一把!