background
The thing is, at present, I am participating in the construction of XXXX project and need to interface with a third party. There are several asynchronous notifications in the other party's interface. In order to ensure the security of the interface, it is necessary to verify the parameters of the interface.
In order to facilitate the processing of asynchronous notification return parameters, colleague Z proposed to uniformly encapsulate the signature verification function. At that time, you only need to pay attention to your own business logic.
Z colleague's solution
Colleague Z chose the solution of "custom parameter parser". Let's learn about it through the code.
Custom annotation
@Documented @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.PARAMETER}) public @interface RsaVerify { /** * Whether to enable the signature verification function. The default is signature verification */ boolean verifySign() default true; }
Custom method parameter parser
@AllArgsConstructor @Component //realization HandlerMethodArgumentResolver Interface public class RsaVerifyArgumentResolver implements HandlerMethodArgumentResolver { private final SecurityService securityService; /** * This method is used to judge whether the requested interface needs to resolve parameters, * Return if necessary true, and then call the following resolveArgument method, * If you do not need to return false */ @Override public boolean supportsParameter(MethodParameter parameter) { return parameter.hasParameterAnnotation(RsaVerify.class); } /** * The real parsing method parses the parameter value in the request into an object * parameter Method parameters to resolve * mavContainer Currently requested ModelAndViewContainer (provides access to the model for requests) * webRequest Current request * WebDataBinderFactory Used to create WebDataBinder Factory */ @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(); } //Logic for processing and signing parameters ...... //Returns the processed entity class parameters return ObjectMapperFactory .getDateTimeObjectMapper("yyyyMMddHHmmss") .readValue(StringUtil.queryParamsToJson(sb.toString()), parameter.getParameterType()); } }
Create configuration class
@Configuration @AllArgsConstructor public class PayTenantWebConfig implements WebMvcConfigurer { private final RsaVerifyArgumentResolver rsaVerifyArgumentResolver; /** * Add the custom method parameter parser to the configuration class */ @Override public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) { resolvers.add(rsaVerifyArgumentResolver); } }
use
The usage method is very simple. You only need to introduce annotations on the parameters
@RestController @Slf4j @RequestMapping("/xxx") public class XxxCallbackController { /** * @param params * @return */ @PostMapping("/callback") public String callback(@RsaVerify CallbackReq params) { log.info("receive callback req={}", params); //Business logic processing ..... return "success"; } }
problem
Question one
Seeing this, careful friends should have some questions: since custom annotations are used here, why not use the aspect to implement, but use the custom parameter parser? Very Good! This is also the question raised by ah Q. my colleague said it was because jackson The priority of the deserialization action of is much higher than that of the aspect, so the error of deserialization failure has been reported before entering the aspect.
Question two
Why controller Chinese annotation @ RequestBody be missing?
To answer this question, we have to understand the class HandlerMethodArgumentResolverComposite, hereinafter referred to as composite. SpringMVC At startup, all parameter parsers will be put into Composite In, composite Is a collection of all parameters. When a parameter is parsed, a support pair is selected from the parameter parser collection parameter The parsed parameter parser, and then use the parser for parameter parsing.
Because the parameter parser RequestResponseBodyMethodProcessor used by @ RequestBody takes precedence over our custom parameter parser, if shared, the former will intercept the parsing. Therefore, in order to use it normally, we need to use @ RequestBody Remove the annotation.
/** * 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. solutions for colleagues
The solution of colleague Z above can solve this problem, but it still has two shortcomings:
-
Each callback needs to create its own controller On the first floor, there is no unified entrance to the outside;
-
User defined annotations need to be added to the method, which is highly invasive;
Therefore, after our discussion, we decided to abandon the scheme, but the idea of the scheme is worth learning. Next, let's analyze the new solution:
Define business interface class
The business interface class contains two methods: the type of specific business processing; Specific processing methods of business.
public interface INotifyService { /** * Processing type */ public String handleType(); /** * Handle specific business */ Integer handle(String notifyBody); }
Asynchronous notification unified portal
@AllArgsConstructor @RestController @RequestMapping(value = "/notify") public class NotifyController { private IService service; @PostMapping(value = "/receive") public String receive(@RequestBody String body) { //Processing notification Integer status = service.handle(body); return "success"; } }
Do two steps in Iservice:
-
After spring starts, collect all types as INotifyService class and put it into the map;
-
Convert the parameters and check the signature;
private ApplicationContext applicationContext; private Map<String,INotifyService> notifyServiceMap; /** * Start loading */ @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) { //Parameter processing + signature verification logic ...... //Get the specific business implementation class INotifyService notifyService=notifyServiceMap.get(notifyType); Integer status=null; if(Objects.nonNull(notifyService)) { //Execute specific business try { status=notifyService.handle(JSON.toJSONString(requestParameter)); } catch (Exception e) { e.printStackTrace(); } } //Subsequent logic processing ...... return status; }
Specific business implementation
@Service public class NotifySignServiceImpl implements INotifyService { @Override public String handleType() { return "type_sign"; } @Override @Transactional public Integer handle(String notifyBody) { //Specific business processing ...... } }
Summary
-
This scheme provides a unified asynchronous notification entry, which separates the public parameter processing and signature verification logic from the business logic.
-
Using the characteristics of java dynamically loading classes, the implementation classes are collected by types.
-
Using the polymorphic characteristics of java, different implementation classes are used to deal with different business logic.
Seeing this, I believe you have a certain understanding of these two implementation schemes. You can try to apply them in future projects and experience them!