2

I am scaling back a large web application that included a web service to become only a Jersey web service, on Spring Boot 1.5.2. Because the web service already had a complete set of JAX-RS annotations implemented by Apache Wink, I decided to go with Spring + Jersey instead of Spring Rest. I found this spring-boot-jersey-sample application to use as a reference. The biggest difference between the application I'm working on and the sample is that my endpoint definitions are divided between interfaces and implementations.

I added the following to my pom.xml:

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-jersey</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-tomcat</artifactId>
        <scope>provided</scope>
    </dependency>

My new Jersey Config looks like this:

package com.example.configuration;

import org.glassfish.jersey.server.ResourceConfig;
import com.example.EndpointImpl;
import org.springframework.stereotype.Component;

@Component
public class JerseyConfiguration extends ResourceConfig {
  public JerseyConfiguration() {
    registerEndpoints();
  }

  private void registerEndpoints() {
    register(EndpointImpl.class);     
  }
}

Then I have the following Application.java:

package com.example;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.boot.web.support.SpringBootServletInitializer;

@SpringBootApplication
public class Application extends SpringBootServletInitializer{
  public static void main(String[] args) {
    new Application().configure(new SpringApplicationBuilder(Application.class)).run(args);
  }
}

The endpoints are defined as an interface and an implementation, like this (minus imports):

public interface Endpoint {
  @GET
  @Produces({MediaType.APPLICATION_JSON})
  public Response getHello(@Context ServletContext sc, @Context HttpServletRequest req, @Context HttpHeaders httpHeaders) ;
}
@Path("")
@Component
public class EndpointImpl implements Endpoint {
  @Override
  public Response getHello(@Context ServletContext sc, @Context HttpServletRequest req,
      @Context HttpHeaders httpHeaders)  {
      return Response.ok("hello").build();
  }
}

When I start up my application, I see messages saying Tomcat has started up, including a messges saying Mapping servlet: 'com.example.configuration.JerseyConfiguration' to [/*]. However, when I go to / with a web browser, I get a 404 Not Found error. It doesn't look like the GET definition is getting picked up.

k-den
  • 853
  • 13
  • 28

3 Answers3

2

This problem is explained in the JAX-RS spec in § 3.6 Annotation Inheritance.

JAX-RS annotations may be used on the methods and method parameters of a super-class or an implemented interface. Such annotations are inherited by a corresponding sub-class or implementation class method provided that the method and its parameters do not have any JAX-RS annotations of their own.

If a subclass or implementation method has any JAX-RS annotations then all of the annotations on the superclass or interface method are ignored. E.g.:

public interface ReadOnlyAtomFeed {
  @GET @Produces("application/atom+xml")
  Feed getFeed();
}

@Path("feed")
public class ActivityLog implements ReadOnlyAtomFeed {
  public Feed getFeed() {...}
}

In the above, ActivityLog.getFeed inherits the @GET and @Produces annotations from the interface.

Conversely:

@Path("feed")
public class ActivityLog implements ReadOnlyAtomFeed {
  @Produces("application/atom+xml")
  public Feed getFeed() {...}
}

In the above, the @GET annotation on ReadOnlyAtomFeed.getFeed is not inherited by ActivityLog.getFeed and it would require its own request method designator (@GET) since it redefines the @Produces annotation.

For consistency with other Java EE specifications, it is recommended to always repeat annotations instead of relying on annotation inheritance.

I've highlighted the important ports. It should be pretty clear why it isn't working for you. In your EndpointImpl, you have repeated the @Context annotations, therefore causing "all of the annotations on the superclass or interface method are ignored". This include the @GET. So ultimately, this causes that endpoint not to be registered, as endpoints require a @METHOD.

As far as the last paragraph in the blockquote, you can choose to follow it or not. I just threw it in there just for completeness.

Paul Samsotha
  • 205,037
  • 37
  • 486
  • 720
  • This appears to be the major offender. Now the question is, should I put all of the annotations on the interface, and set up future programmers to fall into the trap of adding annotations to the implementation and breaking the service? Or should I put the same annotations on the interface and implementation and cause them the pain of maintaining them in both places. – k-den Apr 07 '17 at 19:03
  • 1
    Personally, I've never understood why people would even choose to use interfaces. I really don't see much benefit. If you've ever worked with Spring, you never see people using interfaces for controllers. I can understand using abstract classes with base implementations for common endpoints (no annotation inheritence). But interfaces, I really don't get it. So I have no answer for you :-( – Paul Samsotha Apr 07 '17 at 19:09
  • Yeah, I have stripped out the interfaces. I think future developers on this project will thank me. – k-den Apr 18 '17 at 20:11
  • It doesn't work for me even when no single jax-rs annotation in class implementing annotated interface... – mirec Jul 21 '17 at 09:02
  • @mirec You still need the `@Path` on the implementation class. Other than that, can't really help without seeing some code. If you want to post a new question, I'd be glad to look at it. – Paul Samsotha Jul 21 '17 at 09:04
  • @peeskillet you're right when I moved \@Path from interface to class it is working... Isn't it weird? I checked quickly: this is only jersey issue, using RestEasy it works as expected, no annotation needed on class... – mirec Jul 21 '17 at 09:11
1

What does your application.yml (.properties) look like:

You might need to declare two path mappings, one for Spring MVC dispatcher servlet and one for Jersey dispatcher servlet. Something like:

application.yml

...
# Spring MVC dispatcher servlet path. Needs to be different than Jersey's to enable/disable Actuator endpoints access (/info, /health, ...)
server.servlet-path: /
# Jersey dispatcher servlet
spring.jersey.application-path: /api
...

You should now be able to access Jersey endpoints at /api/.....

I covered this in a blog post I published back in Apr 2016: Microservices using Spring Boot, Jersey, Swagger and Docker

k-den
  • 853
  • 13
  • 28
ootero
  • 3,235
  • 2
  • 16
  • 22
  • I think this may be a secondary issue I am also experiencing! I do not have the conflicting Spring dispatcher-servlet because I removed the Spring Boot webapp starter, and it appears that it the Spring dispatcher-servlet is no longer inserted. However, I think some of the code in this app I am scaling down expects the application path not to be at the root. I will investigate further. – k-den Apr 07 '17 at 19:06
0

Unless something has changed in more recent Spring Boot/Spring MVC + Jersey versions, to get Spring Boot to recognize a Jersey endpoint you need to register it like this with an additional @Component that extends Jersey's ResourceConfig:

@Component
public class JerseyExampleConfig extends ResourceConfig {

    public JerseyExampleConfig() {
        register(EndpointImpl.class);
    } 
}
Kevin Hooke
  • 2,583
  • 2
  • 19
  • 33
  • Strangely, this looks virtually identical to the Jersey Config in my question (second code block) except that I call a method `registerEndpoints()` that calls the `register` method. – k-den Apr 07 '17 at 05:15
  • you're right, don't know why I didn't spot that when I answered, but then I guess you realize already this is required :-) – Kevin Hooke Apr 07 '17 at 23:18