3

Is there any way of making Spring Boot with Jersey serve static content? I've been through a series of tutorials and code samples on integrating Swagger into a Spring Boot's application. I can make it deliver the basic swagger.json, but I can't make Swagger UI work.

I can't even make it deliver a simple hello.txt static file.

The relevant parts of my pom.xml are:

<!--Spring Boot-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>

<!-- Jersey -->
<dependency>
    <groupId>org.glassfish.jersey.core</groupId>
    <artifactId>jersey-client</artifactId>
    <version>${jersey.version}</version>
</dependency>
<dependency>
    <groupId>org.glassfish.jersey.ext</groupId>
    <artifactId>jersey-spring3</artifactId>
    <version>${jersey.version}</version>
</dependency>
<dependency>
    <groupId>org.glassfish.jersey.ext</groupId>
    <artifactId>jersey-bean-validation</artifactId>
    <version>${jersey.version}</version>
</dependency>
<dependency>
    <groupId>org.glassfish.jersey.containers</groupId>
    <artifactId>jersey-container-servlet</artifactId>
    <version>${jersey.version}</version>
</dependency>

<!-- Swagger -->
<dependency>
    <groupId>io.swagger</groupId>
    <artifactId>swagger-jersey2-jaxrs</artifactId>
    <version>1.5.7</version>
</dependency>

And my code:

@Configuration
@EnableAutoConfiguration
@ComponentScan({"com.xxxx"})
public class AdminApplication {

    public static void main(String[] args) {
        ConfigurableApplicationContext applicationContext = new SpringApplicationBuilder(AdminApplication.class)
                .run(args);
    }

    @Bean
    public ServletRegistrationBean jerseyServlet() {
        ServletRegistrationBean registration = new ServletRegistrationBean(new ServletContainer(), "/*");
        registration.addInitParameter(ServletProperties.JAXRS_APPLICATION_CLASS, JerseyConfig.class.getName());
        return registration;
    }
}




package com.xxxxxx.admin.config;
import com.xxxxxx.admin.resource.Status;
import org.glassfish.jersey.filter.LoggingFilter;
import org.glassfish.jersey.server.ResourceConfig;
import org.glassfish.jersey.server.ServerProperties;
import org.glassfish.jersey.server.spring.scope.RequestContextFilter;    
import io.swagger.jaxrs.config.BeanConfig;

public class JerseyConfig extends ResourceConfig {

    public JerseyConfig() {
        register(RequestContextFilter.class);
        packages("com"); // TODO needs more detailed level
        register(LoggingFilter.class);
        // Validation
        this.property(ServerProperties.BV_SEND_ERROR_IN_RESPONSE, true);
        this.property(ServerProperties.RESPONSE_SET_STATUS_OVER_SEND_ERROR, true);
        configureSwagger();
    }

    private void configureSwagger() {
        register(io.swagger.jaxrs.listing.ApiListingResource.class);
        register(io.swagger.jaxrs.listing.SwaggerSerializers.class);
        BeanConfig beanConfig = new BeanConfig();
        beanConfig.setVersion("1.0.0");
        beanConfig.setSchemes(new String[]{"http"});
        beanConfig.setHost("localhost:8080");
        beanConfig.setBasePath("/"); // tried other things like "/api", but doesn't change anything
        beanConfig.setResourcePackage("com.xxxxxx.admin");
        beanConfig.setPrettyPrint(true);
        beanConfig.setScan(true);
    }

}



//other imports
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;

@Service
@Path("/status")
@Api(value = "status", description = "Check status")
public class Status {

    @GET
    @Produces(MediaType.APPLICATION_JSON)
    @ApiOperation("Return status")
    public Response status() {
        return Response.ok("Up").build();
    }
}

I also tried making Jersey run as a filter (with spring.jersey.type=filter) and changing Jersey's servlet pattern as stated in this answer, but that doesn't seem to affect anything.

@ApplicationPath("/rootPath")
public class JerseyConfig extends ResourceConfig {

I have a hello.txt file under /src/main/resources/public and Swagger UI's static files under /src/main/resources/public/swagger.

As I said, my application works fine, and GET http://localhost:8080/swagger.json shows me the plain json documentation, but both http://localhost:8080/hello.txt and http://localhost:8080/swagger/index.html return 404.

I'm using Jersey 2.8 and Spring Boot 1.3.0

Community
  • 1
  • 1
user384729
  • 403
  • 1
  • 7
  • 16

2 Answers2

2

I also tried changing Jersey's servlet pattern

@ApplicationPath("/rootPath")
public class JerseyConfig extends ResourceConfig {

The way you are configuring your app, the @ApplicationPath doesn't matter. The reason it worked for this answer you linked to, is because the Spring Boot auto configuration sets the servlet mapping when it extracts the @ApplicationPath value from your resource config.

You are currently not using the ServletRegistrationBean provided by Spring Boot, that accomplishes this. If you goal, by using your own ServletRegistrationBean, was so that you can register your ResourceConfig, you could've have done the same simply by either

  1. Annotating your ResourceConfig with @Component to make it a Spring bean, or
  2. Make it a Spring bean in your configuration class

    @Bean
    public ResourceConfig config() {
        return new JerseyConfig();
    }
    

Spring Boot will then inject your ResourceConfig into the JerseyAutoConfiguration, where it will get the @ApplicationPath value (if present) on the ResourceConfig, and use it to register its own ServletRegistrationBean.

You can see the JerseyAutoConfiguration to get an idea of everything you get for free, when you let Spring Boot handle the configuration.

Of if you want to keep your current SpringRegistrationBean, just change the path you are using. You are using /*, which is mentioned to be the problem in the linked answer. So just change to /rooPath/* if that's what you want.

Jacob van Lingen
  • 8,989
  • 7
  • 48
  • 78
Paul Samsotha
  • 205,037
  • 37
  • 486
  • 720
  • 1
    That's a great answer, thanks! But there is something weird. I set `spring.jersey.type=filter` and `spring.jersey.application-path=rootPath`, and everything works nicely: static content is served and the app endpoints are moved to `rootPath/` and reply correctly. But If I only set `spring.jersey.type=filter` and not `spring.jersey.application-path`, the static content is no longer reachable. According to option 1 in your linked answer, setting jersey to run as a filter should be enough. What am I missing? – user384729 Feb 11 '16 at 15:47
  • Because you still need to set the property `ServletProperties.FILTER_FORWARD_ON_404` to `true`. See [this answer](http://stackoverflow.com/a/29670751/2587435) – Paul Samsotha Feb 11 '16 at 15:47
  • I forgot to mention that you were right in that I added my own `ServletRegistrationBean` to registe `ResourceConfi`g. I followed your advice and annotated `ResourceConfig` with `@Component` and no longer use my own `ServletRegistrationBean`. – user384729 Feb 11 '16 at 15:49
  • Ok, so that means that I can't possibly have the static content and the jersey resources under the same path, right? – user384729 Feb 11 '16 at 15:50
  • No you can, but you need to tell Jersey (filter) to forward the request when it can't find your static content. That's what the above property does. But with a servlet, no you can't. It needs to be a filter – Paul Samsotha Feb 11 '16 at 15:51
  • Added that property, but still doesn't work. Could it be caused by some other filter (I'm thinking of `LoggingFilter`) adding an entity to the response? `ServletProperties.FILTER_FORWARD_ON_404`'s documentation says _...and a 404 response with no entity body is returned from either the runtime or the application..._ And the responses that I get have both a 404 header and a "HTTP 404 Not Found" body. – user384729 Feb 11 '16 at 16:11
  • No the logging filter is not a problem. AFAIK it doesn't add an entity to the response. I use it all the time for development. Are you sure you set the Spring Boot property `spring.jersey.type=filter`? This works for me all the time – Paul Samsotha Feb 11 '16 at 16:14
  • If you want to put together a minimal (github) project that I can test out, I'll check it out, but I can't reproduce the problem on my end. – Paul Samsotha Feb 11 '16 at 16:49
  • Found it: I needed to remove the `entity()` part of `return Response.status(Response.Status.NOT_FOUND).entity(exception.getMessage()).build()` in my `ExceptionMapper.toResponse`. Again, thanks a lot. – user384729 Feb 12 '16 at 09:43
2

It looks the same as a common issue when using Spring MVC. A servlet container is required per servlet specification to implement a default server with lowest priority that is able to serve static content located outside WEB-INF folder. Unfortunately, you are mapping Jersey servlet to "/*", meaning that every URL will be submitted to Jersey, and Jersey does not know what to do with static URLs.

So what can be (easily) done?

  • map the Jersey servlet to a subpath (say /api) and move all you controllers there:

    ServletRegistrationBean registration =
        new ServletRegistrationBean(new ServletContainer(), "/api/*");
    ...
    beanConfig.setBasePath("/api/");
    

    and ask GET http://localhost:8080/api/swagger.json

  • only map the servlet to *.json URLs:

    ServletRegistrationBean registration =
        new ServletRegistrationBean(new ServletContainer(), "*.json");
    
Serge Ballesta
  • 143,923
  • 11
  • 122
  • 252