61

I've mapped the Spring MVC dispatcher as a global front controller servlet on /*.

<servlet>       
  <servlet-name>home</servlet-name>         
  <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>     
</servlet>  
<servlet-mapping>       
  <servlet-name>home</servlet-name>         
  <url-pattern>/*</url-pattern>     
</servlet-mapping>

However, this mapping stops the access to static files like CSS, JS, images etc which are all in the /res/ folder.

How can I access them anyway?

BalusC
  • 1,082,665
  • 372
  • 3,610
  • 3,555
Rahul Garg
  • 8,410
  • 8
  • 33
  • 28

19 Answers19

75

Map the controller servlet on a more specific url-pattern like /pages/*, put the static content in a specific folder like /static and create a Filter listening on /* which transparently continues the chain for any static content and dispatches requests to the controller servlet for other content.

In a nutshell:

<filter>
    <filter-name>filter</filter-name>
    <filter-class>com.example.Filter</filter-class>
</filter>
<filter-mapping>
    <filter-name>filter</filter-name>
    <url-pattern>/*</url-pattern>
</filter-mapping>

<servlet>
    <servlet-name>controller</servlet-name>
    <servlet-class>com.example.Controller</servlet-class>
</servlet>
<servlet-mapping>
    <servlet-name>controller</servlet-name>
    <url-pattern>/pages/*</url-pattern>
</servlet-mapping>

with the following in filter's doFilter():

HttpServletRequest req = (HttpServletRequest) request;
String path = req.getRequestURI().substring(req.getContextPath().length());

if (path.startsWith("/static")) {
    chain.doFilter(request, response); // Goes to default servlet.
} else {
    request.getRequestDispatcher("/pages" + path).forward(request, response);
}

No, this does not end up with /pages in browser address bar. It's fully transparent. You can if necessary make "/static" and/or "/pages" an init-param of the filter.

BalusC
  • 1,082,665
  • 372
  • 3,610
  • 3,555
  • Perhaps I'm missing something, but what do have the doGet in your controller do? In mine, if I have the RequestDispatcher forward to '/pages/default.jsp' after I've updated the request model by setting attributes on it, I get an infinite loop. – MStodd Sep 04 '10 at 17:11
  • 2
    @MStodd: Just don't forward back to the controller, it has already done its job. Hide the views away somewhere in `/WEB-INF`, e.g. `/WEB-INF/pages/default.jsp`. For more hints see [this](http://stackoverflow.com/questions/2523430/hidden-features-of-jsp-servlet/2525995#2525995) and [this](http://stackoverflow.com/questions/3541077/design-patterns-web-based-applications/3542297#3542297) answer. – BalusC Sep 04 '10 at 17:14
  • Awesome! I think I moved my jsps out of there trying to solve this a different way earlier, and didn't move them back in. – MStodd Sep 04 '10 at 17:30
  • 1
    This is a brilliant solution to this problem. – sksamuel Oct 24 '11 at 12:07
  • Good solution, thanks. I had to use request.getServletPath() not request.getRequestURI() as the latter includes the servlet context, therefore did not match /static. – Will Jan 15 '12 at 15:25
  • @Will: If your webapp is not deployed on context root, you'd indeed need to substring the context path off. I have improved the answer. Note that `getServletPath()` may return `null` or something completely different depending on request URI or servlet mapping type or even the forward-dispatched request. This information is only reliable/fixed inside a servlet, not in a filter. I do not recommend using `getServletPath()` in a filter. – BalusC Jan 15 '12 at 15:42
  • @JohnS: That's correct. It can also with [OCPsoft Rewrite](http://ocpsoft.org/rewrite/). All of those URL rewrite filters do under the covers basically the same as demonstrated in my answer, but then just based on some XML configuration file which allows easier external controllable configuration and finetuning. Although, adopting a whole 3rd party library might be "overkill" if you just need this single simple task and further nothing else. – BalusC Aug 17 '12 at 16:10
  • I agree! :) I meant after learning the main concept it's a good idea to consider using an url rewrite in this or similar cases. – Ali Shakiba Aug 18 '12 at 14:10
  • So, to paraphrase: put a filter in front of the default servlet that decides whether a request should be remapped as a controller request. Neat. – David Carboni Oct 26 '12 at 06:30
  • But how can I apply further filters? For instance I want my sitemesh filter to intercept. – Christian Dec 18 '12 at 09:17
  • @Wax: depending on what exactly it does, either map it with `FORWARD`, or just put its filter mapping in front of the controller filter. Filters are invoked in the order their mappings are declared in `web.xml`. – BalusC Dec 18 '12 at 13:58
  • the dispatcher directive seems useful. Because your filter has to be the last in order. – Christian Dec 18 '12 at 14:08
  • Fooling the path with /pages and removing that in the filter is a perfect solution for me, it allows to still have an empty "visible" path in the browser. Thanks a lot for this idea. – BxlSofty Jul 28 '14 at 10:06
  • I did what you said and end up with infinite loop of request dispatcher: `request.getRequestDispatcher("WEB-INF/some.jsp").forward(request, response);` in /pages servlet – ErShakirAnsari Nov 09 '19 at 10:13
43

With Spring 3.0.4.RELEASE and higher you can use

<mvc:resources mapping="/resources/**" location="/public-resources/"/>

As seen in Spring Reference.

Joaquín L. Robles
  • 6,261
  • 10
  • 66
  • 96
21

What you do is add a welcome file in your web.xml

<welcome-file-list>
    <welcome-file>index.html</welcome-file>
</welcome-file-list>

And then add this to your servlet mappings so that when someone goes to the root of your application, they get sent to index.html internally and then the mapping will internally send them to the servlet you map it to

<servlet-mapping>
    <servlet-name>MainActions</servlet-name>
    <url-pattern>/main</url-pattern>
</servlet-mapping>
<servlet-mapping>
    <servlet-name>MainActions</servlet-name>
    <url-pattern>/index.html</url-pattern>
</servlet-mapping>

End result: You visit /Application, but you are presented with /Application/MainActions servlet without disrupting any other root requests.

Get it? So your app still sits at a sub url, but automatically gets presented when the user goes to the root of your site. This allows you to have the /images/bob.img still go to the regular place, but '/' is your app.

casey
  • 1,118
  • 1
  • 13
  • 25
  • I found this solution worked well for me on Tomcat, where I wanted my own index.template files to be handled by my own Servlet. I don't know that it's a required interpretation of the Servlet Specification though - if anything, the section 10.10 on Welcome Files would seem to demand that the container serves up a static resource if it exists, before checking for mappings ... but I'm not sure I've understood it right. – David Bullock Aug 14 '20 at 01:31
  • This is also a relevant place to add, that in the days of yore, the JavaEE specs anticipated a separation of roles between 'deployers' and 'developers' and 'host admins'. These days, Docker has taken over as the 'deployable unit', and as such, the Tomcat instance running an application (or Jetty, or ...) is itself a chosen-at-development-time component. So if it works with *your* servlet container, for *your* app, just be happy. You only need to worry about interoperability if you ship Servlets as re-usable components to be deployed 'anywhere'. And that happens ... almost never. – David Bullock Aug 14 '20 at 01:35
17

Serving static content with appropriate suffix in multiple servlet-mapping definitions solved the security issue which is mentioned in one of the comments in one of the answers posted. Quoted below:

This was a security hole in Tomcat (WEB-INF and META-INF contents are accessible this way) and it has been fixed in 7.0.4 (and will be ported to 5.x and 6.x as well). – BalusC Nov 2 '10 at 22:44

which helped me a lot. And here is how I solved it:

<servlet-mapping>
    <servlet-name>default</servlet-name>
    <url-pattern>*.js</url-pattern>
</servlet-mapping>
<servlet-mapping>
    <servlet-name>default</servlet-name>
    <url-pattern>*.css</url-pattern>
</servlet-mapping>
<servlet-mapping>
    <servlet-name>default</servlet-name>
    <url-pattern>*.jpg</url-pattern>
</servlet-mapping>
<servlet-mapping>
    <servlet-name>default</servlet-name>
    <url-pattern>*.htm</url-pattern>
</servlet-mapping>
<servlet-mapping>
    <servlet-name>default</servlet-name>
    <url-pattern>*.html</url-pattern>
</servlet-mapping>
GO.exe
  • 646
  • 7
  • 13
  • 2
    you can add this to your application's web.xml. you do not have to edit global web.xml which is in ${TOMCAT_HOME}/conf/web.xml. – mert inan Jan 09 '12 at 11:28
  • 3
    yes depends on your needs. I did not mention global or app-wise configuration though. So your comment is irrelevant. – GO.exe Jan 10 '12 at 23:35
17

If you use Tomcat, you can map resources to the default servlet:

<servlet-mapping>
    <servlet-name>default</servlet-name>
    <url-pattern>/static/*</url-pattern>
</servlet-mapping>

and access your resources with url http://{context path}/static/res/...

Also works with Jetty, not sure about other servlet containers.

  • This is on App Engine, as the original asker said, not in Tomcat or Jetty. – Nick Johnson Sep 30 '09 at 13:21
  • 7
    This was a security hole in Tomcat (WEB-INF and META-INF contents are accessible this way) and it has been fixed in 7.0.4 (and will be ported to 5.x and 6.x as well). – BalusC Nov 02 '10 at 22:44
  • @BalusC worth nothing that the Tomcat security fix still allows this technique to work, but the mapping should be /res/* instead of /static/res/* and the resources should be accessed with http://{context path}/res/ (which happily is actually closer to what the asker was looking for, albeit on the wrong servlet container) – CupawnTae Apr 25 '13 at 11:01
12

I've run into this also and never found a great solution. I ended up mapping my servlet one level higher in the URL hierarchy:

<servlet-mapping>       
  <servlet-name>home</servlet-name>             
  <url-pattern>/app/*</url-pattern>     
</servlet-mapping>

And now everything at the base context (and in your /res directory) can be served up by your container.

Dillon Ryan Redding
  • 1,213
  • 12
  • 17
Gandalf
  • 9,648
  • 8
  • 53
  • 88
  • 18
    I would really like to see a better solution to this. It sucks to be stuck with a /app/ prefix everywhere. – pjesi Dec 07 '09 at 13:48
  • 1
    @pjesi You can use Tuckey URL Rewrite to fix that problem. – Adam Gent Sep 11 '11 at 13:35
  • you probably mean http://tuckey.org/urlrewrite/ - it's ok but on appengine it serves your static content, and therefore consumes your instance time; I guess that @Rahul wants to have a servlet to serve "everything except static files" so that instance time is consumed only when needed – Petr Kozelka Jan 15 '15 at 17:12
9

As of 3.0.4 you should be able to use mvc:resources in combination with mvc:default-servlet-handler as described in the spring documentation to achieve this.

http://static.springsource.org/spring/docs/3.0.x/spring-framework-reference/html/mvc.html#mvc-static-resources

digitaljoel
  • 26,265
  • 15
  • 89
  • 115
6

The reason for the collision seems to be because, by default, the context root, "/", is to be handled by org.apache.catalina.servlets.DefaultServlet. This servlet is intended to handle requests for static resources.

If you decide to bump it out of the way with your own servlet, with the intent of handling dynamic requests, that top-level servlet must also carry out any tasks accomplished by catalina's original "DefaultServlet" handler.

If you read through the tomcat docs, they make mention that True Apache (httpd) is better than Apache Tomcat for handling static content, since it is purpose built to do just that. My guess is because Tomcat by default uses org.apache.catalina.servlets.DefaultServlet to handle static requests. Since it's all wrapped up in a JVM, and Tomcat is intended to as a Servlet/JSP container, they probably didn't write that class as a super-optimized static content handler. It's there. It gets the job done. Good enough.

But that's the thing that handles static content and it lives at "/". So if you put anything else there, and that thing doesn't handle static requests, WHOOPS, there goes your static resources.

I've been searching high and low for the same answer and the answer I'm getting everywhere is "if you don't want it to do that, don't do that".

So long story short, your configuration is displacing the default static resource handler with something that isn't a static resource handler at all. You'll need to try a different configuration to get the results you're looking for (as will I).

Bill
  • 61
  • 1
  • 1
3

'Static' files in App Engine aren't directly accessible by your app. You either need to upload them twice, or serve the static files yourself, rather than using a static handler.

Nick Johnson
  • 100,655
  • 16
  • 128
  • 198
2

The best way to handle this is using some kind of URL re-writing. In this way, you can have clean restful URLs, and NOT with any extensions i.e abc.com/welcom/register as opposed to abc.com/welcome/resister.html

I use Tuckey URL which is pretty cool.

It's got instructions on how to set up your web app.I have set it up with my Spring MVC web app. Of course, everything was fine until I wanted to use annotations for Spring 3 validations like @Email or @Null for domain objects.

When I add the Spring mvc directives:

< mvc:annotation-driven  /> 
< mvc:default-servlet-handler />

.. it breaks the good ol Tuckey code. Apparently, < mvc:default-servlet-handler /> replaces Tuckey, which I'm still trying to solve.

Tim Post
  • 33,371
  • 15
  • 110
  • 174
logixplayer
  • 939
  • 2
  • 13
  • 23
  • 2
    I edited this to be more of an answer, which it is. If you want to ask a new question, feel free - but an answer is not the place to do that. Please come back and update this if you get the issue resolved. – Tim Post Aug 18 '12 at 15:40
1

I found that using

<mvc:default-servlet-handler />

in the spring MVC servlet bean definition file works for me. It passes any request that isn't handled by a registered MVC controller on to the container's original default handler, which should serve it as static content. Just make sure you have no controller registered that handles everything, and it should work just fine. Not sure why @logixplayer suggests URL rewriting; you can achieve the effect he's looking for just adequately using Spring MVC alone.

Jules
  • 14,841
  • 9
  • 83
  • 130
  • 1
    I agree, if you're using Spring then adding additional URL re-writing (via Tuckey URLRewriteFilter for example) is a waste of time and adding needless configuration to solve the problem. – Joshua Hutchison May 09 '14 at 06:31
1

I found a simpler solution with a dummy index file.

Create a Servlet (or use the one you wanted to respond to "/") which maps to "/index.html" (Solutions mentioned here use the mapping via XML, I used the 3.0 version with annotation @WebServlet) Then create a static (empty) file at the root of the static content named "index.html"

I was using Jetty, and what happened was that the server recognized the file instead of listing the directory but when asked for the resource, my Servlet took control instead. All other static content remained unaffected.

1

Add the folders which you don't want to trigger servlet processing to the <static-files> section of your appengine-web.xml file.

I just did this and looks like things are starting to work ok. Here's my structure:

/

/pages/<.jsp files>

/css

I added "/pages/**" and "/css/**" to the <static-files> section and I can now forward to a .jsp file from inside a servlet doGet without causing an infinite loop.

MStodd
  • 4,716
  • 3
  • 30
  • 50
  • i dont quite understand what you mean here . For my issue its a single page application and i have my compiled js files in a folder inside my webapp but when i deploy to google cloud i cant get my index.html doesn't load my js and css files . i just dont know what is wrong . i get a 404 for those resource files. – I.Tyger Sep 22 '16 at 12:25
1

After trying the filter approach without success (it did for some reason not enter the doFilter() function) I changed my setup a bit and found a very simple solution for the root serving problem:

Instead of serving " / * " in my main Servlet, I now only listen to dedicated language prefixes "EN", "EN/ *", "DE", "DE/ *"

Static content gets served by the default Servlet and the empty root requests go to the index.jsp which calls up my main Servlet with the default language:

< jsp:include page="/EN/" /> (no other content on the index page.)

sebut
  • 53
  • 1
  • 7
1

I'd recommend trying to use a Filter instead of a default servlet whenever possible.

Other two possibilities:

Write a FileServlet yourself. You'll find plenty examples, it should just open the file by URL and write its contents into output stream. Then, use it to serve static file request.

Instantiate a FileServlet class used by Google App Engine and call service(request, response) on that FileServlet when you need to serve the static file at a given URL.

You can map /res/* to YourFileServlet or whatever to exclude it from DispatcherServlets' handling, or call it directly from DispatcherServlet.

And, I have to ask, what does Spring documentation say about this collision? I've never used it.

alamar
  • 18,729
  • 4
  • 64
  • 97
0

In Embedded Jetty I managed to achieve something similar by adding a mapping for the "css" directory in web.xml. Explicitly telling it to use DefaultServlet:

<servlet>
  <servlet-name>DefaultServlet</servlet-name>
  <servlet-class>org.eclipse.jetty.servlet.DefaultServlet</servlet-class>
</servlet>

<servlet-mapping>
  <servlet-name>DefaultServlet</servlet-name>
  <url-pattern>/css/*</url-pattern>
</servlet-mapping>
QaZ
  • 31
  • 2
0
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:mvc="http://www.springframework.org/schema/mvc"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="
        http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
        http://www.springframework.org/schema/mvc
        http://www.springframework.org/schema/mvc/spring-mvc-3.0.xsd">
<mvc:default-servlet-handler/>
</beans>

and if you want to use annotation based configuration use below code

@Override
    public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) {
        configurer.enable();
    }
Jekin Kalariya
  • 3,475
  • 2
  • 20
  • 32
0

With regard to Tomcat, a lot depends on the particular version. There was a bug fix https://bz.apache.org/bugzilla/show_bug.cgi?id=50026 which means the servlet-mapping (other than for '/') for the default servlet behaves differently in Tomcat 6.0.29 (and earlier) compared with later versions.

-1

In section "12.2 Specification of Mappings" of the Servlet Specification, it says:

A string containing only the ’/’ character indicates the "default" servlet of the application.

So in theory, you could make your Servlet mapped to /* do:

getServletContext().getNamedDispatcher("/").forward(req,res);

... if you didn't want to handle it yourself.

However, in practice, it doesn't work.

In both Tomcat and Jetty, the call to getServletContext().getNamedDispatcher('/') returns null if there is a servlet mapped to '/*'

David Bullock
  • 6,112
  • 3
  • 33
  • 43
  • A servlet on `/*` overrides a servlet on `/`. The servlet on `/` is only invoked if there is no matching servlet found. This is not the case in this specific question. See also a.o. https://stackoverflow.com/q/4140448 – BalusC Aug 13 '20 at 21:45
  • @BalusC that does seem to be the case in actual implementations, but one would not have predicted that behaviour from the spec. If `/' is a special name, it's a special name. – David Bullock Aug 15 '20 at 07:32