简体   繁体   中英

It there a “Spring way” to get an implementation from an annotated interface?

I want to scrap some boilerplate. Say I have a custom interface with custom annotations:

interface MyInterface {
    @DoSomething("crazy")
    public String aMethod(int numberOfJumps); 
}

Now I can write an InvocationHandler , and generate a Proxy implementation which does something more or less useful based on the annotation and method arguments, and returns an appropriate result. This works fine.

My question is if I could use some mechanism from Spring to get the same result, but maybe safer, faster, more flexible and/or more configurable. The Spring AOP annotations look promising, but they seem to require a class, not an interface.

[Update]

In order to make clear what I want here the outline of my current code:

public interface TestInterface {
    @MyAnnotation(name = "foo")
    public void testMethod(String arg);
}

public class AnnotationProxy {
    @SuppressWarnings("unchecked")
    public static <T> T getImplementation(Class<T> annotatedInterface) {
       return (T) Proxy.newProxyInstance(annotatedInterface.getClassLoader(),
               new Class<?>[]{annotatedInterface},
               new AnnotationHandler());
    }

    private static class AnnotationHandler implements InvocationHandler {
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            System.out.println("Args " + Arrays.toString(args));
            System.out.println("Annotations" + Arrays.toString(method.getAnnotations()));
            return "useful value";
        }
    }
}

TestInterface ti = getImplementation(TestInterface.class);
String s = ti.testMethod("xyz"); //"useful value"

As you can see, I create an object out of thin air (and some ugly reflection stuff). I want to know if I can to do this in a more civilized and spring-like way.

One way to do it is to scan for interfaces which have methods with your favorite annotation, create and register proxies in the Spring application context.

Example

Let's have an interface Test which we want to create and register the proxy for:

package com.test;

public interface Test {
    @DoSomething(pattern = "[%s]")
    void print(String value);
}

Our annotation looks like this:

package com.test;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;

@Retention(RetentionPolicy.RUNTIME)
public @interface DoSomething {

    String pattern();

}

And we would like to use it in an application like this:

import org.springframework.context.support.ClassPathXmlApplicationContext;

import com.test.Test;

public class Main {
    public static void main(final String[] args) throws Exception {
        final ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext("test.xml");
        final Test test = ctx.getBean(Test.class);
        test.print("Hello");
    }
}

Our test.xml Spring configuration file contains just a scan for components:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:context="http://www.springframework.org/schema/context"
    xsi:schemaLocation="http://www.springframework.org/schema/beans
           http://www.springframework.org/schema/beans/spring-beans-3.1.xsd
           http://www.springframework.org/schema/context
           http://www.springframework.org/schema/context/spring-context-3.1.xsd">

    <context:component-scan base-package="com.test"/>
</beans>

Not to the point. In order to create and register proxies we have to implement BeanFactoryPostProcessor to scan for the interfaces and create proxies:

package com.test;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.Map;
import java.util.Set;

import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.BeanFactoryPostProcessor;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.core.io.Resource;
import org.springframework.core.type.AnnotationMetadata;
import org.springframework.core.type.MethodMetadata;
import org.springframework.core.type.classreading.MetadataReader;
import org.springframework.core.type.classreading.SimpleMetadataReaderFactory;
import org.springframework.stereotype.Component;

@Component
public class DoSomethingPostprocessor implements BeanFactoryPostProcessor, ApplicationContextAware {

    private ApplicationContext applicationContext;

    private Object createDoSomethingBean(final MethodMetadata mmd, final Map<String, Object> attributes)
            throws Exception {
        final String pattern = (String) attributes.get("pattern");
        final Class<?> clazz = Class.forName(mmd.getDeclaringClassName());
        return Proxy.newProxyInstance(clazz.getClassLoader(), new Class[] { clazz },
                new InvocationHandler() {

                    @Override
                    public Object invoke(final Object proxy, final Method method, final Object[] args) throws Throwable {
                        System.out.println(String.format(pattern, args[0]));

                        return null;
                    }
                });
    }

    @Override
    public void postProcessBeanFactory(final ConfigurableListableBeanFactory beanFactory) throws BeansException {
        try {
            final String packageSearchPath = "classpath*:com/**/*.class";

            final Resource[] resources =
                    applicationContext.getResources(packageSearchPath);
            final SimpleMetadataReaderFactory factory = new
                    SimpleMetadataReaderFactory(applicationContext);

            for (final Resource resource : resources) {
                final MetadataReader mdReader = factory.getMetadataReader(resource);

                final AnnotationMetadata am = mdReader.getAnnotationMetadata();
                final Set<MethodMetadata> methodMetadata =
                        am.getAnnotatedMethods(DoSomething.class.getName());
                for (final MethodMetadata mmd : methodMetadata) {
                    final Map<String, Object> attributes =
                            mmd.getAnnotationAttributes(DoSomething.class.getName());
                    final String beanName = mmd.getDeclaringClassName();
                    beanFactory.registerSingleton(beanName, createDoSomethingBean(mmd, attributes));
                }
            }
        } catch (final Exception e) {
            throw new RuntimeException(e);
        }

    }

    @Override
    public void setApplicationContext(final ApplicationContext applicationContext) throws BeansException {
        this.applicationContext = applicationContext;
    }
}

You can use Spring AOP to intercept and post process annotated methods:

@Aspect
public class ProcessDoSomethingAspect {

    @AfterReturning(value = "execution(@annotation(doSomething) * *.*(..))", returning = "result")
    public String processResult(my.package.DoSomething doSomething, String result) {
        // process result here
        return result;
    }   

}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM