Jersey 2 + HK2 - automatic binding of classess

2019-04-30 19:02发布

Continuation of topic Jersey 2 + HK2 - @ApplicationScoped not working.

I already know, how to bind classes in order to @Inject them properly.

Do you have any ideas, how to automize this process? Putting every single service in bind statements seems like very bad smell in my application.

3条回答
神经病院院长
2楼-- · 2019-04-30 19:44

I Have a sugestion that solved my problem here, i've tried proposed solution and not worked here. In my solution it's necessary annotate each class with @MyInjectable annotation.

1-Create an Annotation

@Retention(RUNTIME)
@Target(ElementType.TYPE)
public @interface MyInjectable {
}

2-Create a AbstractBinder implementation

public class MyApplicationBinder extends AbstractBinder {
    @Override
    protected void configure() {
        bindFactory(EMFFactory.class).to(EntityManagerFactory.class).in(Singleton.class);
        bindFactory(EMFactory.class).to(EntityManager.class).in(RequestScoped.class);
        bind(Environment.class).to(Environment.class);
        scanAndBind("com.yourpackage.here");
    }

    private void scanAndBind(String packageName) {
        try {
            Class[] classes = getClasses(packageName);
            for (Class<?> klazz:
                 classes) {
                MyInjectable annotation = klazz.getAnnotation(MyInjectable.class);
                if (annotation!= null) {
                    bind(klazz).to(klazz);
                }
            }
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    private static Class[] getClasses(String packageName)
            throws ClassNotFoundException, IOException {
        ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
        assert classLoader != null;
        String path = packageName.replace('.', '/');
        Enumeration<URL> resources = classLoader.getResources(path);
        List<File> dirs = new ArrayList<>();
        while (resources.hasMoreElements()) {
            URL resource = resources.nextElement();
            dirs.add(new File(resource.getFile()));
        }
        ArrayList<Class> classes = new ArrayList<Class>();
        for (File directory : dirs) {
            classes.addAll(findClasses(directory, packageName));
        }
        return classes.toArray(new Class[classes.size()]);
    }

    private static List<Class> findClasses(File directory, String packageName) throws ClassNotFoundException {
        List<Class> classes = new ArrayList<Class>();
        if (!directory.exists()) {
            return classes;
        }
        File[] files = directory.listFiles();
        for (File file : files) {
            if (file.isDirectory()) {
                assert !file.getName().contains(".");
                classes.addAll(findClasses(file, packageName + "." + file.getName()));
            } else if (file.getName().endsWith(".class")) {
                classes.add(Class.forName(packageName + '.' + file.getName().substring(0, file.getName().length() - 6)));
            }
        }
        return classes;
    }

}

3-Create a ResourceConfig

public class MyApplication extends ResourceConfig {
    @Inject
    public MyApplication(ServiceLocator locator) {
        ServiceLocatorUtilities.enableImmediateScope(locator);
        ....
        register(new MyApplicationBinder());
    }
}

4-Configure properly in web.xml

<servlet>
    <servlet-name>Jersey Web Application</servlet-name>
    <servlet-class>org.glassfish.jersey.servlet.ServletContainer</servlet-class>
    <init-param>
        <param-name>jersey.config.server.provider.packages</param-name>
        <param-value>br.com.solutiontrue.ws</param-value>
    </init-param>
    <init-param>
        <param-name>com.sun.jersey.api.json.POJOMappingFeature</param-name>
        <param-value>true</param-value>
    </init-param>
    <init-param>
        <param-name>javax.ws.rs.Application</param-name>
        <param-value>your.package.name.MyApplication</param-value>
    </init-param>
    <init-param>
        <param-name>jersey.config.server.resource.validation.disable</param-name>
        <param-value>true</param-value>
    </init-param>

    <load-on-startup>1</load-on-startup>
</servlet>
查看更多
放荡不羁爱自由
3楼-- · 2019-04-30 19:47

I would suggest first looking here: Automatic Service Population.

The basic process is to use @Service annotations on your classes and use the JSR-269 (APT) processor (Metadata Generator) at build time. Doing so will add some metadata to your jar files (normally under META-INF/hk2-locator/default).

You can then make sure these services get picked up automatically rather than having to do all those pesky binds by using a Populator which you get from the Dynamic Configuration Service which is available in every ServiceLocator.

The pseudo-code would be something like this:

public void populate(ServiceLocator locator) throws Exception {
    DynamicConfigurationService dcs = locator.getService(DynamicConfigurationService.class);
    Populator populator = dcs.getPopulator();
    populator.populate(new ClasspathDescriptorFileFinder(getClass().getClassLoader()));
}

In the above code the ClasspathDescriptorFileFinder is used to search through the classpath to find the metadata. Other strategies could be used in environments like OSGi.

IMO this is a much better way to add services rather than doing all the binds by yourself.

查看更多
再贱就再见
4楼-- · 2019-04-30 20:02

After using Google's Guice for a number of years, I am accustomed to the availability of a Just-In-Time binder, allowing the injection of arbitrary types without requiring any upfront configuration.

I too found the idea of having to explicitly bind every service to be a bad code smell. I'm also not crazy about the need to use a special build step and the added initialization code for the populator.

So I came up with the following JustInTimeResolver implementation:

/**
 * Mimic GUICE's ability to satisfy injection points automatically,
 * without needing to explicitly bind every class, and without needing
 * to add an extra build step.
 */
@Service
public class JustInTimeServiceResolver implements JustInTimeInjectionResolver {

    @Inject
    private ServiceLocator serviceLocator;

    @Override
    public boolean justInTimeResolution( Injectee injectee ) {
    final Type requiredType = injectee.getRequiredType();

        if ( injectee.getRequiredQualifiers().isEmpty() && requiredType instanceof Class ) {
            final Class<?> requiredClass = (Class<?>) requiredType;

            // IMPORTANT: check the package name, so we don't accidentally preempt other framework JIT resolvers
            if ( requiredClass.getName().startsWith( "com.fastmodel" )) {
                final List<ActiveDescriptor<?>> descriptors = ServiceLocatorUtilities.addClasses( serviceLocator, requiredClass );

                if ( !descriptors.isEmpty() ) {
                    return true;
                }
            }
        }
        return false;
    }
} 

With this in my project, I simply added the following to my binder in my Jersey application configuration:

bind( JustInTimeServiceResolver.class ).to( JustInTimeInjectionResolver.class );

and I get automatic binding creation like I did in Guice.

查看更多
登录 后发表回答