After reading the code written by my colleagues, I began to imitate silently...

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
//Implement the 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,
     *	If you need to return to true, 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 ModelAndViewContainer of the current request (providing access to the model for the request)
     * webRequest Current request
     * WebDataBinderFactory Factory for creating 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();
        }
        
        //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 a question raised by ah Q. my colleague said that because jackson's deserialization action priority is much higher than that of the aspect, the error of deserialization failure has been reported before entering the aspect.

Question two

Why is the @ RequestBody annotation missing in the controller?

To answer this question, we have to understand the class HandlerMethodArgumentResolverComposite, hereinafter referred to as Composite. When spring MVC starts, it will put all parameter parsers into Composite, which is a collection of all parameters. When parsing parameters, a parameter parser that supports parameter parsing will be selected from the parameter parser collection, and then the parser will be used for parameter parsing.

Because the parameter parser RequestResponseBodyMethodProcessor used by @ RequestBody takes precedence over our customized parameter parser, the former will intercept the parsing if shared. Therefore, in order to use it normally, we need to remove the @ RequestBody 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 layer without an external unified entry;
  • 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 classes of INotifyService type and put them 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!

That's all for today. If you have different opinions or better idea s, please contact ah Q. add ah q and join the technical exchange group to participate in the discussion!

Keywords: Java

Added by oaf357 on Fri, 10 Dec 2021 05:54:36 +0200