假设现在我们有一个已知的算法,我们需要写任意一个接口打上我们特有的标签,那么这个接口的方法都可以执行这个算法,好比Mybatis的Dao,或者Feign的接口。现在假设我们这个特有的标签如下:
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface ProxyVersion {
}
已知的算法为打印方法的所有参数。
@Override
public Object invoke(Object[] argv) throws Throwable {
Stream.of(argv).sequential().forEach(System.out::println);
return null;
}
-------------------------------------------------------------
现在需求清楚了,我们来随意写个接口,并打上该标签。
@ProxyVersion
public interface ProxyTest {
String find(String a, Integer b);
}
先写一个动态代理类
@AllArgsConstructor
public class ProxyInvocationHandler implements InvocationHandler {
private Map<Method,MethodHandler> dispatch;
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
return dispatch.get(method).invoke(args);
}
}
其中MethodHandler为方法处理器接口,定义如下
public interface MethodHandler {
Object invoke(Object[] argv) throws Throwable;
}
然后写一个方法处理器接口的实现类,它包含了我们固定要实现的算法。
public class DefaultMethodHandler implements MethodHandler {
@Override
public Object invoke(Object[] argv) throws Throwable {
Stream.of(argv).sequential().forEach(System.out::println);
return null;
}
}
我们首先写一个目标类,因为我们不知道我们要代理的是啥接口,所以使用泛型,并且包含一个该泛型的Class属性type。
@Data
@AllArgsConstructor
public class Target<T> {
private Class<T> type;
}
然后为来创建该目标类,写一个目标工厂类,从该目标工厂类去搜索包下的所有类,并获取带有@ProxyVersion标签的接口放入我们的目标对象属性中。这里我们做了简化处理,只取第一个接口。
public class TargetFactory {
public static Target createTarget() {
Set<Class<?>> classes = ClassUtil.getClassSet("com.guanjian.demo.proxytest");
List<Class<?>> collect = classes.stream()
.filter(Class::isInterface)
.filter(clazz -> clazz.isAnnotationPresent(ProxyVersion.class))
.collect(Collectors.toList());
return new Target(collect.get(0));
}
}
ClassUtil代码请参考@Compenent,@Autowired,@PostConstruct自实现
现在我们要调用动态代理类,这里我们也做了简化处理,只取第一个方法。最终返回我们代理的接口实例
public class ProxyBean {
public Object proxyTest() {
Map<Method,MethodHandler> methodToHandler = new HashMap<>();
//获取目标对象
Target target = TargetFactory.createTarget();
//将目标对象的方法以及方法处理器(方法处理器包含我们需要的固定算法)放入映射中
methodToHandler.put(target.getType().getMethods()[0],new DefaultMethodHandler());
//构建动态代理对象
InvocationHandler handler = new ProxyInvocationHandler(methodToHandler);
//返回动态代理代理的接口实例
return Proxy.newProxyInstance(target.getType().getClassLoader(), new Class[]{target.getType()}, handler);
}
}
再加一个ProxyBean的工厂
public class ProxyBeanFactory {
public static ProxyBean createProxyBean() {
return new ProxyBean();
}
}
最后写测试
public class ProxyMain {
public static void main(String[] args) {
ProxyTest test = (ProxyTest)ProxyBeanFactory.createProxyBean().proxyTest();
test.find("aaa",233);
}
}
运行结果
aaa
233
如果我们换一个接口替换ProxyTest也是一样,随意定义接口,都可以获取执行的结果。
由于我们在使用Mybatis或者Feign的时候都是使用依赖注入的,所以我们现在要将该例子修改为Spring依赖注入的形式。在此要感谢我的好兄弟雄爷(李少雄)提供的支持。
要实现Spring依赖注入,我们需要修改一部分代码。
首先要将TargetFactory修改如下,不再使用静态方法,而修改成单例模式,便于获取接口的类型。
public class TargetFactory {
private Set<Class<?>> classes = ClassUtil.getClassSet("com.guanjian.demo.proxytest");
private static final TargetFactory instance = new TargetFactory();
public static TargetFactory getInstance(){
return instance;
}
private TargetFactory() {}
public Target createTarget() {
return new Target(getNeedProxyClass());
}
public Class<?> getNeedProxyClass() {
List<Class<?>> collect = classes.stream()
.filter(Class::isInterface)
.filter(clazz -> clazz.isAnnotationPresent(ProxyVersion.class))
.collect(Collectors.toList());
return collect.get(0);
}
}
所以ProxyBean在获取target目标对象的时候需要由单例来获取,并且proxyTest方法返回改为泛型。
public class ProxyBean<T> {
public T proxyTest() {
Map<Method,MethodHandler> methodToHandler = new HashMap<>();
//获取目标对象
Target target = TargetFactory.getInstance().createTarget();
//将目标对象的方法以及方法处理器(方法处理器包含我们需要的固定算法)放入映射中
methodToHandler.put(target.getType().getMethods()[0],new DefaultMethodHandler());
//构建动态代理对象
InvocationHandler handler = new ProxyInvocationHandler(methodToHandler);
//返回动态代理代理的实例
return (T) Proxy.newProxyInstance(target.getType().getClassLoader(), new Class[]{target.getType()}, handler);
}
}
ProxyBean工厂需要实现FactoryBean接口,该接口为Spring环境的接口(org.springframework.beans.factory.FactoryBean)
@AllArgsConstructor
public class ProxyBeanFactory<T> implements FactoryBean<T> {
private Class<T> interfaceType;
/**
* 返回由FactoryBean创建的bean实例,如果isSingleton()返回true,则该实例会放到Spring容器中单实例缓存池中。
* @return
* @throws Exception
*/
@Override
public T getObject() throws Exception {
ProxyBean<T> proxyBean = new ProxyBean<>();
return proxyBean.proxyTest();
}
/**
* 返回FactoryBean创建的bean类型。
* @return
*/
@Override
public Class<T> getObjectType() {
return interfaceType;
}
/**
* 返回由FactoryBean创建的bean实例的作用域是singleton还是prototype。
* @return
*/
@Override
public boolean isSingleton() {
return true;
}
}
然后我们需要对接口以及动态代理代理的接口实例在Spring环境中进行bean的动态注册,其实无论是Mybatis的Dao还是Feign的接口都是这么处理的。
/**
* 实现BeanDefinitionRegistryPostProcessor接口来动态生成bean
*/
@Component
public class DynamicProxyServiceBeanRegister implements BeanDefinitionRegistryPostProcessor {
@Override
public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException {
//获取我们需要的接口类型
Class<?> beanClazz = TargetFactory.getInstance().getNeedProxyClass();
//根据接口类生成一个bean的建造器
BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(beanClazz);
//根据该建造起获取bean的定义器
GenericBeanDefinition definition = (GenericBeanDefinition) builder.getRawBeanDefinition();
//定义该接口类型为该bean的匹配类型
//如使用SpringbootApplication.getBean(匹配类型.class)
definition.getConstructorArgumentValues().addGenericArgumentValue(beanClazz);
//定义bean工厂中的动态代理代理的接口实例为bean的对象
//如 bean对象=SpringbootApplication.getBean(匹配类型.class)
definition.setBeanClass(ProxyBeanFactory.class);
//定义@Autowired的装配方式,这里用默认装配方式即可
// definition.setAutowireMode(GenericBeanDefinition.AUTOWIRE_BY_TYPE);
//注册该定义器,并把接口名做为注册的标识
registry.registerBeanDefinition(beanClazz.getSimpleName(), definition);
}
@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
}
}
最后进行测试
@Component
public class ProxyMain {
@Autowired
private ProxyTest proxyTest;
@PostConstruct
public void action() {
proxyTest.find("aaa",233);
}
}
启动Springboot,运行结果如下
2019-11-27 22:48:09.419 INFO 535 --- [ main] com.guanjian.demo.DyimportApplication : Starting DyimportApplication on admindeMacBook-Pro.local with PID 535 (/Users/admin/Downloads/dyimport/target/classes started by admin in /Users/admin/Downloads/dyimport)
2019-11-27 22:48:09.422 INFO 535 --- [ main] com.guanjian.demo.DyimportApplication : No active profile set, falling back to default profiles: default
aaa
233
2019-11-27 22:48:10.025 INFO 535 --- [ main] com.guanjian.demo.DyimportApplication : Started DyimportApplication in 15.783 seconds (JVM running for 26.124)