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

CoderBear發表於2019-02-13

在上一節中,我介紹了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("正在查詢中");
    }
}
複製程式碼

執行:

image.png

但是你有沒有嘗試過把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();
    }
}
複製程式碼

image.png

一點問題都沒有!!!是不是到這裡已經顛覆了你對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配置類,列印出它的類名。

執行:

image.png

可以看到從容器取出來的就是AppConfig類,各位看官肯定會想,這不是廢話嗎,難道從容器取出來會變成了一隻老母雞?

別急嘛,讓我們繼續。

我們再在AppConfig類加上@Configuration註解,使其變成Full配置類,然後還是一樣,註冊這個配置類,取出這個配置類,列印類名:

image.png

你會驚訝的發現,的確從容器裡取出了一個老母雞,哦,不,是一個奇怪的類,從類名我們可以看到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);
    }
}
複製程式碼

執行:

image.png

發現列印了兩次"ServiceImpl類的構造方法",這也很好理解,因為new了兩次ServiceImpl嘛,肯定會執行兩次ServiceImpl構造方法呀。

我們在把@Configuration註解給加上,讓AppConfig稱為一個Full配置類,再次執行:

image.png

你會驚訝的發現只列印了一次"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();
    }
}
複製程式碼

執行結果:

image.png
可以看到我們的功能已經實現了。其實Mybatis的MapperScan註解也是利用了ImportBeanDefinitionRegistrar介面去實現的。

可以看到第二塊內容,其實已經比較複雜了,不光光有ImportBeanDefinitionRegistrar,還整合FactoryBean,還融入了動態代理。如果我們不知道FactoryBean,可能這個需求就很難實現了。所以每一塊知識點都很重要。

這一節的內容到這裡就結束了。

相關文章