6

I'm trying to reserve all paths starting with /static/** for a resource handler. Unfortunately, I have some wildcards deriving out of the root path / in request mappings. Something like this:

Preview

What did I try?

  • ResourceHandlerRegistry#setOrder:

    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        registry.addResourceHandler("/static/**")
                .addResourceLocations("classpath:/resources/static/");
    
        registry.setOrder(1);
    }
    
  • Various versions of interceptor (with or without order):

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new ResourcesInterceptor())
                .excludePathPatterns("/static/**")
                .order(2);
    }
    

That is a half-hearted success (probably it won't even work if I change mapping to /{profile}/{project}/**), because:

/static/style/home.css      # works
/static/style/home.cssxxx   # 404, works
/static/style               # catched by controller, expected: 404
/static                     # catched by controller, expected: 404

I've found some similar questions, mostly unanswered or with a little dirty solutions, like:

Summary:

  • I don't want to use regex, because it will be painful in the future
  • I also can't change mappings. I know, that it is the simplest way, but I'm just not able to do that.
  • Changing an order not working
  • Creating dedicated controller still have some issues with paths

I'm looking for a simple soulution, fully automated and preferably from the configuration. The question is: what's the proper way to achieve that?

dzikoysk
  • 1,560
  • 1
  • 15
  • 27
  • 4
    Very cute image! :3 What tool have you used to draw it? – Nikolas Charalambidis Aug 09 '18 at 13:25
  • 1
    Gimp + Gaegu font :) – dzikoysk Aug 09 '18 at 13:28
  • 1
    Simplest solution: change your mapping from `/{profile}` to `/profiles/{profile}`. That also has the enormous advantage to make sure that you won't have any conflict with every other endpoints you might add in the future (actuator, users, questions, faq, whatever) – JB Nizet Aug 09 '18 at 13:29
  • It's not a rest service, I should mention that I can't redefine my paths – dzikoysk Aug 09 '18 at 13:30
  • Why can't you? (the fact that it's not a REST service is irrelevant). – JB Nizet Aug 09 '18 at 13:32
  • That is irrelevant. The only relevance it has is that, since it's a monolith, there is a big chance of additional endpoints later, which will also cause conflicts. Which is one more reason to change the mapping to /profiles/{profile}. – JB Nizet Aug 09 '18 at 13:36
  • But the final url would be, for example: `example.com/profile/dzikoysk`. I'm expecting `example.com/dzikoysk` – dzikoysk Aug 09 '18 at 13:38
  • Well, change your expectations. That's the whole point. You also expect such a URL not to conflict with /static, but it does. They won't anymore if you change your mapping. – JB Nizet Aug 09 '18 at 14:16
  • 1
    You are avoiding, not solving the problem. As I said, I'm not able to change mappings, so these considerations are pointless. I'm aware of conflict and I'm looking for the best way, to make it work. It is a github-like service and these paths are really important. – dzikoysk Aug 09 '18 at 14:28

3 Answers3

3

Kinda hacky, but you could use a regex in your profile mapping to exclude static:

For /{profile}:

@RequestMapping("/{profile:^(?:static.+|(?!static).*)$}")

For /{profile}/{project}:

@RequestMapping("/{profile:^(?:static.+|(?!static).*)$}/{project}")

EDIT:

Ah, I just saw that you already found regex as a possible solution and were wondering (among other solutions) if that was the proper way to do it.

Personally, my preferred solution would be to change the URI for the Controller. I find all other solutions to be somewhat similar and hacky: fronting static with a controller, using regex for profile URI...

I think I'd fallback on using the regex above if changing the URI isn't possible. I find it explicit.

alexbt
  • 16,415
  • 6
  • 78
  • 87
  • I've mentioned that I don't want to use regex, but I'll explain why. Firstly, it will be difficult to maintain, I have to copy-paste it in all my wildcard-based controllers and I'll have to remember to add this to all my submappings. Secondly, I'd like to have simple mappings without regex (performance issues). This expression will be matched for every each request and I'm expecting a lot of requests. I think, a better solution is to create dedicated controller mapped to /static, but anyway, thanks for your response, +1 :) – dzikoysk Aug 09 '18 at 14:52
  • 1
    @dzikoysk unfortunately there is no **proper** way to achieve that. It's just bad API design. The usual good practice is when REST-endpoints have some predefined prefix like `/api` that obviously never clashes with `/static` – Nikolai Shevchenko Aug 09 '18 at 15:12
  • Yeah, I know, all controllers has prefix expect this one - path is generally user-friendly url like github.com/dzikoysk/ExampleProject. I think I'll try to define dedicated controller for static content, but let's wait a little, maybe someone has a nice workaround. – dzikoysk Aug 09 '18 at 15:17
3

This issue is caused beause of the way that Spring handles requests from users. There are several HandlerMappings and they are executed in a specified order. Most important for us are these two:

  1. RequestMappingHandlerMapping registered in WebMvcConfigurationSupport with order=0 (we can see this in the source code and the documentation)

    /**
     * Return a {@link RequestMappingHandlerMapping} ordered at 0 for mapping
     * requests to annotated controllers.
     */
    
  2. AbstractHandlerMapping instantiated in ResourceHandleRegistry with default order Integer.MAX_VALUE-1

    private int order = Ordered.LOWEST_PRECEDENCE - 1;
    

When you create a RequestMapping with path /{profile}/{project} and try to reach resource /static/somefile.css, the request you send is grabbed by the RequestMappingHandlerMapping and does not reach the HandlerMapping created by ResourceHandlerRegistry.

A simple solution for this issue is to set order to -1 in addResourceHandlers

@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
    registry.addResourceHandler("/static/**")
        .addResourceLocations("classpath:/resources/static/");

    registry.setOrder(-1);
}

Then the proper HandlerMapping will serve static files, and if there is no such file, it will pass execution to your Controllers.

DmqCsm
  • 101
  • 2
  • 4
northpl93
  • 524
  • 4
  • 8
1

I've found interesting solution based on custom handler mapping and pseudo controller. If we create AbstractHandlerMapping bean, it will be called if Spring does not match any other Controller:

@Bean
public AbstractHandlerMapping profileHandlerMapping(RequestMappingHandlerAdapter handlerAdapter) {
    HandlerMethodArgumentResolverComposite argumentResolvers = new HandlerMethodArgumentResolverComposite();
    argumentResolvers.addResolvers(handlerAdapter.getArgumentResolvers());

    ProfileController profileHandlerMapping = new ProfileController(argumentResolvers);
    profileHandlerMapping.setOrder(2);

    return profileHandlerMapping;
}

Pseudo controller:

public class ProfileController extends AbstractHandlerMapping {

    private final HandlerMethodArgumentResolverComposite argumentResolvers;

    public ProfileController(HandlerMethodArgumentResolverComposite argumentResolvers) {
        this.argumentResolvers = argumentResolvers;
    }

    @Override
    protected Object getHandlerInternal(HttpServletRequest request) throws Exception {
        InvocableHandlerMethod handlerMethod = new InvocableHandlerMethod(this, getClass().getMethod("profile", HttpServletRequest.class));
        handlerMethod.setHandlerMethodArgumentResolvers(argumentResolvers);
        return handlerMethod;
    }

    @ResponseBody
    public String profile(HttpServletRequest request) {
        return "Profile: " + request.getRequestURI();
    }

}

Pros:

  • Conflit with wildcards does not exist
  • Easy to maintain in the future, automatic
  • Dispatched only if there is no matching controller, conflict with 404 also will not occur

Cons:

  • Lack of support for some dynamic annotation-based variables, e.g @PathVariable
dzikoysk
  • 1,560
  • 1
  • 15
  • 27