在上一节中,我介绍了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,可能这个需求就很难实现了。所以每一块知识点都很重要。

这一节的内容到这里就结束了。

Spring中你可能不知道的事(二)的更多相关文章

  1. Spring中你可能不知道的事(一)

    Spring作为Java的王牌开源项目,相信大家都用过,但是可能大家仅仅用到了Spring最常用的功能,Spring实在是庞大了,很多功能可能一辈子都不会用到,今天我就罗列下Spring中你可能不知道 ...

  2. 关于Spring Boot你不得不知道的事--Spring Boot的基本操作

    1 Pom文件 1.1 spring-boot-starter-parent 表示当前pom文件从spring-boot-starter-parent继承下来,在spring-boot-starter ...

  3. 关于Spring Boot你不得不知道的事

    1 Spring Boot官网[2.1.5 CURRENT GA] 1.1 Pivotal Wiki Pivotal Software, Inc. is a software and services ...

  4. Java你可能不知道的事(3)HashMap

    概述 HashMap对于做Java的小伙伴来说太熟悉了.估计你们每天都在使用它.它为什么叫做HashMap?它的内部是怎么实现的呢?为什么我们使用的时候很多情况都是用String作为它的key呢?带着 ...

  5. java你可能不知道的事(2)--堆和栈

    在java语言的学习和使用当中你可能已经了解或者知道堆和栈,但是你可能没有完全的理解它们.今天我们就一起来学习堆.栈的特点以及它们的区别.认识了这个之后,你可能对java有更深的理解. Java堆内存 ...

  6. ES6 你可能不知道的事 – 基础篇

    序 ES6,或许应该叫 ES2015(2015 年 6 月正式发布),对于大多数前端同学都不陌生. 首先这篇文章不是工具书,不会去过多谈概念,而是想聊聊关于每个特性 你可能不知道的事,希望能为各位同学 ...

  7. overflow:hidden 你所不知道的事

    overflow:hidden 你所不知道的事 overflow:hidden这个CSS样式是大家常用到的CSS样式,但是大多数人对这个样式的理解仅仅局限于隐藏溢出,而对于清除浮动这个含义不是很了解. ...

  8. java你可能不知道的事(2)--堆和栈<转>

    在java语言的学习和使用当中你可能已经了解或者知道堆和栈,但是你可能没有完全的理解它们.今天我们就一起来学习堆.栈的特点以及它们的区别.认识了这个之后,你可能对java有更深的理解. Java堆内存 ...

  9. JavaScript中你可能不知道的九件事

    今天凑巧去W3School扫了一遍JavaScript教程,发现从中看到了不少自己曾经没有注意过的细节. 我这些细节列在这里.分享给可能相同不知道的朋友: 1.使用 document.write() ...

随机推荐

  1. 利用js实现 禁用浏览器后退

    现在很多的内部系统,一些界面,都是用户手动点击退出按钮的.但是为了避免,用户误操作 点击浏览器后退,或者用鼠标手势后退什么的.容易出现误操作.        所以在有些页面上,适当的禁用浏览器的后退, ...

  2. selenium爬取煎蛋网

    selenium爬取煎蛋网 直接上代码 from selenium import webdriver from selenium.webdriver.support.ui import WebDriv ...

  3. 如何在Ubuntu 18.04中安装VMware Workstation Player

    参考链接 如何在Ubuntu 18.04中安装VMware Workstation Player https://www.sysgeek.cn/ubuntu-18-04-install-vmware- ...

  4. 20165319 Exp6 信息收集与漏洞扫描

    实验过程 whois 在虚拟机Kali的终端输入命令:whois gitee.com,查询码云的域名注册信息. dig或nslookup域名查询 在kali终端输入dig 网址或nslookup 网址 ...

  5. Big big world

    Big big world I'm a big big girl 我是个大女孩 in a big big world 在这个大大的世界上 It's not a big big thing if you ...

  6. CentOS7终端的分辨率和字体修改

    本文转贴自https://blog.csdn.net/u010566813/article/details/40502819 一.修改分辨率 修改/boot/grub2/grub.cfg 添加如上,具 ...

  7. MyBatis(四)多参数处理问题

    这里总结了关于多参数传递时,MyBatis接收的三种方式. (1)接口中编写方法 public Emp getEmpByParams(Integer id,String lastNmae); publ ...

  8. Kubernetes1.7—DNS安装

    虽然通过了Service解决了Pod重建后IP动态变化(服务发现).负载均衡问题,但使用Service还是要需要知道CLUSTER-IP,而通过NDS可以解决该问题:在Kubernetes集群中可通过 ...

  9. 重写Object类里equals方法

    package com.fff; public class Pet { private String name; private int age; public Pet(String nume,int ...

  10. Linux yun命令使用报错:File "/usr/bin/yum", line 30 except KeyboardInterrupt, e:

    原文参考:https://www.cnblogs.com/caiji/p/7891923.html 使用yum更新perl源,报错 问题出现原因: yum包管理是使用python2.x写的,将pyth ...