一、简介
在上一章节《走进Spring框架系列—IOC应用进阶篇》中,笔者一一介绍了几个Spring框架中比较进阶的应用,为了更加深刻体会它们的作用,本章笔者将会带大家使用它们模拟实现简易版的Spring AOP和@MapperScan。
本章的两个模拟都是参考原版的源码,整体实现思路是一致的,所以,如果你对Spring AOP或者@MapperScan的原理感兴趣的话,也请不要错过本章节。
二、AOP模拟
这里实现AOP我要用到的是ImportSelector和BeanPostProcessor,真实的Spring AOP用的是ImportBeanDefinitionRegistrar和BeanPostProcessor,因为它在注册一个BeanPostProcessor前需要动态更改bean的一些属性和参数,我这里就不做那么麻烦了,直接用ImportSelector注册就好。
由于这里只是模拟一个AOP,所以只会提供大概的实现思路,有些复杂的具体逻辑我这里会写死,比如Spring AOP让我们提供的切面,里面包含了切点,切入逻辑等等,还有JDK和CGLib的选择。
阅读本模拟案例,需要你对JDK动态代理的使用有了解。
废话不多说,先定义一个代理类生成接口:
/**
* @Author: Richard Lv
* @Date: 2021/3/1 15:51
* @Description: 为了规范编程,我们还是定义一个代理类生成接口,
* 在我们模拟的AOP中,该接口只有唯一一个实现类,就是使用JDK生成代理的类,
* 在Spring AOP中也有这样一个类似的接口,它的实现类就有JDK和CGLib两个代理生成类
*/
public interface AopProxy {
Object createProxy(Class<?> clazz,Object target);
}
然后编写该接口唯一的实现类,由于要生成代理类,所以顺便也实现了InvocationHandler接口:
/**
* @Author: Richard Lv
* @Date: 2021/3/1 15:52
* @Description: JDK生成代理工厂类
*/
public class MyJDKProxy implements AopProxy, InvocationHandler {
//目标对象
private Object target;
@Override
public Object createProxy(Class<?> clazz,Object target) {
//获得需要代理的类的所有接口
Class<?>[] interfaces = clazz.getInterfaces();
//给目标对象变量赋值
this.target=target;
//创建JDK动态代理并返回
return Proxy.newProxyInstance(this.getClass().getClassLoader(),interfaces,this);
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
/*
* 代理逻辑,我这里就简单写了一个打印"我被代理了"字符串
* 真实的Spring AOP会在这里利用beanFactory得到我们写的切面类
* 获取里面的连接点,织入逻辑等等,然后在这里反射调用
*/
System.out.println("我被代理了");
/*
* 反射调用目标方法
*/
method.invoke(target,args);
return proxy;
}
}
接下来就开始写一个BeanPostProcessor拦截真实类,并替换成代理类:
/**
* @Author: Richard Lv
* @Date: 2021/3/1 15:49
* @Description: 简易版AOP后置处理器
*/
public class MyAopPostProcessor implements BeanPostProcessor {
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
/*
* 这里要注意几点:
* 1.为什么没有在MyJDKProxy类上加@Component然后在这里直接使用@Autowired注入进来?
* 很简单,因为我这里是在模拟Spring AOP,那么在Spring框架源码中,它怎么可能使用@Component和@Autowired,这
* 两个注解本来就是Spring框架定义的,那不就套娃了吗,在Spring框架源码中,对象要么是new出来的,要么是反射生成的
* 2.为什么把new MyJDKProxy()写死了?
* 再次强调,我这里是模拟简易的AOP,所以只写了这一个实现类,要完全模拟的话,还应该有一个MyCGLibProxy实现类,甚至
* 还可以扩展其他动态代理实现逻辑类,那么,这里就会根据用户配置来判断应该用哪一个类。
* 3. 贴出Spring AOP源码证实上面两点:
* public org.springframework.aop.framework.AopProxy createAopProxy(AdvisedSupport config) throws
* AopConfigException {
* if (config.isOptimize() || config.isProxyTargetClass() || hasNoUserSuppliedProxyInterfaces(config)) {
* Class<?> targetClass = config.getTargetClass();
* if (targetClass == null) {
* throw new AopConfigException("TargetSource cannot determine target class: " +
* "Either an interface or a target is required for proxy creation.");
* }
* if (targetClass.isInterface() || Proxy.isProxyClass(targetClass)) {
* return new JdkDynamicAopProxy(config);
* }
* return new ObjenesisCglibAopProxy(config);
* }
* else {
* return new JdkDynamicAopProxy(config);
* }
* }
* 这个方法只有一些if else,很容易读,可以看到,spring aop把创建代理生成类封装成了一个方法,并传了一个配置信息类作为参数
* 然后根据配置信息,创建对应的代理生成类
*/
AopProxy aopProxy=new MyJDKProxy();
//这里Spring AOP会提前解析切面,用来判断当前bean是否需要代理,我这里因为没有切面,就直接对所有类进行代理
return aopProxy.createProxy(bean.getClass(),bean);
}
}
至此就完成了一半,然后定义一个开启AOP功能的注解开关,忘了的可以返回上一篇文章看ImportSelector相关内容:
@Retention(RetentionPolicy.RUNTIME)
@Import({MyImportSelector.class})
public @interface EnableMyAop {
}
编写MyImportSelector类实现ImportSelector接口,动态把我的MyAopPostProcessor放入spring容器:
/**
* @Author: Richard Lv
* @Date: 2021/2/25 16:14
* @Description: 动态把处理AOP的BeanPostProcessor交给spring容器
*/
public class MyImportSelector implements ImportSelector {
@Override
public String[] selectImports(AnnotationMetadata annotationMetadata) {
//检查加了@Import注解的类上有没有@EnableMyAop注解
if(annotationMetadata.hasAnnotation(EnableMyAop.class.getName())){
return new String[]{MyAopPostProcessor.class.getName()};
}
return new String[0];
}
}
最后,搞个类来测试AOP效果,因为JDK动态代理是基于接口的,所以写一个接口,让我的Richard类实现它:
/**
* @Author: Richard Lv
* @Date: 2021/3/1 17:36
* @Description:
*/
public interface Human {
void selfIntroduction();
}
改造后的Richard:
/**
* @Author: Richard Lv
* @Date: 2021/2/23 17:59
* @Description:
*/
@Component("richard")
public class Richard implements Human{
@Override
public void selfIntroduction(){
System.out.println("Hello everyone,My name is Richard");
}
}
大功告成,开始测试,首先,不在配置类上加我的@EnableMyAop注解,正常运行主类:
/**
* @Author: Richard Lv
* @Date: 2021/1/29 10:44
* @Description: 测试类
*/
public class Test {
public static void main(String[] args) {
AnnotationConfigApplicationContext beanFactory=new AnnotationConfigApplicationContext(SpringConfig.class);
/*
* 一旦被动态代理,这里获取的就不是原先的Richard类,而是一个代理类,所以我用Human接口来接收,
* 开启AOP后,这里也不用修改代码,这也是面向抽象编程的好处。
*/
Human richard = (Human) beanFactory.getBean("richard");
richard.selfIntroduction();
}
}
运行结果:
Hello everyone,My name is Richard
在配置类上加上@EnableMyAop,开启我的简易版AOP:
/**
* @Author: Richard Lv
* @Date: 2021/2/2 18:27
* @Description: spring 配置类
*/
@Configuration
@ComponentScan("com.richard")
@EnableMyAop
public class SpringConfig { }
再次运行主类,得到结果打印如下:
我被代理了
Hello everyone,My name is Richard
至此,一个简单的AOP功能就完成了。其实,从本案例中,对AOP概念模糊的读者也可以从中体会到AOP面向切面的真正含义,一个不影响原方法逻辑的增强功能,那么我们就可以把它提出来作为AOP切面逻辑,通过开关(注解)的开启,我们就可以让指定的类获得增强功能,在实际应用中,这样的增强功能就是日志、事务等等,那么又如何让一个类的方法获得增强?那就是使用代理,这就是AOP的本质。
三、@MapperScan模拟
不熟悉@MapperScan应用的读者也大可放心阅读,几乎不涉及其真实应用,这一个模拟用到了ImportBeanDefinitionRegistrar和FactoryBean。
与AOP模拟一样,省略了一些与接口演示无关的逻辑,比如扫描包,执行sql语句等等。
先写一个Mapper接口:
/**
* @Author: Richard Lv
* @Date: 2021/3/2 13:52
* @Description:
*/
public interface UserMapper {
@Select("SELECT * FROM t_userinfo WHERE id = 1")
User getUser();
}
我们知道,接口是无法被实例化的,所以我们也不可能直接把这个接口交给spring容器来管理,那么我们就需要在运行期间动态生成接口的实现类,怎么做呢?用JDK动态代理,生成一个接口对应的代理类,那么又有一个问题了,一个动态代理类能不能被spring容器实例化出来呢,目前,我们不知道,那就先试一试,写一个自定义的ImportBeanDefinitionRegistrar实现类,来注入一个对mapper接口生成的代理类:
/**
* @Author: Richard Lv
* @Date: 2021/2/25 17:37
* @Description: 自定义的ImportBeanDefinitionRegistrar,可以动态注入一个bean给spring容器
*/
public class MyImportBeanDefinitionRegistrar implements ImportBeanDefinitionRegistrar {
@Override
public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
/*
* 本来应该解析用户配置然后扫描指定包下所有的mapper接口,然后遍历循环进行下列操作
* 但本次模拟对简化扫描操作,直接对UserMapper单个接口进行注册
*/
//对UserMapper接口生成代理类
Object o = Proxy.newProxyInstance(this.getClass().getClassLoader(), new Class[]{UserMapper.class},
(proxy, method, args) -> {
System.out.println("执行sql逻辑");
return null;
});
/*
* registerBeanDefinition(String s, BeanDefinition beanDefinition)方法是我们最终要向spring容器注册bean的方法,
* 第一个参数是bean的名字,方便我们向容器取出bean
* 第二个参数是一个数据结构,叫BeanDefinition,它是spring容器对bean的定义,也就是存的bean的元数据,相当于java
* 中用来描述一个类的类Clazz
* spring提供了方法让我们去创建一个BeanDefinition,先根据我们要创建bean的对象的类对象Clazz获取一个BeanDefinitionBuilder,
* BeanDefinitionBuilder就能为我们创建出一个BeanDefinition
*/
//根据生成的代理类的类对象创建BeanDefinitionBuilder
BeanDefinitionBuilder beanDefinitionBuilder = BeanDefinitionBuilder.genericBeanDefinition(o.getClass());
//BeanDefinitionBuilder为我们创建一个BeanDefinition,它返回的是一个父类对象AbstractBeanDefinition,
//我们这里把它强转为更常用的基本BeanDefinition类型GenericBeanDefinition,当然,你要用父类对象也是可以的,只是子类对象有更丰富的API
GenericBeanDefinition beanDefinition = (GenericBeanDefinition) beanDefinitionBuilder.getBeanDefinition();
//向spring容器注册BeanDefinition
registry.registerBeanDefinition("mapper",beanDefinition);
}
}
基本完成,现在定义我自己的MapperScan注解,并import上面的MyImportBeanDefinitionRegistrar:
@Retention(RetentionPolicy.RUNTIME)
@Import({MyImportBeanDefinitionRegistrar.class})
public @interface MyMapperScan {
}
配置类加上注解:
@Configuration
@ComponentScan(value = "com.richard")
@MyMapperScan
public class SpringConfig { }
修改主类:
/**
* @Author: Richard Lv
* @Date: 2021/1/29 10:44
* @Description: 测试类
*/
public class Test {
public static void main(String[] args) {
AnnotationConfigApplicationContext beanFactory=new AnnotationConfigApplicationContext(SpringConfig.class);
UserMapper mapper = (UserMapper) beanFactory.getBean("mapper");
mapper.getUser();
}
}
运行发现报错了:
警告: Exception encountered during context initialization - cancelling refresh attempt: org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'mapper': Unsatisfied dependency expressed through constructor parameter 0; nested exception is org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean of type 'java.lang.reflect.InvocationHandler' available: expected at least 1 bean which qualifies as autowire candidate. Dependency annotations: {}
Exception in thread "main" org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'mapper': Unsatisfied dependency expressed through constructor parameter 0; nested exception is org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean of type 'java.lang.reflect.InvocationHandler' available: expected at least 1 bean which qualifies as autowire candidate. Dependency annotations: {}
at org.springframework.beans.factory.support.ConstructorResolver.createArgumentArray(ConstructorResolver.java:797)
at org.springframework.beans.factory.support.ConstructorResolver.autowireConstructor(ConstructorResolver.java:227)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.autowireConstructor(AbstractAutowireCapableBeanFactory.java:1356)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBeanInstance(AbstractAutowireCapableBeanFactory.java:1203)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:556)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:516)
at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:324)
at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:234)
at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:322)
at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:202)
at org.springframework.beans.factory.support.DefaultListableBeanFactory.preInstantiateSingletons(DefaultListableBeanFactory.java:897)
at org.springframework.context.support.AbstractApplicationContext.finishBeanFactoryInitialization(AbstractApplicationContext.java:879)
at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:551)
at org.springframework.context.annotation.AnnotationConfigApplicationContext.<init>(AnnotationConfigApplicationContext.java:89)
at com.richard.Test.main(Test.java:22)
很明显,我们的代理类没法被实例化,因为spring没法维护里面的依赖关系,那怎么办呢?还记得之前讲的FactoryBean吗,这时候它就能排上用场了,FactoryBean是可以被实例化的,我们在它的getObject()方法中来创建这个代理类不就可以了,说干就干,写一个自定义的FactoryBean:
/**
* @Author: Richard Lv
* @Date: 2021/3/2 17:09
* @Description: 自定义的FactoryBean,顺便把代理用的InvocationHandler接口也实现了
*/
public class MyMapperFactoryBean implements FactoryBean, InvocationHandler {
@Override
public Object getObject() throws Exception {
Object o = Proxy.newProxyInstance(this.getClass().getClassLoader(), new Class[]{UserMapper.class}, this);
return o;
}
@Override
public Class<?> getObjectType() {
return UserMapper.class;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("执行sql逻辑");
return null;
}
}
修改MyImportBeanDefinitionRegistrar类:
/**
* @Author: Richard Lv
* @Date: 2021/2/25 17:37
* @Description: 自定义的ImportBeanDefinitionRegistrar,可以动态注入一个bean给spring容器
*/
public class MyImportBeanDefinitionRegistrar implements ImportBeanDefinitionRegistrar {
@Override
public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
BeanDefinitionBuilder beanDefinitionBuilder = BeanDefinitionBuilder.genericBeanDefinition(MyMapperFactoryBean.class);
GenericBeanDefinition beanDefinition = (GenericBeanDefinition) beanDefinitionBuilder.getBeanDefinition();
registry.registerBeanDefinition("mapper",beanDefinition);
}
}
再次运行主类,结果打印如下:
执行sql逻辑
那么基本功能就完成了,下面进行一些优化,MyMapperFactoryBean中我们把代理接口类型写死了,所以我们要增加一个构造方法,把需要代理的接口类型给传过来,顺便,我们把执行sql逻辑那里也改造一下,让其更贴近真实应用:
/**
* @Author: Richard Lv
* @Date: 2021/3/2 17:09
* @Description: 自定义的FactoryBean,顺便把代理用的InvocationHandler接口也实现了
*/
public class MyMapperFactoryBean implements FactoryBean, InvocationHandler {
Class<?> clazz;
/**
* 加入该构造方法,让接口类型作为参数传进来
* 那么这个MyMapperFactoryBean对象就可以为任何接口提供注入方式
*/
public MyMapperFactoryBean(Class<?> clazz){
this.clazz=clazz;
}
@Override
public Object getObject() throws Exception {
return Proxy.newProxyInstance(this.getClass().getClassLoader(), new Class[]{clazz}, this);
}
@Override
public Class<?> getObjectType() {
return clazz;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
//获取方法上的@Select注解的值,返回的是一个数组,为了简洁,我直接取第0个
String value = method.getDeclaredAnnotation(Select.class).value()[0];
System.out.println("执行sql:"+value);
return null;
}
}
MyMapperFactoryBean就改造好了,但还没完,MyMapperFactoryBean之前只有一个默认的无参构造方法,Spring容器拿到它的Clazz类型,就可以直接通过Clazz.newInstance()反射生成实例对象,但是现在只有个带参构造方法,spring容器就没法去实例化它了,所以我们要修改它的BeanDefinition,设置参数名字让spring容器能通过byName方式把对应的接口类型注入进来,这里不懂的可以先看看有个映像,以后源码章节会讲解相关知识:
/**
* @Author: Richard Lv
* @Date: 2021/2/25 17:37
* @Description: 自定义的ImportBeanDefinitionRegistrar,可以动态注入一个bean给spring容器
*/
public class MyImportBeanDefinitionRegistrar implements ImportBeanDefinitionRegistrar {
@Override
public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
//Map<String, Object> attributes = annotationMetadata.getAnnotationAttributes(MyMapperScan.class.getName());
//假如我的@MyMapperScan中定义了一个value属性用来让用户配置需要扫描的包路径,这里我就要取出value属性的值
//assert attributes != null;
//String packagePath= (String) attributes.get("value");
//假设有一个scanPackages()方法,参数就是用户配置的包路径,该方法会返回指定路径下所有的mapper接口,然后遍历接口集合
//for (Class<?> clazz:scanPackages(packagePath)){
BeanDefinitionBuilder beanDefinitionBuilder = BeanDefinitionBuilder.genericBeanDefinition(MyMapperFactoryBean.class);
GenericBeanDefinition beanDefinition = (GenericBeanDefinition) beanDefinitionBuilder.getBeanDefinition();
/*设置构造方法的参数名字,这里写死为UserMapper接口的类型名字,在真实应用中大概是注释代码中所展示的样子*/
//beanDefinition.getConstructorArgumentValues().addGenericArgumentValue(clazz.getTypeName());
beanDefinition.getConstructorArgumentValues().addGenericArgumentValue(UserMapper.class.getTypeName());
registry.registerBeanDefinition("mapper",beanDefinition);
//}
}
}
再次运行主类,结果打印如下:
执行sql:SELECT * FROM t_userinfo WHERE id = 1
至此,两个模拟实战就完成了,如有任何问题都欢迎留言指出。
《走进Spring框架系列—初识IOC》
《走进Spring框架系列—IOC应用》
《走进Spring框架系列—IOC应用进阶篇》
《走进Spring框架系列—IOC应用进阶实战》
本系列文章参考-------Spring官网以及Spring框架源码