问题
I am currently trying to create an InjectableProvider
with Jersey, but I cannot get Jersey to pick it up.
I cannot find any real examples of its usage, or even how to get it picked up besides using the @Provider
annotation on the implementation. The person that seemingly wrote it within Jersey implied in some posts that this is enough to have it picked up.
Do I need to specify some SPI service file, or add it to some factory somewhere?
Note: I am running within Glassfish 3.1, and using Spring 3.1. It seems reasonable that Spring may be somehow taking over for the automatic loading of the Provider
s. However, I just don't know. I am not using Spring in anyway to manage the suggested InjectableProvider below, nor am I trying to add it in some other way, which may-well be my problem.
import com.sun.jersey.core.spi.component.ComponentContext;
import com.sun.jersey.spi.inject.Injectable;
import com.sun.jersey.spi.inject.PerRequestTypeInjectableProvider;
public abstract class AbstractAttributeInjectableProvider<T>
extends PerRequestTypeInjectableProvider<AttributeParam, T>
{
protected final Class<T> type;
public AbstractAttributeInjectableProvider(Class<T> type)
{
super(type);
this.type = type;
}
@Override
public Injectable<T> getInjectable(ComponentContext componentContext,
AttributeParam attributeParam)
{
return new AttributeInjectable<T>(type, attributeParam.value());
}
}
Basic Implementation:
import javax.ws.rs.ext.Provider;
@Component // <- Spring Annotation
@Provider // <- Jersey Annotation
public class MyTypeAttributeInjectableProvider
extends AbstractAttributeInjectableProvider<MyType>
{
public MyTypeAttributeInjectableProvider()
{
super(MyType.class);
}
}
Reference Annotation
:
@Target({ElementType.FIELD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface AttributeParam
{
/**
* The value is the name to request as an attribute from an {@link
* HttpContext}'s {@link HttpServletRequest}.
* @return Never {@code null}. Should never be blank.
*/
String value();
}
Reference link from Jersey developer.
UPDATE: calvinkrishy pointed out two flaws to my thinking.
First, I assumed that Jersey was going to kick off scanning for @Provider
s after being kicked off by the traditional Jersey-Spring servlet: com.sun.jersey.spi.spring.container.servlet.SpringServlet
. This was mostly incorrect; it does start scanning, but it looks for Spring beans that have the annotation.
Second, I assumed that the PerRequestTypeInjectableProvider
would be asked upon each incoming request for an Injectable
to handle the annotation that it controls. This too was wrong. The PerRequestTypeInjectableProvider
is instantiated upon startup, as expected, but Jersey then immediately asks for Injectable
's to handle the given annotation with the given type
, which it determines by scanning the Restful Services that it has--at this point--decided that it manages (which is to say, all of them).
The difference between the PerRequestTypeInjectableProvider
and SingletonTypeInjectableProvider
seems to be that the resulting Injectable
either contains the value without working for it (singleton), or it looks it up each time for the value (per request), thus enabling the value to change per request.
This threw a smaller wrench into my plans by forcing me to do some extra work in my AttributeInjectable
(code below) rather than passing in some objects, as I had planned, to avoid giving the AttributeInjectable
extra knowledge.
public class AttributeInjectable<T> implements Injectable<T>
{
/**
* The type of data that is being requested.
*/
private final Class<T> type;
/**
* The name to extract from the {@link HttpServletRequest} attributes.
*/
private final String name;
/**
* Converts the attribute with the given {@code name} into the {@code type}.
* @param type The type of data being retrieved
* @param name The name being retrieved.
* @throws IllegalArgumentException if any parameter is {@code null}.
*/
public AttributeInjectable(Class<T> type, String name)
{
// check for null
// required
this.type = type;
this.name = name;
}
/**
* Look up the requested value.
* @return {@code null} if the attribute does not exist or if it is not the
* appropriate {@link Class type}.
* <p />
* Note: Jersey most likely will fail if the value is {@code null}.
* @throws NullPointerException if {@link HttpServletRequest} is unset.
* @see #getRequest()
*/
@Override
public T getValue()
{
T value = null;
Object object = getRequest().getAttribute(name);
if (type.isInstance(object))
{
value = type.cast(object);
}
return value;
}
/**
* Get the current {@link HttpServletRequest} [hopefully] being made
* containing the {@link HttpServletRequest#getAttribute(String) attribute}.
* @throws NullPointerException if the Servlet Filter for the {@link
* RequestContextHolder} is not setup
* appropriately.
* @see org.springframework.web.filter.RequestContextFilter
*/
protected HttpServletRequest getRequest()
{
// get the request from the Spring Context Holder (this is done for
// every request by a filter)
ServletRequestAttributes attributes =
(ServletRequestAttributes)RequestContextHolder.getRequestAttributes();
return attributes.getRequest();
}
}
I was hoping to be able to pass in the HttpServletRequest
from the Provider
, but the AttributeInjectable
is only instantiated per unique annotation/type. As I cannot do that, I do that per value lookup, which uses Spring's RequestContextFilter
singleton, which provides a ThreadLocal
mechanism for safely retrieving the HttpServletRequest
(among other things related to the current request).
<filter>
<filter-name>requestContextFilter</filter-name>
<filter-class>
org.springframework.web.filter.RequestContextFilter
</filter-class>
</filter>
<filter-mapping>
<filter-name>requestContextFilter</filter-name>
<url-pattern>/path/that/i/wanted/*</url-pattern>
</filter-mapping>
The result does work, and it makes the code much more readable without forcing various services to extend a base class just to hide the usage of @Context HttpServletRequest request
, which is then used to access the attributes as done above through some helper method.
Then you can do something along the lines of this:
@Path("my/path/to")
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.TEXT_PLAIN)
public interface MyService
{
@Path("service1")
@POST
Response postData(@AttributeParam("some.name") MyType data);
@Path("service2")
@POST
Response postOtherData(@AttributeParam("other.name") MyOtherType data);
}
@Component // Spring
public class MyServiceBean implements MyService
{
@Override
public Response postData(MyType data)
{
// interact with data
}
@Override
public Response postOtherData(MyOtherType data)
{
// interact with data
}
}
This becomes very convenient as I use a Servlet Filter to ensure that the user has the appropriate privileges to access the service before passing the data, and then I can parse the incoming data (or load it, or whatever) and dump it into the attribute to be loaded.
If you don't want the above Provider
approach, and you want the base class for accessing the attributes, then here you go:
public class RequestContextBean
{
/**
* The current request from the user.
*/
@Context
protected HttpServletRequest request;
/**
* Get the attribute associated with the current {@link HttpServletRequest}.
* @param name The attribute name.
* @param type The expected type of the attribute.
* @return {@code null} if the attribute does not exist, or if it does not
* match the {@code type}. Otherwise the appropriately casted
* attribute.
* @throws NullPointerException if {@code type} is {@code null}.
*/
public <T> T getAttribute(String name, Class<T> type)
{
T value = null;
Object attribute = request.getAttribute(name);
if (type.isInstance(attribute))
{
value = type.cast(attribute);
}
return value;
}
}
@Path("my/path/to")
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.TEXT_PLAIN)
public interface MyService
{
@Path("service1")
@POST
Response postData();
@Path("service2")
@POST
Response postOtherData();
}
@Component
public class MyServiceBean extends RequestContextBean implements MyService
{
@Override
public Response postData()
{
MyType data = getAttribute("some.name", MyType.class);
// interact with data
}
@Override
Response postOtherData()
{
MyOtherType data = getAttribute("other.name", MyOtherType.class);
// interact with data
}
}
UPDATE2: I thought about my implementation of AbstractAttributeInjectableProvider
, which is itself a generic class that only exists to provide AttributeInjectable
's for a given type, Class<T>
and the supplied AttributeParam
. It's far easier to provide a non-abstract
implementation that is told its type (Class<T>
) with each requested AttributeParam
, thus avoiding a bunch of constructor-only implementations providing the type for you. This also avoids having to write code for every single type that you want to use with the AttributeParam
annotation.
@Component
@Provider
public class AttributeParamInjectableProvider
implements InjectableProvider<AttributeParam, Type>
{
/**
* {@inheritDoc}
* @return Always {@link ComponentScope#PerRequest}.
*/
@Override
public ComponentScope getScope()
{
return ComponentScope.PerRequest;
}
/**
* Get an {@link AttributeInjectable} to inject the {@code parameter} for
* the given {@code type}.
* @param context Unused.
* @param parameter The requested parameter
* @param type The type of data to be returned.
* @return {@code null} if {@code type} is not a {@link Class}. Otherwise
* an {@link AttributeInjectable}.
*/
@Override
public AttributeInjectable<?> getInjectable(ComponentContext context,
AttributeParam parameter,
Type type)
{
AttributeInjectable<?> injectable = null;
// as long as it's something that we can work with...
if (type instanceof Class)
{
injectable = getInjectable((Class<?>)type, parameter);
}
return injectable;
}
/**
* Create a new {@link AttributeInjectable} for the given {@code type} and
* {@code parameter}.
* <p />
* This is provided to avoid the support for generics without the need for
* {@code SuppressWarnings} (avoided via indirection).
* @param type The type of data to be returned.
* @param parameter The requested parameter
* @param <T> The type of data being accessed by the {@code param}.
* @return Never {@code null}.
*/
protected <T> AttributeInjectable<T> getInjectable(Class<T> type,
AttributeParam parameter)
{
return new AttributeInjectable<T>(type, parameter.value());
}
}
Note: each Injectable
is instantiated once at startup rather than per request, but they are invoked upon each incoming request.
回答1:
How are you initializing Jersey?
I will assume you are using Jersey using the jersey-spring servlet. In which case Jersey would by default initialize using Spring beans and hence your Provider
has to be a Spring bean. Try adding a @Named
(or if you do not use atinject @Component
or one of the Spring annotaions) to your Provider
.
An example of using Injectable Providers.
Updated: More clarity on the scope of injection:
The Provider
has to be a Singleton, as for all practical purposes its a factory with scope tied to it and there is no need to construct a factory for every request. The injection itself would happen per request. In other words the getInjectable
method would be called for every request. Did you get a chance to try that?
OTOH, if you extend the SingletonTypeInjectableProvider
the same object would be injected into your resource every time.
I am not sure I completely understand your Provider
implementation. I believe something like the following should work.
public class UserProvider extends PerRequestTypeInjectableProvider<AttributeParam, Users>{
public UserProvider(){
super(Users.class);
}
@Context
HttpServletRequest request;
@Override
public Injectable<Users> getInjectable(ComponentContext cc, AttributeParam a) {
String attributeValue = AnnotationUtils.getValue(a);
return new Injectable<Users>(){
public Users getValue() {
System.out.println("Called"); //This should be called for each request
return request.getAttribute(attributeValue);
}
};
}
}
Updated: To provide more information on the injection types and contexts available in Jersey.
As you probably figured by now, if all you need is access to the HttpServletRequest
then just directly injecting it into your Resource
or Provider
using the @Context
annotation will get you that.
However, to pass those values to the Injectable one has to use a AssistedProvider
or use an approach similar to yours. But again you can mitigate that if you inline your Injectable
definition in the Provider and inject the HttpServletRequest
into the Provider
class. In that case the Injectable
would be able to access the HttpServletRequest
instance (since its there in scope). I just updated my example to show that approach.
Injection using PerRequestTypeInjectableProvider
and SingletonTypeInjectableProvider
are not the only two options you have to inject values into your resources. You could also inject using *Param
values using a StringReaderProvider
. Obviously such an injection is request scoped.
@Provider
@Named("userProviderParamInjector")
public class UserProviderParam implements StringReaderProvider<Users> {
@Context
HttpServletRequest request;
public StringReader<Users> getStringReader(Class<?> type, Type type1, Annotation[] antns) {
if(type.equals(Users.class) {
return null;
}
String attributeValue = null;
for(Annotation a : antns) {
if((a.getClass().getSimpleName()).equals("AttributeParam")){
attributeValue = (String)AnnotationUtils.getValue(a);
}
}
return new StringReader<Users>(){
public Users fromString(String string) {
// Use the value of the *Param or ignore it and use the attributeValue of our custom annotation.
return request.getAttribute(attributeValue);
}
};
}
}
This Provider
would be invoked for any *Param
that you have in your resource. So with a Provider
like the one above registered and a resource like the one below, the Users
value would be injected into your resource method.
@Path("/user/")
@Named
public class UserResource {
@Path("{id}")
@GET
@Produces(MediaType.APPLICATION_JSON)
public Result<Users> get(@AttributeParam("foo") @PathParam("id") Users user) {
...
}
}
But to be honest with you I consider this an abuse of the StringReaderProvider contract whereas the former technique of using Injectable
feels cleaner.
来源:https://stackoverflow.com/questions/11944434/jersey-injectableprovider-not-picked-up-spring