preface

Recently, colleagues were too busy to interview candidates, and occasionally asked me to fill in, so I met with a junior girl and two (self-described) senior developers as fill-in interviewers. It’s my habit to interview candidates and ask them whatever they put on their resume. They all said they were familiar with SpringMVC on their resumes, so I asked two random questions and answered them poorly

  • The execution flow of SpringMVC
  • Front end of transmission? username=xx&password=xxcontrollerIt can be used automaticallyUserObject to receive, how is this implemented?

The first question should be relatively simple, casually back on the back of good, however two advanced development is also the answer of the very vague……

Second problem for the primary development may be relatively difficult, but for 5 to 10 years of senior development even didn’t answer, I think it is should not, my request is very simple, is not to the candidate tell a complete source process, as long as the candidates can replied parser/type converter parameters, I can think of this question.

PS most let me sad is the candidate pre-tax annual salary 40W +, expected annual salary 50W + are nearly three times my…… However, none of these basics can be answered, and the application scenarios of message queuing and caching middleware are vague enough to be answered by any question. So I can only comfort myself, they must be to face the management post! High salary normal…… Yeah that’s it!!

The soul of torture

Let’s start with a very common piece of code

@GetMapping("/example/{id}")
public void example(@PathVariable Long id,
                    @RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) LocalDateTime dateTime,
                    @RequestParam List<Long> ids,
                    @RequestParam Boolean enable,
                    @RequestParam ExampleEnum exampleEnum,
                    User user) {
    log.info(id.toString());
    log.info(dateTime.toString());
    log.info(ids.toString());
    log.info(enable.toString());
    log.info(exampleEnum.toString());
    log.info(user.toString());
}
Copy the code

Then we write a test to simulate an HTTP request

@Test void example() throws Exception { mvc.perform( get("/example/1") .param("dateTime", "The 2020-09-02 T16:00:01onsaturday (UK time)"). The param (" ids ", "1, 2, 3"). The param (" enable ", "no"). The param (" exampleEnum ", "ONE"). The param (" username ", "Twilight Enchanting ").andexpect (status().isok ()); }Copy the code

Now look at the printed log

: 1:2020-09-02T16:00:01: [1, 2, 3] : false: ONE: User(username= dusk enchanting 丶Copy the code

We write code like this almost every day, but have you ever thought about it

  • Why?1, 2, 3Automatic conversionList<Long>?
  • Why?noCan be converted to a Boolean typefalse?
  • Why?usernameIt can be automatically encapsulateduserIn the?
  • Why?ONEIt can be automatically encapsulatedExampleEnumIn the?

If you ask me, why don’t I just use it? Why is it important to understand how it is implemented? So jump right here and customize the parameter parser to optimize the paging code

SpringMVC executes the process

Before we can figure that out, we need to know what SpringMVC is doing between the time it receives a request and the time it enters the controller method body. This process online search is everywhere, in fact, to debug the source code is very clear, roughly the following steps

The handle method that handles the request contains a very important step: resolveArgument(), which resolves the parameters of our original request into the type of parameters that our controller method really needs. So how does it parse? Which brings us to SpringMVC’s parameter parser.

Parameter parser

SpringMVC provides 20+ parameter parsers to parse @requestParam, @RequestBody, and @pathVariable…… Request parameters such as annotations.

Is the top interface HandlerMethodArgumentResolver, all parameters of the parser implements this interface, observe its source code

public interface HandlerMethodArgumentResolver { /** * ... */ boolean supportsParameter(MethodParameter parameter); / * * *... */ @Nullable Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer, NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception; }Copy the code

This is a typical policy mode interface that determines whether the current method parameter type is supported and, if so, parses it using the parameter parser of the implementation class. As you’re smart enough to know, it’s obvious that we can implement this interface to customize our own parameter resolver.

This makes it obvious that SpringMVC provides a set of parameter parsers to help us achieve such intelligent transformations. Just look at the implementation in the parameter parser. When it comes to implementation, Spring core type converters have to be mentioned

Type converter

Spring 3.0 introduced a core-.convert package that provides a common type conversion system. The system defines an SPI to implement type conversion logic and an API to perform type conversion at run time. This type conversion system mainly has the following core interfaces

Converter

Look at the source code of the top-level interface Converter

@FunctionalInterface
public interface Converter<S, T> {
    @Nullable
    T convert(S var1);
}
Copy the code

The interface is used to convert type S to T, and the implementation of the interface is used in the parameter parser to convert the front-end request parameters to the parameter types required by the controller method. But in many cases, we actually want to convert one type to a set of types, such as the previous example that converted String to Enum, for which Spring provides another interface, ConverterFactory

ConverterFactory

Look at the source code

public interface ConverterFactory<S, R> {
    <T extends R> Converter<S, T> getConverter(Class<T> var1);
}
Copy the code

The interface is used to convert type S to R and its subtypes (see the source code of the premise of generics must be mastered, if you are not familiar with generics can see my article with you to understand Java generics).

ConversionService

ConversionService defines a unified API for performing type conversion logic at run time and viewing its source code

public interface ConversionService { boolean canConvert(@Nullable Class<? > var1, Class<? > var2); boolean canConvert(@Nullable TypeDescriptor var1, TypeDescriptor var2); @Nullable <T> T convert(@Nullable Object var1, Class<T> var2); @Nullable Object convert(@Nullable Object var1, @Nullable TypeDescriptor var2, TypeDescriptor var3); }Copy the code

Source code can be said to be two methods, the first judge can convert, if you can execute the real convert logic. All Converters work silently behind The ConversionService, so here is an embodiment of the appearance pattern (the Spring family source code is full of design patterns……)

Spring provides Converter

Taking the example code,

  • Ids – > 1, 2, 3 can use a List < Long > provides StringToCollectionConverter receive because of the Spring

  • Enable -> no can be received with Boolean because Spring provides StringToBooleanConverter. All of the following values can be received with Boolean

static {
    trueValues.add("true");
    trueValues.add("on");
    trueValues.add("yes");
    trueValues.add("1");
    falseValues.add("false");
    falseValues.add("off");
    falseValues.add("no");
    falseValues.add("0");
}
Copy the code
  • exampleEnum -> ONETo be able to useExampleEnumReceive because Spring providesStringToEnumConverterFactory. thisconvertThe method body is even simpler
@Nullable
public T convert(String source) {
    return source.isEmpty() ? null : Enum.valueOf(this.enumType, source.trim());
}
Copy the code

All support provided by the Spring of the type of the converter implementation classes are org. Springframework. Core. The convert. Under the support package, interested can read the source code, a lot of converter of the convert method is very simple ~ ~

Converter provided by SpringBoot

Providing the type of the converter in the Spring, SpringBoot and provide us with some converter implementation to simplify the development of the converter in the org. Springframework. Boot. Convert package, The most common StringToDurationConverter, for example, have you ever wondered why you in the application. The 7 d such a string that is configured in the yml, can use Duration in configuration class map?

auto-review-loan:
  effective-time: 7d  
Copy the code
@ConfigurationProperties(prefix = AutoReviewLoanProperties.PREFIX) @Component @Data public class AutoReviewLoanProperties { public static final String PREFIX = "auto-review-loan"; /** effectiveTime = Duration. OfDays (7); /** effectiveTime = Duration. }Copy the code

Double StringToDurationConverter knew ~ source

Custom parameter parsers optimize paging codes

Case scenario

Imagine a scenario where your company has recently introduced the MyBatisPlus component. How do you code when you use the IPage page that comes with it? Most of the way you write it is probably like this

public IPage<Response> page(@RequestParam Integer pageNum , @RequestParam Integer pageSize) {
  IPage<Response> page = new Page<>(pageNum,pageSize);
  return service.page(page);
}
Copy the code

Or it could be

public IPage<Response> page(PageRequest request) {
  IPage<Response> page = new Page<>(request.getPageNum(),request.getPageSize());
  return service.page(page);
}
Copy the code

Of course, there’s nothing wrong with either of these, but if you do a lot of them you’ll notice that this line of code to get a paging object is the same, that each paging interface has to write the following line of code, okay

IPage<Response> page = new Page<>(pageNum,pageSize)
Copy the code

In line with DRY(Dont Repeat Yourself), is there a way to get rid of this repetitive line of code? As you might have figured out, we could try putting the paging object IPage page; The pageNum and pageSize parameters cannot be converted to IPage directly. Remember we have said above, anyway it parameters parsing rules are parser to do, then I realize HandlerMethodArgumentResolver himself wrote a parameter parser to implement this function is not ok ~ ~

Custom parameter resolver

Notice the paging field in the realization of the IPage type of Page, so our strategy to support parameter parser is Page. The class, two methods to realize the core code is HandlerMethodArgumentResolver. Observed in the project have introduced spring – data – common dependence, it has provide us with a useful parameter PageableHandlerMethodArgumentResolver parser implementation class, it can be used directly

/ * * * / public custom paging parameters the parser class PageHandlerMethodArgumentResolver < T > implements HandlerMethodArgumentResolver {private static final int DEFAULT_PAGE = 0; private static final int DEFAULT_SIZE = 10; private static final String DEFAULT_PAGE_PARAMETER = "pageNum"; Private static final String DEFAULT_SIZE_PARAMETER = "pageSize"; private static final String DEFAULT_SIZE_PARAMETER = "pageSize"; Private Final PageableArgumentResolver PageableArgumentResolver; // Create pageableArgumentResolver, set the page field name, The default size attributes such as public PageHandlerMethodArgumentResolver () {PageableHandlerMethodArgumentResolver resolver = new PageableHandlerMethodArgumentResolver(); resolver.setFallbackPageable(PageRequest.of(DEFAULT_PAGE, DEFAULT_SIZE)); resolver.setSizeParameterName(DEFAULT_SIZE_PARAMETER); resolver.setPageParameterName(DEFAULT_PAGE_PARAMETER); resolver.setOneIndexedParameters(true); this.pageableArgumentResolver = resolver; } // The method argument type supported by the parameter parser is Page @override public Boolean supportsParameter(MethodParameter parameter) {return Page.class.equals(parameter.getParameterType()); } // @override public Object resolveArgument(@nonnull MethodParameter parameter, ModelAndViewContainer mavContainer, @NonNull NativeWebRequest webRequest, WebDataBinderFactory binderFactory) { Pageable pageable = pageableArgumentResolver.resolveArgument( parameter, mavContainer, webRequest, binderFactory); return new Page<T>(pageable.getPageNumber() + 1L, pageable.getPageSize()); }}Copy the code

Once the argument parser is defined, we need to add it to the SpringMVC parser collection by overriding addArgumentResolvers() with a configuration class that implements the WebMvcConfigurer interface

/ * * add custom paging parameters parser * / @ Override public void addArgumentResolvers (List < HandlerMethodArgumentResolver > resolvers) { resolvers.add(new PageHandlerMethodArgumentResolver<>()); }Copy the code

With that done, we can now use (Page Page) on the controller method to receive pageNum and pageSize request parameters, as shown below

@GetMapping("/xxx")
public IPage<Response> page(Page<Response> page) {
  return service.page(page);
}
Copy the code

After testing, it does achieve our effect, GET/XXX? PageNum =1&pageSize=10 The requested paging parameters are indeed parsed into Page objects, but the new problem is that the Swagger Page displays a lot of useless information because there are many other properties in the Page class.

Optimize the presentation of paging objects on Swagger pages

Components that integrate Swagger all provide type substitution, so we can replace one type with another to display on the Swagger page. Create a new paging parameter entity class

@data @parameterObject Public class PageRequest {@parameter (description = "number of records per page ", example = "10") private Integer pageSize = 10; @parameter (description = "page number ", example = "1") private Integer pageNum = 1; }Copy the code

If you are using the SpringFox component, you can replace it as follows

@Bean public AlternateTypeRuleConvention pageAlternateTypeRuleConvention(final TypeResolver resolver) { return new AlternateTypeRuleConvention() { @Override public int getOrder() { return Ordered.HIGHEST_PRECEDENCE; } /** Replace Page type with PageRequest on swagger Page, Show only the pageNum and pageSize two parameters * / @ Override public List < AlternateTypeRule > rules () {return Collections. SingletonList ( AlternateTypeRules.newRule( resolver.resolve(Page.class, WildcardType.class), resolver.resolve(PageRequest.class))); }}; }Copy the code

If you are using a SpringDoc component, you can replace it as follows

/* Note that the PageRequest Page type is changed to PageRequest. */ springdocutils.getConfig ().replaceWithClass(page.class, pagerequest.class); }Copy the code

Look at the Swagger page after the replacement has achieved the effect

Note that since the Swagger Page has replaced Page with PageRequest, the paging interface return value can no longer use Page but IPage

conclusion

After the above cases can be found, only to have a certain understanding of the principle of open source components, it can be extended to solve problems ~~ more importantly, it is able to continue to optimize and iterate the existing projects, in order to let the leadership of you sit up and take notice ah, promotion and salary is just around the way!!

conclusion

If this article has helped you, please like it and follow it! Your support is my motivation to continue to create!