欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

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

程序员文章站 2022-12-17 08:18:27
在上一节中,我介绍了Spring中极为重要的BeanPostProcessor BeanFactoryPostProcessor Import ImportSelector,还介绍了一些其他的零碎知识点,正如我上一节所说的,Spring实在是太庞大了,是众多Java开发大神的结晶,很多功能,很多细节 ......

在上一节中,我介绍了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("正在查询中");
    }
}

运行:

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

但是你有没有尝试过把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的认知。

其实,在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配置类,打印出它的类名。

运行:

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

可以看到从容器取出来的就是appconfig类,各位看官肯定会想,这不是废话吗,难道从容器取出来会变成了一只老母鸡?

别急嘛,让我们继续。

我们再在appconfig类加上@configuration注解,使其变成full配置类,然后还是一样,注册这个配置类,取出这个配置类,打印类名:

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

你会惊讶的发现,的确从容器里取出了一个老母鸡,哦,不,是一个奇怪的类,从类名我们可以看到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);
    }
}

运行:

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

发现打印了两次"serviceimpl类的构造方法",这也很好理解,因为new了两次serviceimpl嘛,肯定会执行两次serviceimpl构造方法呀。

我们在把@configuration注解给加上,让appconfig称为一个full配置类,再次运行:

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

你会惊讶的发现只打印了一次"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();
    }
}

运行结果:
Spring中你可能不知道的事(二)
可以看到我们的功能已经实现了。其实mybatis的mapperscan注解也是利用了importbeandefinitionregistrar接口去实现的。

可以看到第二块内容,其实已经比较复杂了,不光光有importbeandefinitionregistrar,还整合factorybean,还融入了动态代理。如果我们不知道factorybean,可能这个需求就很难实现了。所以每一块知识点都很重要。

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