moretti.fabio
moretti.fabio

Reputation: 1138

Spring filter: obtain servlet url-pattern

Imagine I have defined these REST endpoints:

@RequestMapping(path = "/user")
@RestController
public class ConfigUserController {

    [...]
    
    @GetMapping(path = "/variables/")
    public ResponseEntity<List<Variable>> getAllVariables(...)
        [...]
    }
    
    @GetMapping(path = "/variables/{name}")
    public ResponseEntity<Variable> getVariableByName(...)
        [...]
    }
    
    @PutMapping(path = "/variables/{name}/{value}")
    public ResponseEntity<Variable> setVariableByName(...)
        [...]
    }
}

I've defined two filter (logging and authorization), inside the filters I want to get the url-patter which matched current request. Using the example above:

Basically what I need is to get the path defined in the mapping annotation inside the doFilter(ServletRequest request, ServletResponse response, FilterChain chain) method of my filters.

Ideally I want to have do the possibility to access the url-pattern value in my filter like this:

@Component
@Order(3)
public class MyAuthorizationRequestFilter extends GenericFilterBean {
    [...]
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        // here I get the value
        String urlPattern = request.useSomeMethod().toGetUrlPattern();
        [...]
        boolean auth = false;
        // here I do stuff
        [...]
        // finally let's see if the user is authorized
        if (auth) {
            chain.doFilter(request, responde);
        } else {
            throw SomeAuthorizationError();
        }
    }
    [...]
}

I've been searching but I wasn't able to find a working solution.

If it's possible I prefer not to scan for collections or web.xml files (which I don't have), nor using reflections, because the code will be executed every time the filter get triggered and I don't want to affect performance.

Links or suggestions are welcome.

EDIT: added details, addes filter example cose

Upvotes: 4

Views: 3758

Answers (2)

Sibin Muhammed A R
Sibin Muhammed A R

Reputation: 1432

1. Solution using Servlet filter(after Filter doFilter) ## Here, Not recommending this solution.

Filters intercept requests before they reach the DispatcherServlet, making them ideal for coarse-grained tasks such as:

  • Authentication
  • Logging and auditing
  • Image and data compression
  • Any functionality we want to be decoupled from Spring MVC

Note* Do the doFilter first before trying to do the call to getAttribute

Your Controller

@RequestMapping(path = "/user")
@RestController
public class ConfigUserController {

    @GetMapping(path = "/variables/")
    public ResponseEntity<List<Variable>> getAllVariables() {
        return null;
    }

    @GetMapping(path = "/variables/{name}")
    public ResponseEntity<Variable> getVariableByName(@PathVariable("name") String name) {
        return new ResponseEntity<Variable>(new Variable(name), HttpStatus.OK);
    }

    @PutMapping(path = "/variables/{name}/{value}")
    public ResponseEntity<Variable> setVariableByName() {
        return null;
    }
}

Custom Filter

@Component
@Order(Ordered.HIGHEST_PRECEDENCE)
public class CustomFilter implements Filter {

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
            throws IOException, ServletException {
        // TODO
        try {
            chain.doFilter(request, response);
        } catch (Exception e) {
        } finally {
            String pattern = (String) request.getAttribute(HandlerMapping.BEST_MATCHING_PATTERN_ATTRIBUTE);
            System.out.println("Request template is, " + pattern);
        }
    }

}

O/P:

 Request template is, /user/variables/{name}

Note* Do the chain.doFilter first before trying to do the call to getAttribute


2. Solution using Interceptor

HandlerIntercepors intercepts requests between the DispatcherServlet and our Controllers. This is done within the Spring MVC framework, providing access to the Handler and ModelAndView objects.

Custom Interceptor

@Component
public class LoggerInterceptor implements HandlerInterceptor {

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object object, Exception arg3)
            throws Exception {

    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object object, ModelAndView model)
            throws Exception {

    }

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object object) throws Exception {
        String path = (String) request.getAttribute(HandlerMapping.BEST_MATCHING_PATTERN_ATTRIBUTE);
        System.out.println("path : " + path);
        return true;
    }}

Register it with InterceptorRegistry

@Component
public class CustomServiceInterceptorAppConfig implements WebMvcConfigurer {
   @Autowired
   LoggerInterceptor loggerInterceptor;

   @Override
   public void addInterceptors(InterceptorRegistry registry) {
      registry.addInterceptor(loggerInterceptor);
   }
}

enter image description here

3. Solution using Java Reflection API.

Beans that are singleton-scoped and set to be pre-instantiated (the default) are created when the container is created. It's a one time process, you don't worry about the performance.

You may pre-scan all @RequestMapping,@GetMapping, @PostMApping etc.. annotations, keep it and then in the filter match the pattern to get the desired result.

Here is the sample

Bean

@Component
public class Config {

    public List<String> getPatterns() throws ClassNotFoundException {

        List<String> str = new ArrayList<String>();
        Class clazz = Class.forName("com.example.demo.controller.ConfigUserController"); // or list of controllers,
                                                                                            // //you can iterate that
                                                                                            // list
        for (Method method : clazz.getMethods()) {
            for (Annotation annotation : method.getDeclaredAnnotations()) {
                if (annotation.toString().contains("GetMapping")) {
                    str.add(annotation.toString());
                    System.out.println("Annotation is..." + annotation.toString());
                }
                if (annotation.toString().contains("PutMapping")) {
                    str.add(annotation.toString());
                    System.out.println("Annotation is..." + annotation.toString());
                }
                if (annotation.toString().contains("PostMapping")) {
                    str.add(annotation.toString());
                    System.out.println("Annotation is..." + annotation.toString());
                }
            }
        }

        return str;

    }

Custom Filter

@Component
@Order(Ordered.HIGHEST_PRECEDENCE)
public class CustomFilter implements Filter {
    @Autowired
    Config con;
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
            throws IOException, ServletException {
        // TODO
        try {
            //filter match the pattern to get the desired result
            System.out.println(con.getPatterns());
        } catch (ClassNotFoundException e) {
            
        }
        chain.doFilter(request, response);
    }

Upvotes: 6

Binu George
Binu George

Reputation: 1070

Updating the answer to use GenericFilterBean.doFilter as per the question asked. You can do this by adding a finally clause. This is needed since you need to do chain.doFilter(request, response) first before calling request.getAttribute because the attribute is not set until later in the lifecycle of the request. So if you are trying to do some operations based on incoming uri template, subclassing GenericFilterBean may not be the right choice here since you need to call chain.doFilter first.

 public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        try {
            //Do something
            chain.doFilter(request, response);
        } finally {
         String pathTemplate = (String) 
request.getAttribute(HandlerMapping.BEST_MATCHING_PATTERN_ATTRIBUTE);
         System.out.println("Incoming Request path Template from Generic Filter : " + pathTemplate);
}


}

Alternate options:

Assuming that you are using spring web here. You can define an HandlerInterceptor and register it with InterceptorRegistry.

@Component
public class LogPathTemplateInterceptor
        implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object object) throws Exception {
        String pathTemplate = (String)request.getAttribute(HandlerMapping.BEST_MATCHING_PATTERN_ATTRIBUTE);
        System.out.println("Incoming Request path Template : " + pathTemplate);
        return true;
    }
}

After defining this, register it with InterceptorRegistry. See how the path pattern added to the interceptor.

@Configuration
public class InterceptorConfig implements WebMvcConfigurer {
    @Autowired
    private LogPathTemplateInterceptor logInterceptor;
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(logInterceptor)
                .addPathPatterns("/variables/**");
    }
}

This should log the request template path instead of path with variables.

Upvotes: 2

Related Questions