preface

Recently I joined a new company and took over a certification project temporarily. For those who have obsessive-compulsive disorder of code elegance, there is no doubt that they will change the code when they see it. To change! To change! However, after the change, the front end gave me feedback that the interface always reported 401 error. Inside me: Fuck me? Did I fix a bug? Shouldn’t be, how can such a simple thing have a bug! So I tested the next, it is really a problem, but not my problem, the following began to analyze!

Pseudocode scenario restoration

Login interface, simulated error

@PostMapping("/user/login") public LoginResult login(@RequestBody LoginRequest request) { throw new RuntimeException(" Simulated login interface error "); }Copy the code

It then posts an interceptor and throws an exception if the request for authentication does not carry a token, or if the user associated with that token is not found in Redis

public class UserLoginInterceptor implements HandlerInterceptor { @Override public boolean preHandle( HttpServletRequest  request, HttpServletResponse response, Object handler) { String token = request.getHeader("token"); If (token == null) {throw new UnauthorizedException(" unauthenticated or token has expired "); } else {if(redis.get(token) == null) {throw new UnauthorizedException(" unauthenticated or token has expired "); } / /... Set token and user information to ThreadLocal} return true; }}Copy the code

Interceptor configuration

@Configuration public class WebConfig implements WebMvcConfigurer { @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new UserLoginInterceptor()) .excludePathPatterns("/user/login") .addPathPatterns("/**"); }}Copy the code

This project is to obtain the token from the interceptor to Redis to check the user information and put it into ThreadLocal. Because a request from Controller → Service → Mapper thread ID is consistent, Such a chain of requests can get the current logged-in user information from the ThreadLocal. It can be seen that /user/login is allowed by the interceptor. However, when the Controller of this request reported an error, the expected message should be the simulated login interface error. However, the following unauthenticated error was reported, indicating that our request went to the interceptor

{" path ":"/error ", "message" : ". Com. Yinshan auth. Core. Exception. UnauthorizedException: unauthorized or token expired ", "error" : "Unauthorized", "status": 401, "TIMESTAMP ": "2021-09-22T14:03:39.986559500"}Copy the code

Of course, this error message format is my own processing, this is not important, the main point is that I reported 500 error in the login interface, why the interception of 401 unauthenticated.

Debug analysis

Without further ado, debug directly starts by making a breakpoint in the code that throws the exception, and then making a breakpoint in the interceptor

It turns out that after the login interface presses F9, the breakpoint does go into the interceptor,

To be honest, I was like, what the hell is this interface that’s already been cleared by the interceptor? However, a closer look at the preHandle request parameter details in the debug panel revealed something fishy.

The arrows in the picture point to important information:

  • Is the context of the current request, which is not available when the interceptor is normally requested
  • The distribution type of the request. The value of a normal request isREQUEST
  • What stands out is the request resourceuriI didn’t even ask for it/user/loginBut a man/error

As you can see here, the breakpoint goes to the interceptor, not because of the /user/login request, but because of another /error request. So where does this /error come from? Since the TomcatEmbededContext context in the figure is a class in Tomcat embedded in SpringBoot, I assume that the request is an internal /error request re-initiated by the SpringMVC controller when it encounters an unhandled error.

You might wonder, isn’t that the problem? That sounds fast. Why did it take you two hours? Because OF my food! F8 → F7 → F8 → F7…… Pass through the six… When I finally debugged to DispatchServlet, I realized how the hell it went to request forwarding. Finally, I realized that I was numb.

Querying official Documents

Sure enough, you can find instructions in the official SpringMVC documentation

If the exception is not handled by the default exception handler, the Servlet container will dispatch a /error request with DispatchServlet. You can also customize the /error request. See the official SpringMVC documentation for details

The specific reason

SpringMVC controller says something wrong and the server makes a /error request, and since our interceptor didn’t allow this /error request, So the interceptor that will execute the request in DispatchServlet (I suddenly remembered writing a custom SpringBoot exception page two years ago that handled /error requests)

protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception { //... // Determine and execute the interceptor preHandle() if (! mappedHandler.applyPreHandle(processedRequest, response)) { return; }Copy the code

Above is the source code of doDispatch part of DispatchServlet. I believe that most people’s understanding of doDispatch is stuck in the execution process of SpringMVC for interview. In fact, online for SpringMVC execution process drawing diagram are a few key nodes, and not so detailed, if there is no real problem with the debugging of the source code, then the probability is also do not understand the problem.

The solution

Once you understand the cause of the problem, the solution is simple. Simply exclude /error interceptions in our custom authentication interceptor

@Override
public void addInterceptors(InterceptorRegistry registry) {
    registry.addInterceptor(new UserLoginInterceptor())
            .excludePathPatterns("/error").addPathPatterns("/**");
}
Copy the code

Talk about interceptors

In fact, a large part of the above problem is due to no real understanding of interceptors, just knowing that they can intercept a request, without studying at what stage it intercepts and how it is implemented in SpringMVC. So let’s take a closer look at interceptors

Range of interceptors and filters

Check the Filter interface source code can be found, it is javax.mail servlet package, and HandlerInterceptor is org. Springframework. Web. Servlet package, interceptors are implemented for SpringMVC, In fact, it is just a combination of one or more Java classes to implement interception, and is not necessarily associated with web applications. This means that filters can only be used in Web applications, while interceptors can be used anywhere Spring and SpringMVC are available, such as desktop applications.

Execution sequence & execution flow of interceptors and filters

Filter is performed before the request to the Servlet through ApplicationFilterChain. DoFilter () chain of the call, the doFilter () internal access to the next filter as an example, filtering method, Its execution order is filter1 – ApplicaitonFilterChain doFilter () – filter2 – ApplicationFilterChain. DoFilter () – filter3 -…

The following figure

The execution of the interceptor is something done before and after the request arrives at the DispatchServlet for the Controller method, as shown in the figure below, where the filter chain is shown above

PreHandle () is obviously the key to interception, and it is only executed before the request reaches the Controller target method, which determines whether the request needs to be intercepted by returning true/false.

DoDispatch internal interceptor processing part of the source

We all know that the doDispatch() method of DispatchServlet handles all requests, and the internal interceptor-specific code is as follows

// Execute interceptor preHandle() if (! mappedHandler.applyPreHandle(processedRequest, response)) { return; } mv = ha.handle(processedRequest, response, mappedHandler.getHandler()); // Reflection calls Controller target method /** *... Omit * * / mappedHandler. ApplyPostHandle (processedRequest, response, mv); AfterCompletion () processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);Copy the code

In fact, the real problem with debugging source code, there is no need to back SpringMVC execution process interview questions ~~~ I can not back down, but from the source debugging process, I have been very clear DispatchServlet in the request forwarding process are doing those things, In combination with hibernate-Validator and unified exception handling, we can see how SpringMVC implements the parsing and conversion of request parameters.

conclusion

Don’t panic when you encounter problems, source debugging is not that difficult, I think with problems to look at the source can be more impressive. Less than a month to the new company, I have taken the problem to see the source several times…… Just in time for big versions of technical components, there are always all kinds of weird problems.

It’s always a good idea to read the official documentation of frameworks and technical components, not just video tutorials. Read the official documentation to find out possible component problems and causes.