Search code examples
jsftomcatannotationstomcat6tomcat7

Tomcat @Resource annotations API annotation stops working in Tomcat 7


I have been using Tomcat 6.0.26-6.0.35 for several years with JSF 2 Mojarra, various versions up to 2.1.2 which I have been using for some months. I have several request-scoped and session-scoped beans with code like this:

private @Resource(name="jdbc/cLabs", mappedName="jdbc/cLabs") DataSource cLabs;

which has been correctly injected in every version of Tomcat 6 I've used. I also have other types of @Resource that doesn't work either, so it isn't just DataSource resources. I've tried switching to Tomcat 7.0.27 and suddenly none of these constructs works any more. The resource is not injected. I also have other types of @Resource that doesn't work either, so it isn't just DataSource resources. However in each case the resource named does exist, and can be looked up via e.g.

new InitialContext().lookup("java:comp/env/jdbc/cLabs");

[They are defined by elements in context.xml]

This of course is a royal PITA as I spent some time a year or two ago replacing the latter with the former. Is there some other magic spell I have to weave with Tomcat 7 to make it work again?

Note that resources are injected correctly into Servlets, so it isn't completely broken. Some interaction between Tomcat and JSF.


Solution

  • Answering my own question, an improved version of @JeffE's answer. The basic problem is:

    1. A Tomcat6InjectionProvider was provided with JSF 2.0 but was removed at some point.
    2. The default WebContainerInjectionProvider doesn't process @Resource annotations, as JeffE points out.

    You can overcome this without web.xml context-entries as follows:

    1. Create a file called META-INF/services/com.sun.faces.spi.injectionprovider and add the following line to it:

      com.sun.faces.vendor.Tomcat7InjectionProvider:org.apache.catalina.core.DefaultInstanceManager
      

      The meaning of this line is that if the second class is present in the deployment, the first class is used as the injection provider. The second class above is part of Tomcat 7.

    2. Compile the following class.

    This version contains numerous improvements over JeffE's version. Specifically:

    • it processes superclasses, as required by the @Resource and @Resources Javadoc
    • it processes @Resource and @Resources annotations at the class level
    • it processes methods annotated with @Resource, as required by the @Resource Javadoc
    • it handles empty or missing name attributes of @Resources correctly, as required by the @Resource Javadoc
    • it restores the Field's original access
    • it has no dependencies on Tomcat classes.

    Adjust the package name above if you change its package name.

    package com.sun.faces.vendor;
    
    import com.sun.faces.spi.DiscoverableInjectionProvider;
    import com.sun.faces.spi.InjectionProviderException;
    import java.lang.reflect.Field;
    import java.util.logging.Level;
    import java.util.logging.Logger;
    import javax.annotation.Resource;
    import javax.naming.Context;
    import javax.naming.InitialContext;
    import javax.naming.NamingException;
    import javax.servlet.ServletContext;
    
    /**
     * @author Jeff E
     * @author Esmond Pitt Improvements named above.
     * 
     * @see javax.annotation.Resource
     *
     * @see <a href="http://stackoverflow.com/a/21978577/207421">This StackOverflow
     * answer, although what org.apache.catalina.util.Introspection may be and where
     * it lives remains a mystery.</a>
     */
    public class Tomcat7InjectionProvider
        extends DiscoverableInjectionProvider
    {
        private Logger logger = Logger.getLogger(this.getClass().getName());
        private ServletContext  servletContext;
    
        private WebContainerInjectionProvider   delegate = new WebContainerInjectionProvider();
    
        public Tomcat7InjectionProvider(ServletContext servletContext)
        {
            logger.config("constructed");
            this.servletContext = servletContext;
        }
    
        @Override
        public void inject(Object managedBean) throws InjectionProviderException
        {
            logger.log(Level.CONFIG, "managedBean={0}", new Object[]{managedBean.getClass().getName()});
            Class<?> clazz = managedBean.getClass();
            do
            {
                List<Resource>  classResources = new LinkedList<>();
                // Process class-level @Resources and @Resource
                if (clazz.isAnnotationPresent(Resources.class))
                {
                    Resources annotation = clazz.getAnnotation(Resources.class);
                    for (Resource resource : annotation.value())
                    {
                        classResources.add(resource);
                    }
                }
                if (clazz.isAnnotationPresent(Resource.class))
                {
                    Resource    annotation = clazz.getAnnotation(Resource.class);
                    classResources.add(annotation);
                }
                for (Resource annotation : classResources)
                {
                    String  name = annotation.name();
                    // Make sure the resource exists.
                    try
                    {
                        Context ctx = new InitialContext();
                        Object resource = ctx.lookup("java:comp/env/" + name);
                    }
                    catch (NamingException exc)
                    {
                        throw new InjectionProviderException("checking class resource " + annotation.name()+" of "+clazz.getName(), exc);
                    }
                }
                // Process fields with @Resource
                // see org.apache.catalina.core.DefaultInstanceManager
    //            Field[] fields = Introspection.getDeclaredFields(managedBean.getClass());
                Field[] fields = managedBean.getClass().getDeclaredFields();
                for (Field field : fields)
                {
                    if (field.isAnnotationPresent(Resource.class))
                    {
                        Resource annotation = field.getAnnotation(Resource.class);
                        String name = annotation.name();
                        logger.log(Level.CONFIG, "injecting @Resource(name=\"{2}\") into {0}.{1}", new Object[]
                            {
                                managedBean.getClass().getName(), field.getName(), name
                            });
                        try
                        {
                            Context ctx = new InitialContext();
                            Object resource;
                            if (name != null && name.length() > 0)
                            {
                                resource = ctx.lookup("java:comp/env/" + name);
                            }
                            else
                            {
                                resource = ctx.lookup(clazz.getName() + "/" + field.getName());
                            }
                            // field may be private
                            boolean accessibility = field.isAccessible();
                            try
                            {
                                field.setAccessible(true);
                                field.set(managedBean, resource);
                            }
                            finally
                            {
                                field.setAccessible(accessibility);
                            }
                        }
                        catch (NamingException | IllegalAccessException exc)
                        {
                            throw new InjectionProviderException("injecting resource " + annotation.name()+" into "+clazz.getName()+"."+field.getName(), exc);
                        }
                    }
                }
                // Process methods with @Resource
                for (Method method : clazz.getDeclaredMethods())
                {
                    if (method.isAnnotationPresent(Resource.class)
                    && method.getName().startsWith("set")
                    && method.getName().length() > 3
                    && method.getReturnType() == void.class
                    && method.getParameterTypes().length == 1)
                    {
                        // It's a setter with @Resource
                        Resource annotation = method.getAnnotation(Resource.class);
                        String name = annotation.name();
                        logger.log(Level.CONFIG, "injecting @Resource(name=\"{2}\") via {0}.{1}", new Object[]
                            {
                                managedBean.getClass().getName(), method.getName(), name
                            });
                        try
                        {
                            Context ctx = new InitialContext();
                            Object resource;
                            if (name != null && name.length() > 0)
                            {
                                resource = ctx.lookup("java:comp/env/" + name);
                            }
                            else
                            {
                                name = method.getName().substring(3);
                                name = name.substring(0,1).toLowerCase()+name.substring(1);
                                resource = ctx.lookup(clazz.getName() + "/" + name);
                            }
                            // method may be private
                            boolean accessibility = method.isAccessible();
                            try
                            {
                                method.setAccessible(true);
                                method.invoke(managedBean, resource);
                            }
                            finally
                            {
                                method.setAccessible(accessibility);
                            }
                        }
                        catch (NamingException | IllegalAccessException | InvocationTargetException exc)
                        {
                            throw new InjectionProviderException("injecting resource " + annotation.name()+" via "+clazz.getName()+"."+method.getName(), exc);
                        }
                    }
                }
            } while ((clazz = clazz.getSuperclass()) != Object.class);
        }
    
        @Override
        public void invokePostConstruct(Object managedBean) throws InjectionProviderException
        {
            logger.log(Level.CONFIG, "managedBean={0}", new Object[]{managedBean});
            delegate.invokePostConstruct(managedBean);
        }
    
        @Override
        public void invokePreDestroy(Object managedBean) throws InjectionProviderException
        {
            logger.log(Level.CONFIG, "managedBean={0}", new Object[]{managedBean});
            delegate.invokePreDestroy(managedBean);
        }
    }
    

    E&OE