1. 程式人生 > >例子講解Springboot註解@EnableXXX如何工作

例子講解Springboot註解@EnableXXX如何工作

概述

Springboot 提供了很多@EnableXXX這樣的註解,通過使用這些註解,我們能夠很方便地啟用某些功能。那麼這些@EnableXXX註解又是怎麼工作的呢?簡單來講,就是它主要利用了另外一個註解@Import,而關於@Import如何使用以及背後的工作原理,可以參考我的另外一篇文章 :Spring @Import 的使用及其工作原理分析。本文不再重點講解@EnableXXX,@Import的工作原理,而是通過一個例子專案講解註解@EnableXXX如何定義及其工作效果。

自定義@EnableXXX註解的例子專案

演示目的

這個例子專案中,我們通過普通Java

類方式定義了一組服務元件,也就是說沒有使用Spring標準的bean定義方式將它們定義為可被Spring框架直接識別的bean;然後我們自定義了一個@EnableXXX註解,通過使用這個自定義的註解,我們能夠把這些通過普通Java類方式定義的元件註冊為Spring容器bean,從而使他們變得跟其他Spring標準方式定義的bean一樣被使用 。

例子專案的原始碼

下面,我們來看一下這個專案的原始碼。這是一個基於maven的專案。

專案檔案結構

/
│    
│  pom.xml
│
└─src
    └─main
        └─java
            ├─app
            │      Application.java
            │
            ├─config
            │      EnableMyOwnBeanDefinitions.java
            │      LogServiceImportSelector.java
            │      OrderServiceBeanDefinitionRegistrar.java
            │      UserBeansConfig.java
            │
            └─services
                    AdminService.java
                    CustomerService.java
                    LogService.java
                    OrderChangeRecordService.java
                    OrderService.java
                    SettingService.java

pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion
>
<groupId>app</groupId> <artifactId>zero</artifactId> <version>1.0-SNAPSHOT</version> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter</artifactId> <version>2.1.0.RELEASE</version> </dependency> </dependencies> </project>

模擬的服務元件類

在這個例子專案中,我們模擬了以下服務元件類,因為只是出於演示的目的,所以每個服務類元件並沒有提供任何具體的實現。通過類名稱和類上的註解,你應該能理解每個服務元件類想表達的意義。這裡需要指出的是,這些服務元件類都是普通Java類,沒有使用任何Spring標準bean定義方式將它們定義為bean,比如使用XML bean定義方式,或者使用類似@Service,@Component,Bean這樣註解定義的方式。

你可能會問,既然我們能通過@Service,@Component,Bean這樣標準的Spring bean定義方式定義bean,那麼這裡又何苦使用普通Java類方式定義服務元件類呢 ?
這裡有兩個目的。
一,本例子中這樣定義的目的是為了演示一個@Enable註解如何工作;
二,想象一下一個通過其他渠道獲取的java服務包,你無法在原始碼層面對其服務元件類進行修改,但你又想把它們引入Springboot應用體系中,那該怎麼辦呢?等你讀完了本文,再想一下,這裡是不是就提供了一種不錯的解決辦法?

package services;

/**
 * 管理員管理服務:管理員賬號增刪改查等
 */
public class AdminService {
}

package services;

/**
 * 客戶管理服務:客戶賬號,基礎資訊增刪改查等
 */
public class CustomerService {
}
package services;

/**
 * 日誌服務
 */
public class LogService {
}

package services;

/**
 * 訂單變更跟蹤記錄,僅用在 開發模式下
 */
public class OrderChangeRecordService {
}

package services;

/**
 * 訂單服務:訂單的增刪改查等
 */
public class OrderService {
}

package services;

/**
 * 配置服務:系統配置引數的增刪改查等
 */
public class SettingService {
}

自定義@EnableMyOwnBeanDefinitions

準備一個@Configuration配置類

package config;

import services.AdminService;
import services.CustomerService;
import org.springframework.context.annotation.Bean;

/**
 * 一個 Spring 配置類, 註解方式註冊bean的典型用法 :
 *
 * @Configuration 定義配置類 + @Bean 註解配置類方法註冊bean
 */
//@Configuration
public class UserBeansConfig {
    /**
     * 註冊管理員管理服務元件bean
     * AdminService 是一個普通Java類
     *
     * @return
     */
    @Bean
    public AdminService adminService() {
        return new AdminService();
    }

    /**
     * 註冊客戶管理服務元件bean
     * CustomerService 是一個普通Java類
     *
     * @return
     */
    @Bean
    public CustomerService customerService() {
        return new CustomerService();
    }
}

準備一個ImportSelector

package config;

import services.LogService;
import org.springframework.context.annotation.ImportSelector;
import org.springframework.core.type.AnnotationMetadata;

/**
 * 一個 ImportSelector 實現類
 * 用於演示通過 ImportSelector ,給定一組要註冊為bean元件的普通Java類的名稱,
 * 然後通過利用 @Import(LogServiceImportSelector.class)即可註冊相應的bean
 */
public class LogServiceImportSelector implements ImportSelector {

    public String[] selectImports(AnnotationMetadata importingClassMetadata) {
        // 返回要註冊成為 bean 的類的全名稱的陣列
        return new String[]{LogService.class.getName()};
    }
}

準備一個ImportBeanDefinitionRegistrar

package config;

import services.OrderChangeRecordService;
import services.OrderService;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.beans.factory.support.RootBeanDefinition;
import org.springframework.context.annotation.ImportBeanDefinitionRegistrar;
import org.springframework.core.annotation.AnnotationAttributes;
import org.springframework.core.type.AnnotationMetadata;
import org.springframework.util.StringUtils;

/**
 * 一個介面ImportBeanDefinitionRegistrar的實現類,
 * 用於演示 , 演示點:
 * 1. 程式化將普通Java類作為bean註冊到Spring IoC容器;
 * 2. 使用註解元資料屬性動態決定bean的註冊;
 */
public class OrderServiceBeanDefinitionRegistrar implements ImportBeanDefinitionRegistrar {
    /**
     * @param importingClassMetadata 註解元資料
     * @param registry               Spring IoC 容器
     */
    public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, 
    BeanDefinitionRegistry registry) {
        AnnotationAttributes annotationAttributes = AnnotationAttributes.fromMap(
        	importingClassMetadata.getAnnotationAttributes(
        							EnableMyOwnBeanDefinitions.class.getName()));

        {// 註冊型別為 OrderService 的訂單服務元件 bean
            Class beanClass = OrderService.class;
            RootBeanDefinition beanDefinition = new RootBeanDefinition(beanClass);
            String beanName = StringUtils.uncapitalize(beanClass.getSimpleName());
            registry.registerBeanDefinition(beanName, beanDefinition);
        }

        {// 看情況決定是否註冊型別為 OrderChangeRecordService 的訂單變更記錄服務元件 bean
            boolean trackOrderChange = annotationAttributes.getBoolean("trackOrderChange");
            if (trackOrderChange) {
                Class beanClass = OrderChangeRecordService.class;
                RootBeanDefinition beanDefinition = new RootBeanDefinition(beanClass);
                String beanName = StringUtils.uncapitalize(beanClass.getSimpleName());
                registry.registerBeanDefinition(beanName, beanDefinition);
            }
        }
    }
}

定義註解 @EnableMyOwnBeanDefinitions

現在基於上面定義的配置類UserBeansConfig(@Configuration配置類),LogServiceImportSelector(ImportSelector),OrderServiceBeanDefinitionRegistrar(ImportBeanDefinitionRegistrar),我們自定義一個註解@EnableMyOwnBeanDefinitions

這個註解同時定義了一個屬性trackOrderChange,用來表明是否要開啟訂單變動跟蹤,這個語義在上面的OrderServiceBeanDefinitionRegistrar中被具體實現:如果trackOrderChangefalse,服務元件OrderChangeRecordService並不會被註冊到Spring容器。

package config;

import services.SettingService;
import org.springframework.context.annotation.Import;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * 模仿 Spring 框架的 @EnableXXX 註解自定義的一個 @Enable 註解,
 * 此類註解一般使用 @Import 通過以下四種方式進行 bean 定義:
 * 1. @Configuration 註解的專門用於bean定義的類,一般通過@Bean註解的方法註冊bean ;
 * 2. ImportSelector 給出某些要註冊為bean的普通類的類名,將它們註冊為 bean ;
 * 3. ImportBeanDefinitionRegistrar 直接基於某些普通類建立 BeanDefinition 並註冊相應的 bean ,可以有較複雜的邏輯;
 * 4. 直接將某個普通類作為一個bean註冊到容器。
 */
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Import({
		UserBeansConfig.class, 
		LogServiceImportSelector.class, 
		OrderServiceBeanDefinitionRegistrar.class, 
		SettingService.class})
public @interface EnableMyOwnBeanDefinitions {
    /**
     * 註解屬性:是否記錄訂單變更,語義 :
     * false -- 不註冊訂單變更記錄服務元件bean
     * true -- 註冊訂單變更記錄服務元件bean
     *
     * @return
     */
    boolean trackOrderChange() default false;
}

使用自定義註解@EnableMyOwnBeanDefinitions

package app;

import config.EnableMyOwnBeanDefinitions;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ConfigurableApplicationContext;

/**
 * 演示自定義註解@EnableMyOwnBeanDefinitions的使用和效果
 */

@EnableMyOwnBeanDefinitions(trackOrderChange = true)
//@EnableMyOwnBeanDefinitions(trackOrderChange = false)
@SpringBootApplication
public class Application {


    public static void main(String[] args) {
        ConfigurableApplicationContext applicationContext = SpringApplication.run(Application.class, args);
        dumpBeansToConsole(applicationContext);
    }


    /**
     * 往控制檯上輸出容器中註冊的各個bean的名稱
     *
     * @param applicationContext
     */
    private static void dumpBeansToConsole(ConfigurableApplicationContext applicationContext) {
        String[] definitionNames = applicationContext.getBeanDefinitionNames();
        for (String name : definitionNames) {
            Object bean = applicationContext.getBean(name);
            System.out.printf("%s[%s]\n", name, bean.getClass().getName());
        }
    }
}

現在,當我們執行上面的應用時,控制檯上會輸出我們通過Java普通類所定義的服務元件bean,如下所示:

config.UserBeansConfig[config.UserBeansConfig]
adminService[services.AdminService]
customerService[services.CustomerService]
services.LogService[services.LogService]
services.SettingService[services.SettingService]
orderService[services.OrderService]
orderChangeRecordService[services.OrderChangeRecordService]

把註解@EnableMyOwnBeanDefinitions的屬性trackOrderChange設定為false,再執行上面的應用,控制檯上的輸出會變成這個樣子:

config.UserBeansConfig[config.UserBeansConfig]
adminService[services.AdminService]
customerService[services.CustomerService]
services.LogService[services.LogService]
services.SettingService[services.SettingService]
orderService[services.OrderService]

跟上面屬性trackOrderChange設定為true時的輸出相比,可以看到orderChangeRecordService[services.OrderChangeRecordService]不見了,這說明開關屬性trackOrderChange 在起到相應的作用。

總結

本文通過一個實際的例子專案演示瞭如何定義一個@EnableXXX註解並應用在Springboot專案中。通過這個例子,你可以看到@EnableXXX是怎樣被定義的以及起到了什麼樣的作用。關於@EnableXXX背後的工作原理,基本上也就是註解@Import的工作原理,你可以參考我的另外一篇文章 :Spring @Import 的使用及其工作原理分析。另外,通過這個例子,你也可以針對某些第三方提供的java服務包提供一個自定義的@EnableXXX註解將這些服務註冊到Spring的應用環境中來。