在上一节中, 我介绍了 Spring 中极为重要的 BeanPostProcessor BeanFactoryPostProcessor Import ImportSelector, 还介绍了一些其他的零碎知识点, 正如我上一节所说的, Spring 实在是太庞大了, 是众多 Java 开发大神的结晶, 很多功能, 很多细节, 可能一辈子都不会用到, 不会发现, 作为普通开发的我们, 只能尽力去学习, 去挖掘, 也许哪天可以用到呢.
让我们进入正题吧.
Full Lite
在上一节中的第一块内容, 我们知道了 Spring 中除了可以注册我们最常用的配置类, 还可以注册一个普通的 Bean, 今天我就来做一个补充说明.
如果你接到一个需求, 要求写一个配置类, 完成扫描, 你会怎么写?
作为经常使用 Spring 的来说, 这是一个入门级别的问题, 并且在 20 秒钟之内就可以完成编码:
- @Configuration
- @ComponentScan
- public class AppConfig {
- }
- public class Main {
- public static void main(String[] args) {
- AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
- context.getBean(ServiceImpl.class).query();
- }
- }
- @Component
- public class ServiceImpl{
- public void query() {
- System.out.println("正在查询中");
- }
- }
运行:
但是你有没有尝试过把 AppConfig 类上的 @Configuration 注解给去除? 你在心里肯定会犯嘀咕, 这不能去除啊, 这个 @Configuration 注解申明了咱们的 AppConfig 是一个 Spring 配置类, 去除了 @Configuration 注解, 怎么可能可以呢? 但是事实胜于雄辩, 当我们把 @Configuration 注解给删除, 再次运行, 你会见证到奇迹:
- @ComponentScan
- public class AppConfig {
- }
- public class Main {
- public static void main(String[] args) {
- AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
- context.getBean(ServiceImpl.class).query();
- }
- }
一点问题都没有!!! 是不是到这里已经颠覆了你对 Spring 的认知.
其实, 在 Spring 内部, 把带上了 @Configuration 的配置类称之为 Full 配置类, 把没有带上 @Configuration, 但是带上了 @Component @ComponentScan @Import @ImportResource 等注解的配置类称之为 Lite 配置类.
原谅我, 实在找不到合适的中文翻译来表述这里的 Full 和 Lite.
也许你会觉得这并没什么用, 只是 "茴的四种写法" 而已.
别急, 让我们看下去, 将会继续刷新你的三观:
- @ComponentScan
- public class AppConfig {
- }
注意现在的 AppConfig 类上没有加上 @Configuration 注解.
- public class Main {
- public static void main(String[] args) {
- AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
- System.out.println(context.getBean(AppConfig.class).getClass().getSimpleName());
- }
- }
我们注册了 Lite 配置类, 并且从 Spring 容器中取出了 Lite 配置类, 打印出它的类名.
运行:
可以看到从容器取出来的就是 AppConfig 类, 各位看官肯定会想, 这不是废话吗, 难道从容器取出来会变成了一只老母鸡?
别急嘛, 让我们继续.
我们再在 AppConfig 类加上 @Configuration 注解, 使其变成 Full 配置类, 然后还是一样, 注册这个配置类, 取出这个配置类, 打印类名:
你会惊讶的发现, 的确从容器里取出了一个老母鸡, 哦, 不, 是一个奇怪的类, 从类名我们可以看到 CGLIB 这个关键字, CGLIB 是动态代理的一种实现方式, 也就是说我们的 Full 配置类被 CGLIB 代理了.
你是不是从来都没有注意过, 竟然会有如此奇怪的设定, 但是更让人惊讶的事情还在后头, 让我们想想, 为什么好端端的类, Spring 要用 Cglib 代理? 这又不是 AOP.Spring 内部肯定做了一些什么! 没错, 确实做了!!!
下面让我们看看 Spring 到底做了什么:
- public class ServiceImpl {
- public ServiceImpl() {
- System.out.println("ServiceImpl 类的构造方法");
- }
- }
ServiceImpl 类中有一个构造方法, 打印了一句话.
- public class OtherImpl {
- }
再定义一个 OtherImpl 类, 里面什么都没有.
- public class AppConfig {
- @Bean
- public ServiceImpl getServiceImpl() {
- return new ServiceImpl();
- }
- @Bean
- public OtherImpl getOtherImpl() {
- getServiceImpl();
- return new OtherImpl();
- }
- }
这个 AppConfig 没有加上 @Configuration 注解, 是一个 Lite 配置类, 里面定义了两个 @Bean 方法, 其中 getServiceImpl 方法创建并且返回了 ServiceImpl 类的对象, getOtherImpl 方法再次调用了 getServiceImpl 方法.
然后我们注册这个配置类:
- public class Main {
- public static void main(String[] args) {
- AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
- }
- }
运行:
发现打印了两次 "ServiceImpl 类的构造方法", 这也很好理解, 因为 new 了两次 ServiceImpl 嘛, 肯定会执行两次 ServiceImpl 构造方法呀.
我们在把 @Configuration 注解给加上, 让 AppConfig 称为一个 Full 配置类, 再次运行:
你会惊讶的发现只打印了一次 "ServiceImpl 类的构造方法", 说明只调用了一次 ServiceImpl 类的构造方法, 其实这也说的通啊, 因为 Bean 默认是 Singleton 的, 所以只会创建一次对象嘛.
但是问题来了, 为什么我们明明 new 了两次 ServiceImpl 类, 但是真正只 new 了一次? 结合上面的内容, 很容易知道答案, 因为 Full 配置类被 Cglib 代理了, 它已经不是我们原先定义的 AppConfig 类了, 它里面的方法已经被改写了.
好了, 这个问题就讨论到这里, 至于为什么说 (如何证明) 带上 @Configuration 注解的配置类称之为 Full 配置类, 不带的称之为 Lite 配置类, Cglib 是怎么代理 Full 配置类的, 重写的规则又是什么, 这就涉及到 Spring 的源码解析了, 就不在今天的讨论内容之中了.
ImportBeanDefinitionRegistrar
大家一定使用过 Mybatis, 甚至使用过 Mybatis 的扩展, 我在使用的时候, 觉得太特么的神奇了, 只要在配置类上打一个 MapperScan 注解, 指定需要扫描哪些包. 然后这些包里面只有接口, 根本没有实现类, 为什么可以完成数据库的一系列操作, 不知道大家有没有和我一样的疑惑, 直到我知道了 ImportBeanDefinitionRegistrar 这个神奇的接口, 关于这个接口, 我不知道该怎么去描述这个接口的作用, 因为这个接口实在是太强大了, 实在不是用简单的文字可以描述清楚的. 下面我就利用这个接口来完成一个假的 MapperScan, 从中慢慢体验这个接口的强大, 对了, 这个接口要和 Import 注解配合使用.
首先需要定义一个注解:
- @Import(CodeBearMapperScannerRegistrar.class)
- @Retention(RetentionPolicy.RUNTIME)
- public @interface CodeBearMapperScanner {
- String value();
- }
其中 value 就是需要扫描的包名, 在这个注解类中又打了一个 Import 注解, 来引 ImportBeanDefinitionRegistrar 类.
再定义一个注解:
- @Retention(RetentionPolicy.RUNTIME)
- public @interface CodeBearSql {
- String value();
- }
这个注解是打在方法上的, 接收的是一个 sql 语句.
然后要定义一个类, 去实现 ImportBeanDefinitionRegistrar 接口, 重写提供的方法.
- public class CodeBearMapperScannerRegistrar implements ImportBeanDefinitionRegistrar, ResourceLoaderAware {
- private ResourceLoader resourceLoader;
- @Override
- public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
- try {
- AnnotationAttributes annoAttrs =
- AnnotationAttributes.fromMap(importingClassMetadata.getAnnotationAttributes(CodeBearMapperScanner.class.getName()));
- String packageValue = annoAttrs.getString("value");
- String pathValue = packageValue.replace(".", "/");
- File[] files = resourceLoader.getResource(pathValue).getFile().listFiles();
- for (File file : files) {
- String name = file.getName().replace(".class", "");
- Class<?> aClass = Class.forName(packageValue + "." + name);
- if (aClass.isInterface()&&!aClass.isAnnotation()) {
- BeanDefinitionBuilder beanDefinitionBuilder = BeanDefinitionBuilder.genericBeanDefinition();
- AbstractBeanDefinition beanDefinition = beanDefinitionBuilder.getBeanDefinition();
- beanDefinition.setBeanClass(CodeBeanFactoryBean.class);
- beanDefinition.getConstructorArgumentValues().addGenericArgumentValue(packageValue + "." + name);
- registry.registerBeanDefinition(name, beanDefinition);
- }
- }
- } catch (Exception ex) {
- }
- }
- @Override
- public void setResourceLoader(ResourceLoader resourceLoader) {
- this.resourceLoader = resourceLoader;
- }
- }
其中 ResourceLoaderAware 接口的作用不大, 我只是利用这个接口, 获得了 ResourceLoader , 然后通过 ResourceLoader 去获得包下面的类而已. 这方法的核心就是循环文件列表, 根据包名和文件名, 反射获得 Class, 接着判断 Class 是不是接口, 如果是接口的话, 动态注册 Bean. 如何动态去注册 Bean 呢? 我在这里利用的是 BeanDefinitionBuilder, 通过 BeanDefinitionBuilder 获得一个 BeanDefinition, 此时 BeanDefinition 是一个很纯净的 BeanDefinition, 经过一些处理, 再把最终的 BeanDefinition 注册到 Spring 容器.
关键就在于处理的这两行代码了, 这里可能还看不懂, 我们继续看下去.
我们需要再定义一个类, 去实现 FactoryBean,InvocationHandler 两个接口:
- public class CodeBeanFactoryBean implements FactoryBean, InvocationHandler {
- private Class clazz;
- public CodeBeanFactoryBean(Class clazz) {
- this.clazz = clazz;
- }
- @Override
- public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
- CodeBearSql annotation = method.getAnnotation(CodeBearSql.class);
- String sql= annotation.value();
- System.out.println(sql);
- return sql;
- }
- @Override
- public Object getObject() throws Exception {
- Object o = Proxy.newProxyInstance(this.getClass().getClassLoader(), new Class[]{clazz}, this);
- return o;
- }
- @Override
- public Class<?> getObjectType() {
- return clazz;
- }
- }
关于 FactoryBean 接口, 在上一节中有介绍, 这里就不再阐述了.
这个类有一个构造方法, 接收的是一个 Class, 这里接收的就是用来进行数据库操作的接口. getObject 方法中, 就利用传进来的接口和动态代理来创建一个代理对象, 此时这个代理对象就是 FactoryBean 生产的一个 Bean 了, 只要对 JDK 动态代理有一定了解的人都知道, 返回出来的代理对象实现了我们用来进行数据库操作的接口.
我们需要把这个 Bean 交给 Spring 去管理, 所以就有了 CodeBearMapperScannerRegistrar 中的这行代码:
beanDefinition.setBeanClass(CodeBeanFactoryBean.class);
因为创建 CodeBeanFactoryBean 对象需要一个 Class 参数. 所以就有了 CodeBearMapperScannerRegistrar 中的这行代码:
- //packageValue + "." +name 就是接口的全名称
- beanDefinition.getConstructorArgumentValues().addGenericArgumentValue(packageValue + "." + name);
invoke 方法比较简单, 就是获得 CodeBearSql 注解上的 sql 语句, 然后打印一下, 当然这里只是模拟下, 所以并没有去查询数据库.
下面让我们测试一下吧:
- public interface UserRepo {
- @CodeBearSql(value = "select * from user")
- void get();
- }
- @Configuration
- @CodeBearMapperScanner("com.codebear")
- @ComponentScan
- public class AppConfig {
- }
- @Service
- public class Test {
- @Autowired
- UserRepo userRepo;
- public void get(){
- userRepo.get();
- }
- }
运行结果:
可以看到我们的功能已经实现了. 其实 Mybatis 的 MapperScan 注解也是利用了 ImportBeanDefinitionRegistrar 接口去实现的.
可以看到第二块内容, 其实已经比较复杂了, 不光光有 ImportBeanDefinitionRegistrar, 还整合 FactoryBean, 还融入了动态代理. 如果我们不知道 FactoryBean, 可能这个需求就很难实现了. 所以每一块知识点都很重要.
这一节的内容到这里就结束了.
来源: https://www.cnblogs.com/CodeBear/p/10304605.html