Spring基礎

周仙僧發表於2024-03-21

IOC控制反轉

如A類中依賴了B類,傳統生成例項的過程是需要先例項化B,在例項化A時傳入B;控制反轉實現了先例項化A,掃描到需要使用B時再例項化B
實現了例項化過程的解耦,A、B可以單獨例項化,再實現依賴關係

Spring容器管理時的迴圈依賴問題

三級快取解決迴圈依賴
三級快取實際對應的是三次例項化過程,如A與B產生迴圈依賴
第一次:例項化A,放入一級快取
第二次:例項化A是發現依賴了B,會將A的例項化放到二級快取,然後開始B的例項化
第三次:例項化B時發現依賴了A,此時A已存在在二級快取中,會將A放入三級快取-B的例項化工廠,例項化工廠會將A注入到B,最後Spring容器再將B注入到A中。

Spring Bean的作用域

  • singleton : IoC 容器中只有唯一的 bean 例項。Spring 中的 bean 預設都是單例的,是對單例設計模式的應用。
  • prototype : 每次獲取都會建立一個新的 bean 例項。也就是說,連續 getBean() 兩次,得到的是不同的 Bean 例項。
    可作為DTO使用,使用方式:
    @Component
    @Scope("prototype")
    public class MyBean {
    	// ...
    }
    
  • request (僅 Web 應用可用): 每一次 HTTP 請求都會產生一個新的 bean(請求 bean),該 bean 僅在當前 HTTP request 內有效。
  • session (僅 Web 應用可用) : 每一次來自新 session 的 HTTP 請求都會產生一個新的 bean(會話 bean),該 bean 僅在當前 HTTP session 內有效。
  • application/global-session (僅 Web 應用可用):每個 Web 應用在啟動時建立一個 Bean(應用 Bean),該 bean 僅在當前應用啟動時間內有效。
  • websocket (僅 Web 應用可用):每一次 WebSocket 會話產生一個新的 bean。

Spring Bean的執行緒安全問題

Bean的線層安全取決於作用域和資料狀態;理論上論,大部分業務程式碼中都是使用的單例作用域,所以非執行緒安全,但是我們定義的Bean一般都無資料狀態,比如service、mapper,只做資料處理和傳遞,也不存線上程安全問題。

Spring Bean的生命週期

  1. 載入
    • Bean 容器找到配置檔案中 Spring Bean 的定義。
    • Bean 容器利用 Java Reflection API 建立一個 Bean 的例項。
  2. 初始化(包含Bean自身資料狀態和物件頭部資訊)
    • 如果涉及到一些屬性值 利用 set()方法設定一些屬性值。
    • 如果 Bean 實現了 BeanNameAware 介面,呼叫 setBeanName()方法,傳入 Bean 的名字。
    • 如果 Bean 實現了 BeanClassLoaderAware 介面,呼叫 setBeanClassLoader()方法,傳入 ClassLoader物件的例項。
    • 如果 Bean 實現了 BeanFactoryAware 介面,呼叫 setBeanFactory()方法,傳入 BeanFactory物件的例項。
    • 與上面的類似,如果實現了其他 *.Aware介面,就呼叫相應的方法。
  3. 回撥操作
    • 如果有和載入這個 Bean 的 Spring 容器相關的 BeanPostProcessor 物件,執行postProcessBeforeInitialization() 方法。
    • 如果 Bean 實現了InitializingBean介面,執行afterPropertiesSet()方法。
    • 如果 Bean 在配置檔案中的定義包含 init-method 屬性,執行指定的方法。
    • 如果有和載入這個 Bean 的 Spring 容器相關的 BeanPostProcessor 物件,執行postProcessAfterInitialization() 方法
  4. 銷燬
    • 當要銷燬 Bean 的時候,如果 Bean 實現了 DisposableBean 介面,執行 destroy() 方法。
    • 當要銷燬 Bean 的時候,如果 Bean 在配置檔案中的定義包含 destroy-method 屬性,執行指定的方法。

@Component 和 @Bean 的區別

@Bean用於配置檔案中,作用於方法,透過自定義的方式生成例項託管到Spring容器中。
@Component通常是透過類路徑掃描來自動偵測以及自動裝配到 Spring 容器中,利用的Spring的自動裝配機制來載入。

Spring中的事務管理

事務管理方式

  1. 自定義事務管理器
    可在Mybatise的配置檔案中制定自定義事務管理器,核心要點是解決事務開啟、提交、回滾過程
@Bean
    public DataSourceTransactionManager dataSourceTransactionManager() {
        DataSourceTransactionManager dataSourceTransactionManager = new DataSourceTransactionManager();
        dataSourceTransactionManager.setDataSource(dataSource());
        return dataSourceTransactionManager;
    }
  1. 業務程式碼中透過使用@Transactional註解開啟事務

事務的傳播行為

常用的事務傳播行為:

  1. TransactionDefinition.PROPAGATION_REQUIRED
    使用的最多的一個事務傳播行為,我們平時經常使用的@Transactional註解預設使用就是這個事務傳播行為。如果當前存在事務,則加入該事務;如果當前沒有事務,則建立一個新的事務。
  2. TransactionDefinition.PROPAGATION_REQUIRES_NEW
    建立一個新的事務,如果當前存在事務,則把當前事務掛起。也就是說不管外部方法是否開啟事務,Propagation.REQUIRES_NEW修飾的內部方法會新開啟自己的事務,且開啟的事務相互獨立,互不干擾。
  3. TransactionDefinition.PROPAGATION_NESTED
    如果當前存在事務,則建立一個事務作為當前事務的巢狀事務來執行;如果當前沒有事務,則該取值等價於TransactionDefinition.PROPAGATION_REQUIRED。類似於分散式事務,將一個大事務拆分成多個小事務,任一小事務回滾會導致大事務回滾。事務的拆分可以提高執行效率。
  4. TransactionDefinition.PROPAGATION_MANDATORY如果當前存在事務,則加入該事務;如果當前沒有事務,則丟擲異常。(mandatory:強制性)

事務失效的場景

  1. 開啟事務錯誤
    沒使用@Transactional註解或錯誤使用了Propagation.NOT_SUPPORTED傳播行為
  2. 呼叫方式錯誤
    沒用透過Spring容器管理的Bean物件呼叫事務方法,導致AOP失效
  3. 業務程式碼異常沒被捕獲
    業務程式碼自己處理的異常,但是沒有丟擲,導致事務沒法回滾
  4. 事務註解的方法不支援AOP
    final和static修飾的方法由於無法被AOP託管,導致事務失效
  5. 宣告的事務捕獲異常型別不匹配
    如指定事務捕獲的異常型別為@Transactional(rollbackFor = RuntimeException.class),但實際丟擲的是Exception.class異常
  6. 使用的資料庫不支援事務
    如Mysql的資料表使用了MyISAM儲存引擎

Spring Security

利用shrio管理使用者、角色、許可權

AOP-面向切面程式設計

生成切面,利用切面的切點動態程式碼業務程式碼中的方法,在方法執行時新增前置操作或後置操作;
傳統的動態代理需要先實現InvocationHandler去完成代理過程中需要執行的前置操作與後置操作,再透過InvocationHandler實現類的invoke方法完成代理過程,如下:

public class VectorProxy implements InvocationHandler {

    private Object subject = null;

    public VectorProxy(Object subject) {
        this.subject = subject;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        preRequset(method);
        if (args != null){
            for (int i = 0; i < args.length; i++) {
                System.out.println(args[i]);
            }
        }
        Object result = method.invoke(subject, args);
        afterRequset(method);
        return result;
    }

    private void preRequset(Method method){
        System.out.println("before calling "+method);
    }

    private void afterRequset(Method method){
        System.out.println("after calling "+method);
    }
}

AOP實現切面入駐時無法獲取到InvocationHandler實現類,因此無法採用傳統方式,而是使用了動態生成位元組碼(Cglib)的方式直接生成了被代理類的子類,透過子類完成代理過程,如下:

// 新增註解標記為切面
@Aspect
@Component
public class AspectLearn {

    Logger logger = LoggerFactory.getLogger(AspectLearn.class);

    // 指定切點,AOP採用Cglib根據切點生成子類
    @Pointcut("execution(* com.zwj.test.commonlearn.controller.UserInfoController.*(..))")
    private void controllerPointcut() {}

    // 切點環繞操作
    @Around("controllerPointcut()")
    private void controllerLog(ProceedingJoinPoint joinPoint) throws Throwable {
        logger.info("第一次執行around");
        String method = joinPoint.getSignature().getName();
        logger.info("呼叫{}請求引數:" + Arrays.asList(joinPoint.getArgs()), method);
        Object result = joinPoint.proceed();
        logger.info("呼叫{}返回結果:{}", result);
        logger.info("第二次執行around");
    }

    // 切點前置操作
    @Before("controllerPointcut()")
    private void controllerBefore(JoinPoint joinPoint) {
        logger.info("執行before");
    }

 // 切點後置操作
    @After("controllerPointcut()")
    private void controllerAfter(JoinPoint joinPoint) {
        logger.info("執行after");
    }
}

Spring boot

spring-boot啟動過程

由SpringApplication.run()執行開始,執行過程分兩個步驟,完成SpringApplication的初始建立,再執行run方法

public static ConfigurableApplicationContext run(Class<?>[] primarySources, String[] args) {
        return (new SpringApplication(primarySources)).run(args);
    }

SpringApplication建立

public SpringApplication(ResourceLoader resourceLoader, Class<?>... primarySources) {
        this.sources = new LinkedHashSet();
        this.bannerMode = Mode.CONSOLE;
        this.logStartupInfo = true;
        this.addCommandLineProperties = true;
        this.addConversionService = true;
        this.headless = true;
        this.registerShutdownHook = true;
        this.additionalProfiles = Collections.emptySet();
        this.isCustomEnvironment = false;
        this.lazyInitialization = false;
        this.applicationContextFactory = ApplicationContextFactory.DEFAULT;
        this.applicationStartup = ApplicationStartup.DEFAULT;
        this.resourceLoader = resourceLoader;
        Assert.notNull(primarySources, "PrimarySources must not be null");
        this.primarySources = new LinkedHashSet(Arrays.asList(primarySources));
        this.webApplicationType = WebApplicationType.deduceFromClasspath();
        this.bootstrapRegistryInitializers = new ArrayList(this.getSpringFactoriesInstances(BootstrapRegistryInitializer.class));
// ApplicationContext上下文構造器構建
        this.setInitializers(this.getSpringFactoriesInstances(ApplicationContextInitializer.class));
// 監聽器建立
        this.setListeners(this.getSpringFactoriesInstances(ApplicationListener.class));
        this.mainApplicationClass = this.deduceMainApplicationClass();
    }

run方法執行

public ConfigurableApplicationContext run(String... args) {
// 開始時鐘,記錄啟動過程總耗時
        long startTime = System.nanoTime();
        DefaultBootstrapContext bootstrapContext = this.createBootstrapContext();
        ConfigurableApplicationContext context = null;
// 載入java.awt.headless相關配置
        this.configureHeadlessProperty();
        SpringApplicationRunListeners listeners = this.getRunListeners(args);
// 觸發監聽器的starting動作
        listeners.starting(bootstrapContext, this.mainApplicationClass);

        try {
            ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
// 觸發監聽器的environmentPrepared動作,並完成環境變數的準備工作
            ConfigurableEnvironment environment = this.prepareEnvironment(listeners, bootstrapContext, applicationArguments);
            this.configureIgnoreBeanInfo(environment);
//  列印Banner
            Banner printedBanner = this.printBanner(environment);
            context = this.createApplicationContext();
            context.setApplicationStartup(this.applicationStartup);
// 觸發監聽器的contextPrepared動作,完成初始ApplicationContext上下文
            this.prepareContext(bootstrapContext, context, environment, listeners, applicationArguments, printedBanner);
// 重新整理上下文,將所有的Bean載入到上下文中
            this.refreshContext(context);
            this.afterRefresh(context, applicationArguments);
            Duration timeTakenToStartup = Duration.ofNanos(System.nanoTime() - startTime);
            if (this.logStartupInfo) {
                (new StartupInfoLogger(this.mainApplicationClass)).logStarted(this.getApplicationLog(), timeTakenToStartup);
            }
// 觸發監聽器的started動作
            listeners.started(context, timeTakenToStartup);
// 執行啟動命令列的回撥,透過回撥去執行啟動配置的命令引數,也可再該回撥中完成資料預熱的操作
            this.callRunners(context, applicationArguments);
        } catch (Throwable var12) {
            this.handleRunFailure(context, var12, listeners);
            throw new IllegalStateException(var12);
        }

        try {
            Duration timeTakenToReady = Duration.ofNanos(System.nanoTime() - startTime);
// 觸發監聽器的ready動作
            listeners.ready(context, timeTakenToReady);
            return context;
        } catch (Throwable var11) {
            this.handleRunFailure(context, var11, (SpringApplicationRunListeners)null);
            throw new IllegalStateException(var11);
        }
    }

歸納可得出如下核心步驟:

  1. 建立SpringApplication應用物件,該過程中完成了ApplicationContext上下文構造器的建立,同時完成了監聽器的建立
  2. 開始run方法的執行,開始時鐘,記錄啟動過程總耗時
  3. 完成環境變數的準備工作
  4. 完成建立初始ApplicationContext上下文
  5. 重新整理上下文,將所有的Bean載入到上下文中
  6. 執行啟動命令列的回撥,透過回撥去執行啟動配置的命令引數,也可再該回撥中完成資料預熱的操作
    image

監聽器

SpringBoot提供了監聽器機制,用於在SpringBoot的各宣告週期階段完成自定義操作,主要分類如下:

  • SpringApplicationRunListener:
    用於監聽SpringBoot的整個生命週期;實現SpringApplicationRunListener,並在spring.factories中配置後可生效
/**
 * SpringBoot啟動生命週期監聽器,可以監聽啟動過程的每一個階段
 */
public class MySpringApplicationRunListener implements SpringApplicationRunListener {


    private final SpringApplication application;
    private final String[] args;

    public MySpringApplicationRunListener(SpringApplication application, String[] args) {
        this.application = application;
        this.args = args;
    }

    @Override
    public void starting(ConfigurableBootstrapContext bootstrapContext) {
        System.out.println("【starting】專案開始啟動......");
    }

    @Override
    public void environmentPrepared(ConfigurableBootstrapContext bootstrapContext, ConfigurableEnvironment environment) {
        System.out.println("【environmentPrepared】專案環境配置資料開始準備......");

    }

    @Override
    public void contextPrepared(ConfigurableApplicationContext context) {
        System.out.println("【contextPrepared】專案執行上下文物件開始準備......");
    }

    @Override
    public void contextLoaded(ConfigurableApplicationContext context) {
        System.out.println("【contextLoaded】專案執行上下文物件開始載入......");
    }

    @Override
    public void started(ConfigurableApplicationContext context, Duration timeTaken) {
        System.out.println("【started】專案啟動成功.....耗時:" + timeTaken.getSeconds() + "秒");
    }

    @Override
    public void ready(ConfigurableApplicationContext context, Duration timeTaken) {
        System.out.println("【ready】專案正在執行,準備接收客戶端執行指令.....");
    }

    @Override
    public void failed(ConfigurableApplicationContext context, Throwable exception) {
        System.out.println("【failed】專案啟動失敗.....");
    }
}


//spring.factorise檔案配置
org.springframework.boot.SpringApplicationRunListener=\
  com.zwj.test.commonlearn.springBoot.life.MySpringApplicationRunListener
  • ApplicationContextInitializer:
    ApplicationContext上下文初始前執行,此時已經完成了環境變數的載入工作,因此可利用該監聽器完成環境變數的檢查與修改;ApplicationContextInitializer,並在spring.factories中配置後可生效。
/**
 * 當環境變數載入完成,開始初始化Application上下文時呼叫,可用用於檢查修改環境變數
 */
public class MyApplicationContextInitializer implements ApplicationContextInitializer<ConfigurableApplicationContext> {
    @Override
    public void initialize(ConfigurableApplicationContext configurableApplicationContext) {
        System.out.println("MyApplicationContextInitializer執行.......");
    }
}


//spring.factorise檔案配置
org.springframework.context.ApplicationContextInitializer=\
  com.zwj.test.commonlearn.springBoot.life.MyApplicationContextInitializer
  • CommandLineRunner:
    啟動命令列回撥執行器,SpringBoot啟動完成後執行,透過該監聽器可以執行啟動過程中配置的啟動引數,也可做快取預熱使用。實現CommandLineRunner介面,並透過@Component注入到Spring容器中可生效(此時SpringBoot已經完成了啟動過程,所以可使用註解注入)
/**
 * 啟動命令列引數回撥,當應用啟動完成後執行,引數為啟動時配置的命令
 * 可做快取預熱
 */
@Component
public class MyCommandLineRunner implements CommandLineRunner {
    @Override
    public void run(String... args) throws Exception {
        System.out.println("CommandLineRunner執行......  引數: " + Arrays.toString(args));
    }
}
  • ApplicationRunner:
    啟動命令列回撥執行器,SpringBoot啟動完成後執行,功能和用法與CommandLineRunner類似,區別在於ApplicationRunner提供的命令列引數是複雜型別,可以做更多的擴充套件功能。
/**
 * 啟動命令列引數回撥,當應用啟動完成後執行,引數為啟動時配置的命令
 * 效果與CommandLineRunner一致,區別在於ApplicationRunner中的引數時ApplicationArguments,可以擴充套件命令的執行方式
 * 可做快取預熱
 */
@Component
public class MyApplicationRunner implements ApplicationRunner {
    @Override
    public void run(ApplicationArguments args) throws Exception {
        System.out.println("ApplicationRunner正在執行......");
    }
}

Spring boot的自動裝配

自動裝配作用

Spring Boot應用專案在構建過程中會大量使用外部元件,如Mybatis、Redis等等,傳統的Spring方式需要我們自己在xml檔案中定義各種元件相關的Bean,透過IOC注入到Spring容器。Spring Boot的自動裝配提供了元件自動將自己相關的Bean注入到Spring容器的功能,不需要Spring Boot應用專案再單獨注入相關Bean。

自動裝配依賴技術

自動裝載的核心是@Import和@Conditional相關注解,並透過@Enable相關注解作為統一啟用註解

  • @Import註解
    用於將指定的類注入到Spring容器中,指定類的型別包含:單獨的Bean、配置類、依賴於DeferredImportSelector的Bean選擇器以及依賴於ImportBeanDefinitionRegistrar的Bean註冊器
    • 單獨的Bean:直接將制定的Bean例項化,注入到IOC中
    • 配置類:匯入的配置類裡面所有的Bean都會被注入到Spring容器
    • Bean選擇器:透過實現DeferredImportSelector介面的selectImports方法,透過自定義的方式提供要注入的Bean的全路徑陣列
    • Bean注入器:透過實現ImportBeanDefinitionRegistrar介面的registerBeanDefinitions方法,透過自定義的方式直接生成Bean定義,並註冊Bean定義
      @Import註解使用示例:
/**
 * 建立Enable註解,當spring-boot應用找那個需要使用此模組功能時,
 * 可新增改註解,注入當前starter模組的所有Bean
 */
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import({
        // 匯入配置類OtherStarterConfig,透過配置類批次注入Ban
        OtherStarterConfig.class,
        // 匯入OtherStarterImportSelector選擇器,透過選擇器自定義掃描Bean過程
        OtherStarterImportSelector.class,
        // 匯入OtherImportBeanDefinitionRegistrar註冊器,透過選擇器自定義掃描Bean過程
        OtherImportBeanDefinitionRegistrar.class})
// 直接匯入Bean
public @interface EnableOtherStarter {
}
  • @Conditional相關注解
    用於過濾不需要注入的Bean;針對於Spring Boot的自動裝配功能,如果外部元件的所有Bean都預設自動注入,會極大的影響系統的啟動速度,因此提供了@Conditional註解,給Bean定義新增一些注入條件,當符合條件時才注入相關元件的Bean;如Redis元件相關Bean的自動裝配前提是需要在application.yml中配置Redis資料來源。常用的@Conditional如下:
    • @ConditionalOnBean: 根據Spring容器是否已經注入了指定Bean為條件
    • @ConditionalOnProperty:根據配置檔案中的屬性配置為條件
    • @ConditionalOnClass:根據是否載入了指定了Class為條件
      如SpringBoot的web執行容器便是透過@ConditionalOnClass實現,預設狀態下spring-boot-starter-web依賴了tomcat容器相關jar,因此專案啟動時可以載入tomcat相關的class從而預設使用tomcat容器;若需要切換容易,只需要排除tomcat依賴,新增其它容器依賴即可,示例:
implementation('org.springframework.boot:spring-boot-starter-web') {
	exclude group: 'org.springframework.boot',module: 'spring-boot-starter-tomcat'
}
//    替換內建的web容器,遮蔽tomcat後引入jetty
implementation('org.springframework.boot:spring-boot-starter-jetty')
  • @Enable相關注解
    一般用作某個外部外掛的啟用註解,即僅當Spring Boot應用專案中使用了外部元件的@Enable註解,元件才會開始自動裝配;如EurekaServer服務註冊中心,近當新增了@EnableEurekaServer註解後才會開啟相關功能。
    其實現原理是@Enable註解透過@Import對元件相關的各種Bean注入渠道做了整合,當@Enable被使用時,所有被整合的注入渠道都會開始自動注入。
    針對上述示例中EnableOtherStarter註解的使用如下:
//該外部外掛@Enable註解依次匯入配置類、Bean選擇器、Bean註冊器
@EnableOtherStarter
// 直接匯入外部外掛配置類,將配置類裡面的Bean統一IOC到Spring容器
@Import(OtherExecuteStarterConfig.class)
@SpringBootApplication
public class CommonLearnApplication {

    public static void main(String[] args) {
        SpringApplication.run(CommonLearnApplication.class, args);
    }
}

自動裝配實現原理

總體來說Spring Boot的自動裝配使用了@Import(Bean選擇器)的方式。
透過原始碼可以看出Spring Boot的啟動類使用了@SpringBootApplication註解。

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(
    excludeFilters = {@Filter(
    type = FilterType.CUSTOM,
    classes = {TypeExcludeFilter.class}
), @Filter(
    type = FilterType.CUSTOM,
    classes = {AutoConfigurationExcludeFilter.class}
)}
)

而@SpringBootApplication繼承了@EnableAutoConfiguration,@EnableAutoConfiguration便是開啟自動裝配元件的開關注解。

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@AutoConfigurationPackage
@Import({AutoConfigurationImportSelector.class})
public @interface EnableAutoConfiguration {
    String ENABLED_OVERRIDE_PROPERTY = "spring.boot.enableautoconfiguration";

    Class<?>[] exclude() default {};

    String[] excludeName() default {};
}

@EnableAutoConfiguration透過@Import({AutoConfigurationImportSelector.class})指定了Bean選擇器,自動裝配的核心程式碼便是在Bean選擇器中實現。

透過AutoConfigurationImportSelector原始碼可以看出Spring Boot掃描了所有spring.factories檔案中的org.springframework.boot.autoconfigure.EnableAutoConfiguration對應的配置類列表,然後以迴圈新增@AutoConfiguration註解的方式逐個注入配置檔案中的Bean。
spring.factories示例:

org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.zwj.learn.springcloud.common.mvc.config.MvcConfigs,\
com.zwj.learn.springcloud.common.shrio.config.ShiroConfig

專案中有些配置類沒在spring.factories檔案中制定,但是仍然可以載入,是因為引入了自動掃描的功能,譬如Mybtise元件。

相關文章