Network Security Internet Technology Development Database Servers Mobile Phone Android Software Apple Software Computer Software News IT Information

In addition to Weibo, there is also WeChat

Please pay attention

WeChat public account

Shulou

How to deal with @ ApiImplicitParam's intrusion into @ RequestParam's required attribute?

2025-02-24 Update From: SLTechnology News&Howtos shulou NAV: SLTechnology News&Howtos > Development >

Share

Shulou(Shulou.com)06/02 Report--

This article mainly explains "@ ApiImplicitParam how to solve the required attribute intrusion of @ RequestParam". Interested friends may wish to have a look at it. The method introduced in this paper is simple, fast and practical. Now let the editor take you to learn "@ ApiImplicitParam how to solve the intrusion of the required attribute of @ RequestParam"!

The origin of the problem

When building a project using SpringCloud, it is recommended to use Swagger to generate corresponding interface documents. Swagger can provide page access and debug the interface of the back-end system directly on the web page, which is very convenient. Recently, however, we have encountered a somewhat confusing problem. The example of the demonstration interface is as follows (the original functional interface has business implementation logic, which 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

}

}

The problem lies in the required requirement of the interface parameter uid. The @ RequestParam annotation defaults to true and requires it to be required, but the @ ApiImplicitParam annotation defaults to false and is not required. When the business interface carries out joint function debugging, uid can get a null value. According to the general cognitive habit, the main function of @ ApiImplicitParam annotation is to generate interface documents, which should not be intrusive to the attributes of @ RequestParam. The current feedback bug Makes me wonder if @ ApiImplicitParam will hack into the require attribute of @ RequestParam?

Framework selection, version and main function project construction

SpringBoot version: 2.1.6.RELEASESpringCloud version: Greenwich.SR3

Service module

Swagger used by SpringCloud service module:

Swagger bootstrap ui 1.9.6 enhanced swagger ui style spring4all-swagger 1.9.0.RELEASE configuration swagger parameters, eliminating code development

Service gateway

Swagger used by SpringCloud service gateway:

Knife4j 2.0.1 enhanced swagger ui style (gateway is built with gateway, swagger uses knife4j-spring-boot-starter dependency, and swagger documents of business modules can be aggregated)

This time, the scope is only for the SpringCloud business module, and does not involve the Swagger documents of the service gateway for the time being.

Testing tool

Currently, there are two testing tools: swagger doc: access using a browser, as shown below:

Postman: configure API parameters manually, for example:

Case practice Interface Test 1

As shown in the opening section, we first use the following APIs, all using default values, that is, @ ApiImplicitParam has a required of false,@RequestParam and a required of 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

}

Look at the results of swagger:

Look at the results of postman:

Interface Test 2

Let's change the value of @ ApiImplicitParam to true,@RequestParam and restart the module @ ApiImplicitParam (name = "uid", value = "user ID", paramType = "query", required = true, dataType = "Long")

Look at the results of swagger:

By debugging the browser, we can find that the null check is completed by js, and after js determines it to be empty, it does not initiate a request to the backend, so we can think that the required parameter @ ApiImplicitParam in the swagger is valid.

Interface Test 3

Earlier, when we used the postman test interface, we found that the parameter item was empty. After we added the parameter, but did not write the value test, the result was surprising:

And no matter how the value of @ ApiImplicitParam is modified, the result is the same. there must be something wrong, which leads to our misjudgment.

Later, when we carefully checked the data, we found that we misunderstood the required parameter of @ RequestParam. This required means true: the name of the interface parameter must exist, but it doesn't matter whether there is a value after the parameter. Take the example just now:

These two requests are approved:

Localhost:8080/api/json//demo?uid

Localhost:8080/api/json//demo?uid=

Only this kind of request is not approved:

Localhost:8080/api/json//demo?

Small conclusion

Through the test scenarios of the above three interfaces, we can make at least three points clear:

The required parameter of @ ApiImplicitParam does not intrude on the read value of @ RequestParam, and they are not related.

The required parameter of @ ApiImplicitParam will affect the js logic judgment of swagger doc. The null check is done at the js level.

By default, the required parameter of @ RequestParam only verifies whether it has the parameter name, not whether it has a value.

Source code analysis swagger part

In the previous section, it was mentioned that swagger reads the required parameters of the @ ApiImplicitParam annotation, which will eventually be reflected on js and tracked to the swaggerbootstrapui.js file by browser F12. Here is an excerpt of some source codes:

# when you click the send button, read the parameter information line by line and extract the required parameter

ParamBody.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) {

/ / if selected, pay attention to the required:paramtr.data ("required") information extraction for this line

Var trdata= {name:paramtr.find ("td:eq (2)") .find ("input"). Val (), in:paramtr.data ("in"), required:paramtr.data ("required"), type:paramtr.data ("type"), emflag:paramtr.data ("emflag"), schemavalue:paramtr.data ("schemavalue")}

/ / that.log ("trdata....")

/ / that.log (trdata)

/ / obtain key

/ / var key=paramtr.find ("td:eq (1)") .find ("input") .val ()

Var key=trdata ["name"]

/ / obtain value

Var value= ""

Var reqflag=false

/ / the following code is omitted

}

})

To determine whether the attribute required is processed by true on js, the js source code is as follows:

/ / determine whether it is required

If (trdata.hasOwnProperty ("required")) {

Var required=trdata ["required"]

If (required) {

If (! reqflag) {

/ / must verify whether value is empty

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

}

}

}

}

SpringCloud service module part

Swagger frontend js verifies that when you can send a request to the backend or use postman to send a request to the backend system, you can start a series of filters and Servlet processing in the background, and there are many things to do:

/ / the actual business method 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)

/ / extraction and control of request parameters

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)

/ / the following are the filters of various basic Web service components, which don't care for the time being.

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)

The focus of aggregation is on the reading and verification of request parameters. First, look at the resolveArgument method of the org.springframework.web.method.annotation.AbstractNamedValueMethodArgumentResolver class:

@ Override

@ Nullable

Public final Object resolveArgument (MethodParameter parameter, @ Nullable ModelAndViewContainer mavContainer)

NativeWebRequest webRequest, @ Nullable WebDataBinderFactory binderFactory) throws Exception {

/ / Note 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 + "]")

}

/ / temporarily omitted later

}

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

}

When entering the createNamedValueInfo (parameter) method, this part of the code is as follows:

@ Override

Protected NamedValueInfo createNamedValueInfo (MethodParameter parameter) {

RequestParam ann = parameter.getParameterAnnotation (RequestParam.class)

Return (ann! = null? New RequestParamNamedValueInfo (ann): new RequestParamNamedValueInfo ()

}

/ * *

* definition of NamedValueInfo

* Represents 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

}

}

This code is critical. Only @ RequestParam annotations are read here, not @ ApiImplicitParam annotations, so @ ApiImplicitParam annotations do not affect the attributes of @ RequestParam, and no matter whether they are requests from swagger doc or postman, this code is executed. Finally, the result of reading annotations is stored in CurrenctHashMap. The format of key is method 'xxx' parameter ydjingxxx as the method name and y as the sequence number of the parameter, such as method' auth' parameter 0. It can basically guarantee uniqueness.

Phased summary

After reading the source code here, you can basically verify the first two conclusions mentioned above, to quote:

The required parameter of @ ApiImplicitParam does not intrude on the read value of @ RequestParam, and they are not related.

The required parameter of @ ApiImplicitParam will affect the js logic judgment of swagger doc. The null check is done at the js level.

By default, the required parameter of @ RequestParam only verifies whether it has the parameter name, not whether it has a value.

The first two questions have been explained in the source code, let's look at the third question: if the parameter is set to required=true, but only the parameter name is required to exist, if this field is of Long type or Integer type, it can also be written as uid= or 'uid',. After finally entering the method, we still have to manually write the code for null verification, which is obviously not the result we want? How to solve the problem?

The problem of requesting parameter data bind

In the previous section, if such a general parameter has to be judged one by one whether it is empty or not, this is a bit uncomfortable. Is there a better solution? The expected implementation effect is that after the field is added with require=true, the Long type or other numeric type can filter out the "", null. Otherwise, what's the point of require?

There are two ways to solve the problem:

The POST request method encapsulates multiple parameters in a POJO class, declares with @ RequestBody that annotations such as @ NotNull of the @ Validator framework can be used in the POJO class, and declares @ Valid before the parameters.

Custom parameter binding rule extension.

Scenario 2 is more generic, applicable to GET and POST requests, and the original single parameter declaration does not need to be encapsulated in the POJO class.

The official website itself provides an extension of custom parameter binding, see https://docs.spring.io/spring/docs/5.1.8.RELEASE/spring-framework-reference/web.html#mvc-ann-initbinder

The example of the official website is the use of @ InitBinder annotation in the specified Controller class. The scope of influence is limited to this Controller class. The example is as follows:

@ InitBinder

Public void initBinder (WebDataBinder binder) {

/ *

* register an editor that performs trim operations on properties of String type parameter objects

* if the construction parameter indicates whether an empty string is converted to null,false, the null will be converted to an empty string.

, /

Binder.registerCustomEditor (String.class, new StringTrimmerEditor (false))

/ / I have also added other types of attribute editors here. True means to allow the use of "" and treats "" as empty, and false means that it is not allowed to use "".

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))

}

Since the problem we are facing this time is the value of full module @ RequestParam, we need to make a global configuration. In this case, we need to add a new class and use the @ ControllerAdvice annotation as follows:

@ ControllerAdvice

Public class CustomWebBindingInitializer implements WebBindingInitializer {

@ InitBinder

@ Override

Public void initBinder (WebDataBinder binder) {

/ *

* register an editor that performs trim operations on properties of String type parameter objects

* if the construction parameter indicates whether an empty string is converted to null,false, the null will be converted to an empty string.

, /

Binder.registerCustomEditor (String.class, new StringTrimmerEditor (false))

/ / I have also added other types of attribute editors here. True means to allow the use of "" and treats "" as empty, and false means that it is not allowed to use "".

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))

}

}

Notice the passed false parameters initialized by the CustomNumberEditor instance.

Restart the application to see the effect:

Read the related source code after extending DataBinder

It's all here, so take a closer look at the relevant source code, which is still in the second half of the resolveArgument method of the org.springframework.web.method.annotation.AbstractNamedValueMethodArgumentResolver class:

@ Override

@ Nullable

Public final Object resolveArgument (MethodParameter parameter, @ Nullable ModelAndViewContainer mavContainer)

NativeWebRequest webRequest, @ Nullable WebDataBinderFactory binderFactory) throws Exception {

/ / previous omission

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 new MethodArgumentConversionNotSupportedException (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

}

Follow the binder.convertIfNecessary method all the way, omitting some calls in the middle, and finally reaching the setAsText method of the org.springframework.beans.propertyeditors.CustomNumberEditor 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))

}

}

Look at the allowEmpty variable carefully. When we expand the data binding for parameters of type Long, the variable is set to false, which means that null values are not accepted. In the experiment, the value we passed is an empty string, then the conditional branch judgment here must convert the empty string into a numerical value, and execute Long.valueOf ("") result to report the run-time exception java.lang.NumberFormatException, informing the client that the parameters are wrong, which is the desired result.

At this point, I believe you have a deeper understanding of "@ ApiImplicitParam how to solve the required attribute intrusion of @ RequestParam". You might as well do it in practice. Here is the website, more related content can enter the relevant channels to inquire, follow us, continue to learn!

Welcome to subscribe "Shulou Technology Information " to get latest news, interesting things and hot topics in the IT industry, and controls the hottest and latest Internet news, technology news and IT industry trends.

Views: 0

*The comments in the above article only represent the author's personal views and do not represent the views and positions of this website. If you have more insights, please feel free to contribute and share.

Share To

Development

Wechat

© 2024 shulou.com SLNews company. All rights reserved.

12
Report