Spring中你可能不知道的事(二)

在上一节中,我介绍了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

时间: 2024-08-30 07:27:34

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

jquery 常用方法中那些我不知道的事

1. width() 和 outerWidth() width():获取到元素内容的宽度,不包括padding值 outerWidth():获取元素content + padding + border的宽度 2. offset() 和 position() offset():获取相对于浏览器的偏移量 position():获取相对于父级元素(块级元素)的偏移量

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

今天凑巧去W3School扫了一遍JavaScript教程,发现从中看到了不少自己以前没有注意过的细节. 我这些细节列在这里,分享给可能同样不知道的朋友: 1.使用 document.write() 仅仅向文档输出写内容.如果在文档已完成加载后执行 document.write,整个 HTML 页面将被覆盖: 实例 <!DOCTYPE html> <html> <body> <h1>My First Web Page</h1> <p>

网站细节优化,你不得不知道的事

你怎么做优化的?这句话问到许多人的时分他们都是一个回答."文章,外链",其实,搜索引擎优化真的并不是如此.下面天津搜索引擎优化小编举个简略的比方,就好比咱们买了个新房子,文章即是你房子中的家私,外链即是你需求来观赏的兄弟.你的家私多,而且异乎寻常,也即是说你的文章多,自创,当然可以很招引你需求到来的兄弟们的眼球.可是,如果你的房子地没扫,各种规划不合理,很有可能会给别人形成肮脏的印象.所以,做搜索引擎优化并不是只要文章还有外链,还需要你进行仔细的润饰,也即是咱们做的网站细节优化.下面我

Android生命周期里你或许不知道的事

Android生命周期估计连初学者都再熟悉不过的东西了,但这里我抛出几个问题,或许大家以前没有想过或者可能认识的有些错误. 一.当A启动B时,A和B生命周期方法执行的先后顺序是怎样的?当按返回键返回时,又是怎样的?(读者可以先想想,可能会跟你的答案不一致) A--->B时,打印结果如下: 按返回键B--->A,打印结果如下: 结论:先执行当前显示Activity的onPause方法,接着执行完将要显示Activity的生命周期方法,最后再执行当前显示Activity的其它生命周期方法 二.生命

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

ES6 你可能不知道的事 – 基础篇 转载 作者:淘宝前端团队(FED)- 化辰 链接:taobaofed.org/blog/2016/07/22/es6-basics/ 序 ES6,或许应该叫 ES2015(2015 年 6 月正式发布),对于大多数前端同学都不陌生. 首先这篇文章不是工具书,不会去过多谈概念,而是想聊聊关于每个特性 你可能不知道的事,希望能为各位同学 正确使用 ES6,提供一些指导. 对于 ES6,有些同学已经在项目中有过深入使用了,有些则刚刚开始认识他,但不论你是属于哪一类

overflow:hidden 你所不知道的事

overflow:hidden 你所不知道的事 overflow:hidden这个CSS样式是大家常用到的CSS样式,但是大多数人对这个样式的理解仅仅局限于隐藏溢出,而对于清除浮动这个含义不是很了解. 一提到清除浮动,我们就会想到另外一个CSS样式:clear:both,我相信对于这个属性的理解大家都不成问题的.但是对于“浮动”这个词到底包含什么样的含义呢?我们下面来详细的阐述一下. <div id="wai"> <div id="nei">

Spring中的@Transactional深度分析之二

5.   @Transactional之isolation 隔离级别所要解决的问题是在应用程序中,存在多个事务同时在运行之时,需要解决和处理好的问题.那首先来看看,一般会出现哪些问题呢?先来看看吧. 脏读(dirty read) 一个事物更新了数据库中的某些数据,另一个事物读取了这些数据,这时前一个事物由于某些原因回滚了,那么第二个事物读取的数据就是"脏数据" 不可重复读(non-repeatable read) 一个事物需要两次查询同一数据,但两次查询中间可能有另外一个事物更改了这个

Spring中基于Java的容器配置(二)

使用@Configuration注解 @Configuration注解是一个类级别的注解,表明该对象是用来指定Bean的定义的.@Configuration注解的类通过@Bean注解的方法来声明Bean.通过调用注解了@Bean方法的返回的Bean可以用来构建Bean之间的相互依赖关系,可以通过前文来了解其基本概念. 注入inter-bean依赖 当@Bean方法依赖于其他的Bean的时候,可以通过在另一个方法中调用即可. @Configuration public class AppConfi

Spring中你不得不知的各种Utils

在Java开发过程中,我们会用到很多工具类来为我们项目开发做工作,同样,Spring板块中,也有很多工具类,他们有些是专门提供给了框架使用,有些也是提供了外接使用方法.使用适当的工具,让项目事半功倍.前言 Spring的工具类都是以Utils结尾,所以要查看这些工具类,只需要在API文档中查询所有*Utils即可,可以看到有多达几十个.其中有我们非常熟悉的org.springframework.util.StringUtils,有用到过的org.springframework.aop.suppo