目錄html
本文首發於個人我的博客,Bean裝配,從Spring到Spring Boot ,歡迎訪問!java
本文旨在釐清從Spring 到Spring Boot過程當中,Bean裝配的過程。mysql
自從用上Spring Boot,真的是一直用一直爽,已經徹底沒法直視以前Spring的代碼了。約定大於配置的設計理念,使得其不須要太多的配置就能開箱即用。可是因爲其便捷性,也就意味着掩蓋了許多細節的部分,使得直接學習Spring Boot的開發者只會用,而不清楚內部的實現流程。最近恰好有空,從新回顧了一下Spring的相關內容,而且梳理了有關於Bean裝配的一些用法,描述從過去的Spring開發,到如今的Spring開發 Boot在Bean裝配上的變化和進步。程序員
在學習初期,我想每一個人都會去看一些博客,例如「Spring Spring MVC Mybatis整合」。一步一步整合出一個ssm項目。那個時候的我是沒有什麼概念的,徹底就是,跟着步驟走,新建配置文件,把內容貼進來,而後run,一個基本的腳手架項目就出來了。回顧一下,基本是如下幾步:web
1. 創建maven web項目,並在pom.xml中添加依賴。
2. 配置web.xml,引入spring-.xml配置文件。
3. 配置若干個spring-.xml文件,例如自動掃包、靜態資源映射、默認視圖解析器、數據庫鏈接池等等。
4. 寫業務邏輯代碼(dao、services、controller)spring
後期可能須要用到文件上傳了,再去xml中配置相關的節點。在開發中,基本也是遵循一個模式——三層架構,面向接口編程。類若是是Controller的就加一個@Controller;是Services的就加一個@Services註解,而後就能夠愉快的寫業務邏輯了。sql
Spring是什麼?那個時候的理解,像這樣子配置一下,再加上幾個註解,就是Spring。或者說,Spring就是這麼用的。數據庫
隨着學習的深刻,對Spring就有更深的理解了。在Spring當中,一切皆爲Bean。能夠說Bean是組成一個Spring應用的基本單位。Spring(這裏是狹義的概念,指Spring Core)中最核心部分就是對Bean的管理。express
<?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的裝配方式,發展到如今也已經有不少種了,從過去的XML到Java Config,再到如今Spring Boot的Auto Configuration,是一種不斷簡化,不斷清晰的過程。
Bean裝配通常分爲三步:註冊、掃描、注入。
XML配置Bean早已成爲明日黃花了,目前更常見的是使用Java Config和註解來進行Bean的裝配。固然,偶爾也能看到它的身影,例如用於ssm框架的集成的spring-*.xml。
Java Config的優點以下:
所以下面主要講都是基於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,指的是咱們沒法去編輯的代碼,主要是引入外部框架或依賴,或者使用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); } }
其中須要注意的是:
@Scope("prototype")
。對於這個例子而言,默認建立的線程池是單例的,在應用的任何一個地方注入後使用,用到的都是同一個線程池(全局共享);加上@Scope("prototype")
後,在每個Controller中分別注入,意味着,每個Controller都擁有各自的線程池,各自的請求會分別提交到各自的線程池中。@Primary
,標出首選的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,註冊到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,能夠封裝成一個工具類。
因爲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的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的不一樣之處在於:
mybatis
和mybatis-spring
變成了mybatis-spring-boot-starter
這裏也不對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 {}
再看@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時,就會爲咱們自動裝配。
下面的流程圖就是簡單描述了整個自動配置的流程。
其實就是複合註解啦,除了約定大於配置的自動裝配外,Spring Boot經過一個大的複合註解@SpringBootApplication,把@ComponentScan和@SpringBootConfiguration也包在裏面,使得啓動類直接就能夠做爲配置類使用,而且減小了Bean掃描這一步。
本文不算是一篇完整的Spring IoC教程,只是梳理了一下從Spring 到Spring Boot一路走來,在Bean裝配上的用法和改變。能夠看到這是一個不斷簡化,不斷進步的過程,可是核心依然不變,所以即便當下轉到Spring Boot 進行開發,在遇到一些複雜的業務上,任然須要用到Spring IoC相關的技術點,例如控制Bean的做用域,條件化Bean等等。
@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,須要改造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); } }