本文首發於我的個人博客,Bean裝配,從Spring到Spring Boot ,歡迎訪問!
本文旨在厘清從Spring 到Spring Boot過程中,Bean裝配的過程。
自從用上Spring Boot,真的是一直用一直爽,已經完全無法直視之前Spring的代碼了。約定大於配置的設計理念,使得其不需要太多的配置就能開箱即用。但是由於其便捷性,也就意味着掩蓋了許多細節的部分,使得直接學習Spring Boot的開發者只會用,而不清楚內部的實現流程。最近剛好有空,重新回顧了一下Spring的相關內容,並且梳理了有關於Bean裝配的一些用法,描述從過去的Spring開發,到現在的Spring開發 Boot在Bean裝配上的變化和進步。
從SSM的集成談到Bean的裝配
在學習初期,我想每個人都會去看一些博客,例如“Spring Spring MVC Mybatis整合”。一步一步整合出一個ssm項目。那個時候的我是沒有什么概念的,完全就是,跟着步驟走,新建配置文件,把內容貼進來,然后run,一個基本的腳手架項目就出來了。回顧一下,基本是以下幾步:
1. 建立maven web項目,並在pom.xml中添加依賴。
2. 配置web.xml,引入spring-.xml配置文件。
3. 配置若干個spring-.xml文件,例如自動掃包、靜態資源映射、默認視圖解析器、數據庫連接池等等。
4. 寫業務邏輯代碼(dao、services、controller)
后期可能需要用到文件上傳了,再去xml中配置相關的節點。在開發中,基本也是遵循一個模式——三層架構,面向接口編程。類如果是Controller的就加一個@Controller;是Services的就加一個@Services注解,然后就可以愉快的寫業務邏輯了。
Spring是什么?那個時候的理解,像這樣子配置一下,再加上幾個注解,就是Spring。或者說,Spring就是這么用的。
隨着學習的深入,對Spring就有更深的理解了。在Spring當中,一切皆為Bean。可以說Bean是組成一個Spring應用的基本單位。Spring(這里是狹義的概念,指Spring Core)中最核心部分就是對Bean的管理。
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:mvc="http://www.springframework.org/schema/mvc"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.2.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context-3.2.xsd
http://www.springframework.org/schema/mvc
http://www.springframework.org/schema/mvc/spring-mvc.xsd">
<context:annotation-config/>
<context:component-scan base-package="com.zjut.ssm.controller">
<context:include-filter type="annotation" expression="org.springframework.stereotype.Controller"/>
</context:component-scan>
<bean id="defaultViewResolver" class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<property name="viewClass" value="org.springframework.web.servlet.view.JstlView"/>
<property name="prefix" value="/WEB-INF/views/"/>
<property name="suffix" value=".jsp"/>
</bean>
<bean id="multipartResolver" class="org.springframework.web.multipart.commons.CommonsMultipartResolver">
<property name="maxUploadSize" value="20971500"/>
<property name="defaultEncoding" value="UTF-8"/>
<property name="resolveLazily" value="true"/>
</bean>
</beans>
讓我們再次看一下Spring MVC的配置文件,除了一些參數外,還有兩個bean節點,注入InternalResourceViewResolver來處理視圖,注入CommonsMultipartResolver來處理文件上傳。這個時候,如果需要集成Mybatis一起工作,類似的,注入相關的Bean就可以了。Mybatis最核心的Bean就是SqlSessionFactory,通過創建Session來進行數據庫的操作。在不使用Spring時,可以通過加載XML,讀入數據庫信息,進行創建。
String resource = "org/mybatis/example/mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
顯然,上面的代碼是不符合Spring的思想的。為了達到松耦合,高內聚,盡可能不直接去new一個實例,而是通過DI的方式,來注入bean,由Spring IoC容器進行管理。Mybatis官方給到一個MyBatis-Spring包,只需添加下面的Bean就可以組織Mybatis進行工作了(創建sqlSessionFactory,打開Session等工作),關於Mybatis的內容,這里不展開了。
<beans>
<bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource">
<property name="driverClass" value="${jdbc.driver}"/>
<property name="jdbcUrl" value="${jdbc.url}"/>
<property name="user" value="${jdbc.username}"/>
<property name="password" value="${jdbc.password}"/>
</bean>
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<property name="dataSource" ref="dataSource"/>
<property name="typeAliasesPackage" value=""/>
<property name="mapperLocations" value="classpath:mapper/*.xml"/>
</bean>
<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
<property name="sqlSessionFactoryBeanName" value="sqlSessionFactory"/>
<property name="basePackage" value=""/>
</bean>
</beans>
那么,現在我們就知道了,通過XML配置Spring,集成各種框架的實質,就是Bean裝配的。每一個框架都是由N多個Bean構成的,如果需要使用它,就必須根據框架的要求裝配相應的Bean。裝配成功的Bean由Spring IoC容器統一管理,就能夠正常進行工作。
Bean的裝配
具體Bean的裝配方式,發展到現在也已經有很多種了,從過去的XML到Java Config,再到現在Spring Boot的Auto Configuration,是一種不斷簡化,不斷清晰的過程。
Bean裝配一般分為三步:注冊、掃描、注入。
由XML到Java Config
XML配置Bean早已成為明日黃花了,目前更常見的是使用Java Config和注解來進行Bean的裝配。當然,偶爾也能看到它的身影,例如用於ssm框架的集成的spring-*.xml。
Java Config的優勢如下:
- Java是類型安全的,如果裝配過程中如果出現問題,編譯器會直接反饋。
- XML配置文件會隨着配置的增加而越來越大,不易維護管理。
- Java Config更易於重構和搜索Bean。
因此下面主要講都是基於Java的配置方法。基本流程如下:
// 注冊
@Configuration
public class BeanConfiguration {
@Bean
public AtomicInteger count() {
return new AtomicInteger();
}
}
//或者
@Componment
public class Foo{}
// 掃描
@ComponentScan(basePackages={})
@Configuration
public class BeanConfiguration {}
// 注入
@Autowired
private AtomicInteger c;
下面詳細展開。
Java Config注冊Bean,主要分為兩類,注冊非源碼的Bean和注冊源碼的Bean。
注冊非源碼的Bean
非源碼的Bean,指的是我們無法去編輯的代碼,主要是引入外部框架或依賴,或者使用Spring的一些Bean。這些Bean的配置一般采用Java文件的形式進行聲明。
新建一個使用@Configuration
修飾的配置類,然后使用@Bean
修飾需要創建Bean的方法,具體的可指定value和name值(兩者等同)。示例如下:
@Configuration
public class BeanConfiguration {
@Scope("prototype")
@Bean(value = "uploadThreadPool")
public ExecutorService downloadThreadPool() {
return Executors.newFixedThreadPool(10);
}
}
其中需要注意的是:
- Bean默認是以單例的形式創建的,整個應用中,只創建一個實例。如果需要,每次注入都創建一個新的實例,可添加注解
@Scope("prototype")
。對於這個例子而言,默認創建的線程池是單例的,在應用的任何一個地方注入后使用,用到的都是同一個線程池(全局共享);加上@Scope("prototype")
后,在每一個Controller中分別注入,意味着,每一個Controller都擁有各自的線程池,各自的請求會分別提交到各自的線程池中。 - 注入多個相同類型的Bean時,手動指定name或value值加以區分。或通過
@Primary
,標出首選的Bean。
注冊源碼的Bean
源碼的Bean,指的是我們自己寫的代碼,一般不會以@Bean的形式裝配,而是使用另外一系列具有語義的注解。(@Component、@Controller、@Service、@Repository)添加這些注解后,該類就成為Spring管理的組件類了,列出的后三個注解用的幾率最高,基本已經成為樣板注解了,Controller類添加@Controller,Service層實現添加@Service。
下面展示一個例子,通過Spring聲明自己封裝的類。
@Scope("prototype")
@Component(value = "uploadThread")
public class UploadTask implements Runnable {
private List<ByteArrayInputStream> files;
private List<String> fileNameList;
private PropertiesConfig prop = SpringUtil.getBean(PropertiesConfig.class);
// 如果直接傳入MutiPartFile,文件會無法存入,因為對象傳遞后spring會將tmp文件緩存清楚
public UploadThread(List<ByteArrayInputStream> files, List<String> fileNameList) {
this.files = files;
this.fileNameList = fileNameList;
}
@Override
public void run() {
for (int i = 0; i < files.size(); ++i) {
String fileName = fileNameList.get(i);
String filePath = FileUtils.generatePath(prop.getImageSavePath(),fileName);
FileUtils.save(new File(filePath), files.get(i));
}
}
}
接着上面的線程池講,這里我們實現了一個task,用來處理異步上傳任務。在傳統JUC中,我們一般會這么寫代碼:
private ExecutorService uploadThreadPool = Executors.newFixedThreadPool(10);
uploadThreadPool.submit(new UploadTask(fileCopyList, fileNameList));
在Spring中,我覺得就應該把代碼寫的更Spring化一些,因此添加@Component使之成為Spring Bean,並且線程非單例,添加@Scope注解。重構后的代碼如下:
@Resource(name = "uploadThreadPool")
private ExecutorService uploadThreadPool;
@PostMapping("/upload")
public RestResult upload(HttpServletRequest request) {
uploadThreadPool.submit((Runnable) SpringUtils.getBean("uploadThread", fileCopyList, fileNameList));
}
Bean的注入在下節會仔細講。其實這樣寫還有零一個原因,非Spring管理的Bean一般是無法直接注入Spring Bean的。如果我們需要在UploadTask中實現一些業務邏輯,可能需要注入一些Services,最好的做法就是講UploadTask本身也注冊成Spring Bean,那么在類中就能夠使用@Autowired進行自動注入了。
額外需要注意的是:由於線程安全的一些原因,線程類是無法直接使用@Autowired注入Bean的。一般會采用SpringUtils.getBean()
手動注入。
自動掃描
在配置類上添加 @ComponentScan
注解。該注解默認會掃描該類所在的包下所有的配置類,特殊的包可以配置basePackages
屬性。Spring掃描到所有Bean,待注入就可以使用了。
Bean的注入
對於一些不直接使用的Bean,注冊到Spring IoC容器后,我們是不需要手動去注入的。例如前面提到Mybatis的三個Bean。我們只需要根據文檔進行使用,創建Mapper接口,並使用@Mapper修飾,在調用具體的查詢方法時,Mybatis內部會進行Bean的注入,Open一個Session進行數據庫的操作。
對於我們需要使用到的Bean,就需要注入到變量中進行使用。常用Bean注入的方式有兩種。
1.注解注入(@Autowired和@Resource)。
@Autowired
是Bean注入最常用的注解,默認是通過byType的方式注入的。也就是說如果包含多個相同類型的Bean,是無法直接通過@Autowired注入的。這個時候需要通過@Qualifier限定注入的Bean。或者使用@Resource。@Resource
是通過byName的方式注入,直接在注解上標明Bean的name即可。
public class Foo{
// 正常字段注入
@Autowired
private AtomicInteger c;
// 正常構造器注入
private final AtomicInteger c;
@Autowired
public Foo(AtomicInteger c){this.c = c;}
// 歧義加上@Qualifier
@Autowired
@Qualifier("count")
private AtomicInteger c;
// 歧義直接使用@Resource(與前一種等同)
@Resource("count")
private AtomicInteger c;
}
2.調用Application Context的getBean方法。
推薦使用注解注入,但是在默寫特殊情況下,需要使用getBean()方法來注入Bean。例如之前講到的多線程環境。具體實現可見附錄,實現ApplicationContextAware,可以封裝成一個工具類。
SSM集成的Java版
由於Java Config的優勢,框架集成工作很多選擇不用XML配置了。例如之前在xml中配置Spring MVC的ViewResolver,在Java中就可以這樣去注入:
@Configuration
@EnableWebMvc
public class WebConfig extends WebMvcConfigurerAdapter {
@Bean
public ViewResolver viewResolver(){
InternalResourceViewResolver resolver = new InternalResourceViewResolver();
resolver.setPrefix("/WEB-INF/view/");
resolver.setSuffix(".jsp");
return resolver;
}
}
有興趣的,可以自己去使用Java Config集成一下,其實需要配置的東西都是一致的,只是在Java中,有的是要注冊對應的Bean,有的需要實現對應的接口罷了。由XML到Java,僅僅只是配置語言的改變,真正解放程序員,提高生產力是Spring Boot。基於約定大於配置的思路,通過Auto Configuration,大規模減少了一些缺省Bean的配置工作。
Spring Boot Magic
Auto Configuration
接下來我們看一下,基於Spring Boot的SSM集成需要配置的內容。
spring:
datasource:
url: jdbc:mysql://localhost:3306/test
username: root
password: fur@6289
mybatis:
type-aliases-package: com.fur.mybatis_web.mapper
mapper-locations: classpath:mapping/*.xml
用過Spring Boot的都知道,上面的是Spring Boot的配置文件application.yml
。只需要在pom.xml添加對應依賴,在yml里配置必要的信息。(框架再智能也不可能知道我們的數據庫信息吧😄)之前看到的CommonsMultipartResolver、SqlSessionFactoryBean等再也不需要手動去裝配了。
Spring Boot和傳統SSM的不同之處在於:
- 添加的maven依賴不同,由
mybatis
和mybatis-spring
變成了mybatis-spring-boot-starter
- 多了@SpringBootApplication,少了@ComponentScan
這里也不對Spring Boot Starter進行介紹了,只要知道這就是一個新的dependency,包含自動配置和其他需要的依賴就行了。在過去Spring中引入的依賴中,如果Boot實現了對應的starter,應該優先使用Starter。
下面我們就跟一下源碼,了解一下Spring Boot到底是如何實現Auto Configuration的。Take it easy,我們不會逐行去分析源碼,只是梳理下大概的流程。
順着思路,首先看到的是@SpringBootApplication注解中的內容,顯然這是一個復合注解,主要包含@SpringBootConfiguration、@EnableAutoConfiguration和@ComponentScan。
@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) })
public @interface SpringBootApplication {}
- @SpringBootConfiguration,內部是一個@Configuration,跟之前一樣,注解修飾的類是一個Spring配置類。
- @ComponentScan,也不陌生。集成在@SpringBootApplication注解中,創建項目時默認添加,我們無需手動開啟Bean掃描了。
- @EnableAutoConfiguration,這個就是Auto Configuration的關鍵。簡而言之,加上這個注解,Spring會自動掃描引入的依賴(classpath下的jar包)中需要Auto Configuration的項目,並且根據yml進行自動配置工作。
再看@EnableAutoConfiguration,發現內部import一個AutoConfigurationImportSelector
類,這個類基本上就是處理Auto Configuration工作的核心類了。
// AutoConfigurationImportSelector.java
/**
* Return the auto-configuration class names that should be considered. By default
* this method will load candidates using {@link SpringFactoriesLoader} with
* {@link #getSpringFactoriesLoaderFactoryClass()}.
* @param metadata the source metadata
* @param attributes the {@link #getAttributes(AnnotationMetadata) annotation
* attributes}
* @return a list of candidate configurations
*/
protected List<String> getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes) {
List<String> configurations = SpringFactoriesLoader.loadFactoryNames(getSpringFactoriesLoaderFactoryClass(),getBeanClassLoader());
Assert.notEmpty(configurations, "No auto configuration classes found in META-INF/spring.factories. If you "
+ "are using a custom packaging, make sure that file is correct.");
return configurations;
}
看到上面的這個方法,getCandidateConfigurations()
的作用就是獲取需要自動配置的依賴信息,核心功能由SpringFactoriesLoader的loadFactoryNames()
方法來完成。
// SpringFactoriesLoader.java
public final class SpringFactoriesLoader {
public static final String FACTORIES_RESOURCE_LOCATION = "META-INF/spring.factories";
/**
* Load the fully qualified class names of factory implementations of the
* given type from {@value #FACTORIES_RESOURCE_LOCATION}, using the given
* class loader.
* @param factoryClass the interface or abstract class representing the factory
* @param classLoader the ClassLoader to use for loading resources; can be
* {@code null} to use the default
* @throws IllegalArgumentException if an error occurs while loading factory names
* @see #loadFactories
*/
public static List<String> loadFactoryNames(Class<?> factoryClass, @Nullable ClassLoader classLoader) {
String factoryClassName = factoryClass.getName();
return loadSpringFactories(classLoader).getOrDefault(factoryClassName, Collections.emptyList());
}
}
這里簡單說一下流程,有興趣的可以看下這個類的源碼。SpringFactoriesLoader會掃描classpath下的所有jar包,並加載META-INF/spring.factories下的內容。我們以mybatis-spring-boot-starter為例,里面有個依賴mybatis-spring-boot-autoconfigure,可以看到存在META-INF/spring.factories。
內容如下:
# Auto Configure
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\org.mybatis.spring.boot.autoconfigure.MybatisAutoConfiguration
接下來,會獲取到key為org.springframework.boot.autoconfigure.EnableAutoConfiguration
的值。也就是Mybatis具體的自動配置類。可以看到在包org.mybatis.spring.boot.autoconfigure
下,有MybatisAutoConfiguration
和MybatisProperties
,前者是自動配置類,后者是用於配置的一些參數,對應在yml中的mybatis節點下的鍵值對。下面貼一些源碼看看:
// MybatisAutoConfiguration.java
@Configuration
@ConditionalOnClass({ SqlSessionFactory.class, SqlSessionFactoryBean.class })
@ConditionalOnSingleCandidate(DataSource.class)
@EnableConfigurationProperties(MybatisProperties.class)
@AutoConfigureAfter(DataSourceAutoConfiguration.class)
public class MybatisAutoConfiguration implements InitializingBean {
@Bean
@ConditionalOnMissingBean
public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception {
SqlSessionFactoryBean factory = new SqlSessionFactoryBean();
factory.setDataSource(dataSource);
// 省略
return factory.getObject();
}
}
這里就看到了熟悉的SqlSessionFactory,之前我們需要手動注入,現在通過@ConditionalOnMissingBean
,當Spring容器中不存在這個Bean時,就會為我們自動裝配。
下面的流程圖就是簡單描述了整個自動配置的流程。
Syntactic sugar
其實就是復合注解啦,除了約定大於配置的自動裝配外,Spring Boot通過一個大的復合注解@SpringBootApplication,把@ComponentScan和@SpringBootConfiguration也包在里面,使得啟動類直接就可以作為配置類使用,並且減少了Bean掃描這一步。
總結
本文不算是一篇完整的Spring IoC教程,只是梳理了一下從Spring 到Spring Boot一路走來,在Bean裝配上的用法和改變。可以看到這是一個不斷簡化,不斷進步的過程,但是核心依然不變,因此即使當下轉到Spring Boot 進行開發,在遇到一些復雜的業務上,任然需要用到Spring IoC相關的技術點,例如控制Bean的作用域,條件化Bean等等。
參考文獻
附錄
SpringUtils.java
@Component
public class SpringUtils implements ApplicationContextAware {
private static ApplicationContext applicationContext;
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
if (SpringUtil.applicationContext == null) {
SpringUtil.applicationContext = applicationContext;
}
}
public static ApplicationContext getApplicationContext() {
return applicationContext;
}
public static Object getBean(String name) {
return getApplicationContext().getBean(name);
}
public static <T> T getBean(Class<T> clazz) {
return getApplicationContext().getBean(clazz);
}
public static <T> T getBean(String name, Class<T> clazz) {
return getApplicationContext().getBean(name, clazz);
}
public static <T> T getBean(String name, Object... args) {
return (T) getApplicationContext().getBean(name, args);
}
}
非Spring Bean中獲取Spring Bean的方法
在非Spring Bean中獲取Spring Bean,需要改造SpringUtils.java,去掉@Component,並且不需要實現接口了,手動注入Spring Context。
public class SpringUtils {
private static ApplicationContext applicationContext;
public static void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
if (SpringUtil.applicationContext == null) {
SpringUtil.applicationContext = applicationContext;
}
}
// 余下代碼如上
}
public class SkeletonApplication {
public static void main(String[] args) {
ApplicationContext context = SpringApplication.run(SkeletonApplication.class, args);
SpringUtils.setApplicationContext(context);
}
}