Question origin

When building projects with SpringCloud, using Swagger to generate interface documentation is a recommended option. Swagger provides page access and facilitates debugging of back-end system interfaces directly on a web page. Recently, I encountered a somewhat confused problem. The demo interface example is as follows (the original functional interface has business implementation logic, and the interface is simplified here) :

/ * * *@description: Demo class *@author: Huang Ying
 **/
@Api(tags = "Demo class")
@RestController
@Slf4j
public class DemoController {

	@ApiOperation(value = "Test interface")
	@ApiImplicitParams({
			@ApiImplicitParam(name = "uid", value = "User ID", paramType = "query", dataType = "Long")})@RequestMapping(value = "/api/json/demo", method = RequestMethod.GET)
	public String auth(@RequestParam(value = "uid") Long uid) {
		System.out.println(uid);
		return "the uid: "+ uid; }}Copy the code

[root@requestParam] [root@requestParam] [root@requestParam] [root@requestParam] [root@requestParam] [root@requestParam] [root@requestParam] [root@requestParam] [root@requestParam] [root@requestParam] [root@requestParam] [root@requestParam] [root@requestParam] [root@requestParam] The @apiIMPLICITParam annotation should not be intrusive to the @requestParam attribute. The @apiIMPLicitParam annotation should not be intrusive to the @requestParam attribute. Makes me wonder if @apiIMPLicitParam is intruding on @requestParam’s require property?

Frame selection, version and main functions

Project structures,

RELEASE SpringCloud RELEASE: Greenwich.SR3

Business module

Swagger used by SpringCloud business modules:

Spring4all-swagger 1.9.0.RELEASE configures swagger parameters and saves code development

Business gateway

Swagger used by SpringCloud Business Gateway:

Knife4j 2.0.1 enhanced Swagger UI style (Gateway with Gateway, Swagger with Knife4J-spring-boot-starter dependency, can aggregate business module swagger document)

This scope is only for The SpringCloud business module and does not involve the Swagger document of the business gateway for the time being.

Testing tools

There are currently two test tools: Swagger Doc: Access using a browser, as shown below:

Postman: manually configuring interface parameters. Example:

Case of actual combat

Interface Test 1

We will use the following interfaces, all with default values: @APIIMPLICITParam required is false, @requestParam required is true:

@ApiOperation(value = "Test interface")
@ApiImplicitParams({
		@ApiImplicitParam(name = "uid", value = "User ID", paramType = "query", dataType = "Long")})@RequestMapping(value = "/api/json/demo", method = RequestMethod.GET)
public String auth(@RequestParam(value = "uid") Long uid) {
	System.out.println(uid);
	return "the uid: " + uid;
}
Copy the code

See swagger’s results:

Look at Postman’s results:

Interface Test 2

We change @apiIMPLICITParam’s required value to true, @requestParam stays the same, Restart the module @APIIMPLICITParam (name = “uid”, value = “user ID”, paramType = “query”, Required = true, dataType = “Long”)

See swagger’s results:

The swagger @APIIMPLICITParam required parameter is in effect. If the swagger is null, js does not send the request to the back end.

Interface Test 3

Postman = postman; postman = postman; postman = Postman; postman = Postman

And no matter how @apiIMPLicitParam’s required value is modified, the result is the same. There must be a mistake somewhere, causing us to misjudge.

After reviewing the data, we found that we misunderstood @requestParam’s required parameter. The required value true means that the interface parameter name must exist, but it does not matter whether or not the parameter has a value. Take the example above:

These two requests pass: localhost:8080/api/json//demo? uid
localhost:8080/api/json//demo? uid=Only this type of request will fail: localhost:8080/api/json//demo?
Copy the code

Conclusion small

After the above three interface test scenarios, we can make at least three points clear:

  1. The @APIIMPLICITParam required parameter does not intrude on the @requestParam required value; the two are unrelated.
  2. The @APIIMPLICITParam required parameter affects the JS logic of Swagger Doc. Null validation is done at the JS level.
  3. The @requestParam required parameter checks only the name of the parameter, not its value, by default.

Source analysis

Swagger part

In the previous section, swagger reads @apiIMPLICITParam’s required parameter, which is finally reflected in the js file.

Find ("tr"). Each (function () {var paramtr=$(this); var cked=paramtr.find("td:first").find(":checked").prop("checked"); var _urlAppendflag=true; //that. Log (cked) if (cked){ Note that the line required:paramtr.data("required") information extracts var trdata={name:paramtr.find("td:eq(2)").find("input").val(),in:paramtr.data("in"),required:paramtr.data("required"),type:p aramtr.data("type"),emflag:paramtr.data("emflag"),schemavalue:paramtr.data("schemavalue")}; //that.log("trdata...." ) //that.log(trdata); / / to get key/var/key = paramtr. Find (" td: eq (1) "). The find (" input "). Val (); var key=trdata["name"]; Var value=""; var reqflag=false; }})Copy the code

Check whether the attribute required is true.

// Check whether required
if (trdata.hasOwnProperty("required")){
    var required=trdata["required"];
    if (required){
        if(! reqflag){// Yes, verify that value is null
            if(value==null||value==""){
                validateflag=true;
                var des=trdata["name"]
                //validateobj={message:des+" cannot be empty "};
                validateobj={message:des+i18n.message.debug.fieldNotEmpty};
                return false; }}}}Copy the code

SpringCloud business module section

Swagger front-end JS verification can send requests to the background, or postman to send requests to the background system, start to enter the background of a series of filters, Servlet processing, many things:

// The actual business methods section
auth:28, DemoController (com.hy.demo.controller)
invoke0:-1, NativeMethodAccessorImpl (sun.reflect)
invoke:62, NativeMethodAccessorImpl (sun.reflect)
invoke:43, DelegatingMethodAccessorImpl (sun.reflect)
invoke:498, Method (java.lang.reflect)

// Request parameters extraction, control part
doInvoke:190, InvocableHandlerMethod (org.springframework.web.method.support)
invokeForRequest:138, InvocableHandlerMethod (org.springframework.web.method.support)
invokeAndHandle:104, ServletInvocableHandlerMethod (org.springframework.web.servlet.mvc.method.annotation)
invokeHandlerMethod:892, RequestMappingHandlerAdapter (org.springframework.web.servlet.mvc.method.annotation)
handleInternal:797, RequestMappingHandlerAdapter (org.springframework.web.servlet.mvc.method.annotation)
handle:87, AbstractHandlerMethodAdapter (org.springframework.web.servlet.mvc.method)

// Here are the filters for the various underlying Web service components
doDispatch:1039, DispatcherServlet (org.springframework.web.servlet)
doService:942, DispatcherServlet (org.springframework.web.servlet)
processRequest:1005, FrameworkServlet (org.springframework.web.servlet)
doGet:897, FrameworkServlet (org.springframework.web.servlet)
service:634, HttpServlet (javax.servlet.http)
service:882, FrameworkServlet (org.springframework.web.servlet)
service:741, HttpServlet (javax.servlet.http)
internalDoFilter:231, ApplicationFilterChain (org.apache.catalina.core)
doFilter:166, ApplicationFilterChain (org.apache.catalina.core)
doFilter:53, WsFilter (org.apache.tomcat.websocket.server)
internalDoFilter:193, ApplicationFilterChain (org.apache.catalina.core)
doFilter:166, ApplicationFilterChain (org.apache.catalina.core)
doFilter:84, SecurityBasicAuthFilter (com.github.xiaoymin.swaggerbootstrapui.filter)
internalDoFilter:193, ApplicationFilterChain (org.apache.catalina.core)
doFilter:166, ApplicationFilterChain (org.apache.catalina.core)
doFilter:53, ProductionSecurityFilter (com.github.xiaoymin.swaggerbootstrapui.filter)
internalDoFilter:193, ApplicationFilterChain (org.apache.catalina.core)
doFilter:166, ApplicationFilterChain (org.apache.catalina.core)
doFilter:124, WebStatFilter (com.alibaba.druid.support.http)
internalDoFilter:193, ApplicationFilterChain (org.apache.catalina.core)
doFilter:166, ApplicationFilterChain (org.apache.catalina.core)
doFilterInternal:88, HttpTraceFilter (org.springframework.boot.actuate.web.trace.servlet)
doFilter:109, OncePerRequestFilter (org.springframework.web.filter)
internalDoFilter:193, ApplicationFilterChain (org.apache.catalina.core)
doFilter:166, ApplicationFilterChain (org.apache.catalina.core)
doFilterInternal:99, RequestContextFilter (org.springframework.web.filter)
doFilter:109, OncePerRequestFilter (org.springframework.web.filter)
internalDoFilter:193, ApplicationFilterChain (org.apache.catalina.core)
doFilter:166, ApplicationFilterChain (org.apache.catalina.core)
doFilterInternal:92, FormContentFilter (org.springframework.web.filter)
doFilter:109, OncePerRequestFilter (org.springframework.web.filter)
internalDoFilter:193, ApplicationFilterChain (org.apache.catalina.core)
doFilter:166, ApplicationFilterChain (org.apache.catalina.core)
doFilterInternal:93, HiddenHttpMethodFilter (org.springframework.web.filter)
doFilter:109, OncePerRequestFilter (org.springframework.web.filter)
internalDoFilter:193, ApplicationFilterChain (org.apache.catalina.core)
doFilter:166, ApplicationFilterChain (org.apache.catalina.core)
filterAndRecordMetrics:114, WebMvcMetricsFilter (org.springframework.boot.actuate.metrics.web.servlet)
doFilterInternal:104, WebMvcMetricsFilter (org.springframework.boot.actuate.metrics.web.servlet)
doFilter:109, OncePerRequestFilter (org.springframework.web.filter)
internalDoFilter:193, ApplicationFilterChain (org.apache.catalina.core)
doFilter:166, ApplicationFilterChain (org.apache.catalina.core)
doFilterInternal:200, CharacterEncodingFilter (org.springframework.web.filter)
doFilter:109, OncePerRequestFilter (org.springframework.web.filter)
internalDoFilter:193, ApplicationFilterChain (org.apache.catalina.core)
doFilter:166, ApplicationFilterChain (org.apache.catalina.core)
invoke:202, StandardWrapperValve (org.apache.catalina.core)
invoke:96, StandardContextValve (org.apache.catalina.core)
invoke:490, AuthenticatorBase (org.apache.catalina.authenticator)
invoke:139, StandardHostValve (org.apache.catalina.core)
invoke:92, ErrorReportValve (org.apache.catalina.valves)
invoke:74, StandardEngineValve (org.apache.catalina.core)
service:343, CoyoteAdapter (org.apache.catalina.connector)
service:408, Http11Processor (org.apache.coyote.http11)
process:66, AbstractProcessorLight (org.apache.coyote)
process:853, AbstractProtocol$ConnectionHandler (org.apache.coyote)
doRun:1587, NioEndpoint$SocketProcessor (org.apache.tomcat.util.net)
run:49, SocketProcessorBase (org.apache.tomcat.util.net)
runWorker:1149, ThreadPoolExecutor (java.util.concurrent)
run:624, ThreadPoolExecutor$Worker (java.util.concurrent)
run:61, TaskThread$WrappingRunnable (org.apache.tomcat.util.threads)
run:748, Thread (java.lang)
Copy the code

Focus on read verification of request parameters, First see org. Springframework. Web. Method. The annotation. ResolveArgument AbstractNamedValueMethodArgumentResolver class methods:

@Override
@Nullable
public final Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer, NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception {

    // Pay attention to this method call
	NamedValueInfo namedValueInfo = getNamedValueInfo(parameter);
	MethodParameter nestedParameter = parameter.nestedIfOptional();

	Object resolvedName = resolveStringValue(namedValueInfo.name);
	if (resolvedName == null) {
		throw new IllegalArgumentException(
				"Specified name must not resolve to null: [" + namedValueInfo.name + "]");
	}
	//
}
Copy the code

The getNamedValueInfo method is implemented as follows:

/** * Obtain the named value for the given method parameter. */
private NamedValueInfo getNamedValueInfo(MethodParameter parameter) {
	NamedValueInfo namedValueInfo = this.namedValueInfoCache.get(parameter);
	if (namedValueInfo == null) {
		namedValueInfo = createNamedValueInfo(parameter);
		namedValueInfo = updateNamedValueInfo(parameter, namedValueInfo);
		this.namedValueInfoCache.put(parameter, namedValueInfo);
	}
	return namedValueInfo;
}
Copy the code

When entering the createNamedValueInfo(parameter) method, this part of the code looks like this:

@Override
protected NamedValueInfo createNamedValueInfo(MethodParameter parameter) {
	RequestParam ann = parameter.getParameterAnnotation(RequestParam.class);
	return(ann ! =null ? new RequestParamNamedValueInfo(ann) : new RequestParamNamedValueInfo());
}

/** * The information about a named value, including name, whether it's required and a default value. */
protected static class NamedValueInfo {

	private final String name;

	private final boolean required;

	@Nullable
	private final String defaultValue;

	public NamedValueInfo(String name, boolean required, @Nullable String defaultValue) {
		this.name = name;
		this.required = required;
		this.defaultValue = defaultValue; }}Copy the code

The @requestParam annotation will only be read, not the @ApiIMPLicitParam annotation, so the @APIIMPLicitParam annotation will not affect the @RequestParam attribute, and whether requests come from Swagger Doc, CurrenctHashMap is used to store the result of the CurrenctHashMap. The key format is method ‘XXX’ parameter y, where XXX is the method name and y is the sequence number of the parameter. Method ‘auth’ parameter 0, for example, is almost guaranteed to be unique.

Stage summary

Read the source code here, basically can verify the first 2 mentioned before the small conclusion, quote:

  1. The @APIIMPLICITParam required parameter does not intrude on the @requestParam required value; the two are unrelated.
  2. The @APIIMPLICITParam required parameter affects the JS logic of Swagger Doc. Null validation is done at the JS level.
  3. The @requestParam required parameter checks only the name of the parameter, not its value, by default.

The first two questions have been explained from the source code, let’s look at the third question: If the parameter is set to required=true, but only requires the parameter name to exist, and if the field is of type Long or Integer, write uid= or ‘uid’, it will still pass the check. After finally entering the method, we still have to write the code manually for null check, which is obviously not what we want. How to solve it?

A problem with the request parameter data bind

In the next section, if such a general parameter, have to judge one by one whether empty, this approach is a little uncomfortable, is there a better solution? The desired effect is that Long or other numeric types can filter out “” and null if require=true. Otherwise, what is the point of require?

There are two solutions:

  1. A POST request encapsulates multiple parameters in a POJO class that is declared as @RequestBody. A POJO class can use @notnull, an annotation from the @Validator framework, and declare @VALID before the parameter.
  2. Custom parameter binding rule extension.

Scheme 2 is more general, applicable to GET and POST requests, and the original individual parameter declarations do not need to be encapsulated in POJO classes.

The website itself offers an extension for custom parameter binding, See https://docs.spring.io/spring/docs/5.1.8.RELEASE/spring-framework-reference/web.html#mvc-ann-initbinder

An example of using the @initBinder annotation in a specified Controller class is as follows:

@InitBinder
public void initBinder(WebDataBinder binder) {
	/* * Registers an editor that trims properties of String arguments. The * constructor argument indicates whether the null String is converted to null. False, null is converted to null. * /
	binder.registerCustomEditor(String.class, new StringTrimmerEditor(false));
	// Here I have added other types of property editors. True means "" is allowed and "" is treated as null. False means "" is not allowed.
	binder.registerCustomEditor(Short.class, new CustomNumberEditor(Short.class, false));
	binder.registerCustomEditor(Integer.class, new CustomNumberEditor(Integer.class, false));
	binder.registerCustomEditor(Long.class, new CustomNumberEditor(Long.class, false));
	binder.registerCustomEditor(Float.class, new CustomNumberEditor(Float.class, false));
	binder.registerCustomEditor(Double.class, new CustomNumberEditor(Double.class, false));
	binder.registerCustomEditor(BigDecimal.class, new CustomNumberEditor(BigDecimal.class, false));
	binder.registerCustomEditor(BigInteger.class, new CustomNumberEditor(BigInteger.class, false));
}
Copy the code

We need to create a new class with @controllerAdvice annotation as follows:

@ControllerAdvice
public class CustomWebBindingInitializer implements WebBindingInitializer {

	@InitBinder
	@Override
	public void initBinder(WebDataBinder binder) {
		/* * Registers an editor that trims properties of String arguments. The * constructor argument indicates whether the null String is converted to null. False, null is converted to null. * /
		binder.registerCustomEditor(String.class, new StringTrimmerEditor(false));
		// Here I have added other types of property editors. True means "" is allowed and "" is treated as null. False means "" is not allowed.
		binder.registerCustomEditor(Short.class, new CustomNumberEditor(Short.class, false));
		binder.registerCustomEditor(Integer.class, new CustomNumberEditor(Integer.class, false));
		binder.registerCustomEditor(Long.class, new CustomNumberEditor(Long.class, false));
		binder.registerCustomEditor(Float.class, new CustomNumberEditor(Float.class, false));
		binder.registerCustomEditor(Double.class, new CustomNumberEditor(Double.class, false));
		binder.registerCustomEditor(BigDecimal.class, new CustomNumberEditor(BigDecimal.class, false));
		binder.registerCustomEditor(BigInteger.class, new CustomNumberEditor(BigInteger.class, false)); }}Copy the code

Notice the passed false parameter initialized by the CustomNumberEditor instance.

Restart the application and see what it looks like:

DataBinder extension after the relevant source code to read

Now that we’re here, let’s take a look at the source code. Or in the org. Springframework. Web. Method. The annotation. AbstractNamedValueMethodArgumentResolver resolveArgument method during the second half of the class:

@Override
@Nullable
public final Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer, NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception {
    // the preceding is omitted
	if(binderFactory ! =null) {
		WebDataBinder binder = binderFactory.createBinder(webRequest, null, namedValueInfo.name);
		try {
		    // Convert the parameters here
			arg = binder.convertIfNecessary(arg, parameter.getParameterType(), parameter);
		}
		catch (ConversionNotSupportedException ex) {
			throw newMethodArgumentConversionNotSupportedException(arg, ex.getRequiredType(), namedValueInfo.name, parameter, ex.getCause());  }catch (TypeMismatchException ex) {
			throw new MethodArgumentTypeMismatchException(arg, ex.getRequiredType(),
					namedValueInfo.name, parameter, ex.getCause());

		}
	}

	handleResolvedValue(arg, namedValueInfo.name, parameter, mavContainer, webRequest);

	return arg;
}
Copy the code

With all the way down from the binder. ConvertIfNecessary method, intermediate omit some calls, Eventually reach org. Springframework. Beans. Propertyeditors. CustomNumberEditor setAsText method of a class:

/** * Parse the Number from the given text, using the specified NumberFormat. */
@Override
public void setAsText(String text) throws IllegalArgumentException {
	if (this.allowEmpty && ! StringUtils.hasText(text)) {// Treat empty String as null value.
		setValue(null);
	}
	else if (this.numberFormat ! =null) {
		// Use given NumberFormat for parsing text.
		setValue(NumberUtils.parseNumber(text, this.numberClass, this.numberFormat));
	}
	else {
		// Use default valueOf methods for parsing text.
		setValue(NumberUtils.parseNumber(text, this.numberClass)); }}Copy the code

Look closely at the allowEmpty variable. For arguments of type Long, when we extend the data binding, this variable is set to false, meaning that we do not accept null values. In the experiment, we passed an empty string, so the conditional branching here must convert the empty string to a value. Perform Long. The valueOf (” “) results reported a runtime exception. Java lang. A NumberFormatException, inform the client parameters is wrong, this is the desired outcome.

conclusion

At the beginning, I thought @ApiIMPLicitParam was intrusive to @RequestParam’s required attribute. I was surprised and went into the source code to prove my ideas. After reading the source code, I found that this was not the case. It was our initial misunderstanding of required. Given the limited use of required, there is no doubt that a general solution can be found to avoid manually writing code that nullates all parameters, solves a problem, finds a new problem, and then continues to solve it. If the analysis is not exhaustive, please correct it.

Focus on Java high concurrency, distributed architecture, more technical dry products to share and experience, please pay attention to the public account: Java architecture community can scan the left QR code to add friends, invite you to join the Java architecture community wechat group to discuss technology