經過Spring Boot的非web應用理解全註解下的Spring IoC
1、工程背景
我曾經搭建了一個系統,把這個系統運行在阿里雲服務器上。由於我只有一個雲服務器能夠運行,因此開發的系統是單機運行的。沒有額外的地方能夠存放靜態資源,那麼系統裏的靜態資源——主要是圖片,只能存放在這臺服務器上。當多用戶訪問這臺服務器的時候,受限於此服務器捉襟見肘的1M寬帶,用戶們會明顯感受圖片加載緩慢,難以忍受長時間等待。其實若是不加載圖片的話,這臺服務器能頂住足夠多的用戶同時訪問的壓力,只是無奈圖片資源這些耗流量大戶佔用大量寬帶,我也沒有足夠資金提升服務。html
爲了提升圖片加載速度,我打算將圖片資源存放在另外網絡比較好的地方。通常就是考慮雲計算提供商的對象存儲產品,恰好騰訊雲對象存儲這款產品價格實惠,因此入手了一個。把我服務器裏面的圖片遷移出來,騰訊雲有提供多端的工具可使用,這些都簡單實用,可是大量圖片遷移會產生不少重複工做,這裏騰訊雲沒有合適的工具供我選擇,並且我在遷移完成以後要對相應數據表進行更新字段信息的操做,爲了知足我個性化的需求,更快更好完成圖片遷移,我決定本身開發一個簡單的工具來遷移圖片。java
簡單規劃了一下,我決定把開發遷移工具採用 Spring Boot 框架來搭建,作出一個非 web 應用。mysql
2、踩坑之路
2.一、開始搭建
快速創建 Spring Boot 項目,項目的依賴有(省略其餘不緊要的內容)web
... <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.2.4.RELEASE</version> <relativePath/> <!-- lookup parent from repository --> </parent> ... <properties> <java.version>1.8</java.version> </properties> ... <dependencies> <!-- Spring starter --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter</artifactId> </dependency> <!-- Spring JPA --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> <!-- 自動插入工具 --> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <!-- 騰訊雲對象存儲SDK --> <dependency> <groupId>com.qcloud</groupId> <artifactId>cos_api</artifactId> <version>5.6.24</version> </dependency> <!-- mysql驅動 --> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> </dependency> </dependencies> ... <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build>
沿用以往的構建思路,而且去掉控制層,搭建非web應用,結構十分簡單(甚至用不着建包,此處爲了方便理解),省略個性化部分後,整塊目錄結構以下:spring
... │ └── src ├── test 使用Junit對java中的源代碼進行測試 └── main 主要文件目錄 ├── resources 資源根目錄(包含項目配置文件) └── java 源代碼(包含主程序) └──... 自建包目錄(好比com.xxx.xxx) ├── entity 實體類目錄 ├── repository 數據訪問層目錄 ├── service 服務層目錄 └── ***Application.java springboot啓動類
首先建立 spring boot 啓動類,很常見很簡單,類上添加 @SpringBootApplication
註解:sql
... @SpringBootApplication public class ImageMigrationApplication { public static void main(String[] args) throws Exception { SpringApplication.run(ImageMigrationApplication.class, args); } }
而後寫實體類(省略部分字段),類上添加 @Entity
註解(@Data
、@Builder
、@AllArgsConstructor
、@NoArgsConstructor
是 lombok 的註解,用於簡化開發),繼續寫 Spring 的 JPA 裏的 repository 接口(省略無關方法),用於數據訪問,類上添加 @Repository
註解:數據庫
... @Entity(name = "sys_survey_image") @Data @Builder @AllArgsConstructor @NoArgsConstructor public class TbSurveyImageEntity implements Serializable { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @Column(nullable = false, updatable = false, length = 11) private Long id; /** * 圖片類型 */ @Column(nullable = false) private String type; /** * 圖片名稱 */ private String name; /** * 相對路徑 */ @Column(nullable = false) private String virtualPath; /** * 本地路徑 */ @Column(nullable = false) private String diskPath; /** * 騰訊雲對象存儲路徑,新添字段 */ private String qcloudPath; ... }
... @Repository public interface MigrationRepository extends JpaRepository<TbSurveyImageEntity, Long> { ... }
接着是服務提供類,類上添加 @Service
註解( @Slf4j
不是spring的註解,是 lombok 的註解,這裏爲了方便開發),參考騰訊雲提供的文檔(點擊查看),使用 @Autowired
註解注入剛纔寫好的依賴的 MigrationRepository 類,用於調用相關方法:apache
... @Slf4j @Service public class MigrationService { @Autowired private MigrationRepository migrationRepository; // 1 初始化用戶身份信息(secretId, secretKey) String secretId = "COS_SECRETID"; String secretKey = "COS_SECRETKEY"; COSCredentials cred = new BasicCOSCredentials(secretId, secretKey); // 2 設置 bucket 的區域 // clientConfig 中包含了設置 region, https(默認 http), 超時, 代理等 set 方法 Region region = new Region("COS_REGION"); ClientConfig clientConfig = new ClientConfig(region); // 3 生成 cos 客戶端 COSClient cosClient = new COSClient(cred, clientConfig); /** * 查詢存儲桶列表 * @return List<Bucket> 桶列表 */ public List<Bucket> getBucketList () { List<Bucket> buckets = cosClient.listBuckets(); for (Bucket bucketElement : buckets) { String bucketName = bucketElement.getName(); String bucketLocation = bucketElement.getLocation(); log.info("bucketName:" + bucketName + " bucketLocation:" + bucketLocation); } return buckets; } /** * 獲取本地圖片列表,一次最多1000條數據 * @return 返回圖片列表 */ public List<TbSurveyImageEntity> getImgageList () { Pageable pageable = PageRequest.of(0, 1000); Page<TbSurveyImageEntity> imageEntityPage = migrationRepository.findAll(pageable); log.info("Total number of images:" + imageEntityPage.getTotalElements()); return imageEntityPage.getContent(); } ... }
最後把配置信息補充完整,用於啓動並運行。相關配置文件在資源目錄 resources 目錄下的 application.yml
下,由於本地開發和阿里雲服務器的運行環境不同,因此會有 application-dev.yml
、application-prd.yml
等相似 application-*.yml 文件名的配置文件,用於區分不一樣環境下的配置信息。這裏展現本地運行環境下配置信息,阿里雲的幾乎同樣(yml文件的配置與properties文件只是簡寫和縮進的差異,差別不大,我習慣採用yml文件)編程
spring: # 數據訪問配置 datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/[數據庫名字]?serverTimezone=GMT%2B8&useSSL=false username: [數據庫帳號] password: [數據庫密碼] jpa: database: mysql hibernate: ddl-auto: update show-sql: true
至此,該工具的工程初步創建,而且能夠開始測試應用是否能夠運行,測試與騰訊雲、本地數據庫和本地文件是否能夠訪問。api
2.二、出現問題
(1) 啓動和訪問騰訊雲成功
開始測試是否能啓動和騰訊雲對象存儲服務連通性,在啓動類中添加服務類,以下
@SpringBootApplication public class ImageMigrationApplication { public static void main(String[] args) throws Exception { SpringApplication.run(ImageMigrationApplication.class, args); MigrationService migrationService = new MigrationService(); migrationService.getBucketList(); } }
測試結果:成功運行,而且打印出存儲桶列表
【】:該符號標註日誌關鍵信息行
... 2020-07-12 16:52:16.117 INFO 17100 --- [ main] c.d.i.m.ImageMigrationApplication : The following profiles are active: dev 2020-07-12 16:52:16.809 INFO 17100 --- [ main] .s.d.r.c.RepositoryConfigurationDelegate : Bootstrapping Spring Data JPA repositories in DEFAULT mode. 2020-07-12 16:52:16.907 INFO 17100 --- [ main] .s.d.r.c.RepositoryConfigurationDelegate : Finished Spring Data repository scanning in 85ms. Found 1 JPA repository interfaces. 2020-07-12 16:52:17.562 INFO 17100 --- [ main] o.hibernate.jpa.internal.util.LogHelper : HHH000204: Processing PersistenceUnitInfo [name: default] 2020-07-12 16:52:17.673 INFO 17100 --- [ main] org.hibernate.Version : HHH000412: Hibernate Core {5.4.10.Final} 2020-07-12 16:52:17.916 INFO 17100 --- [ main] o.hibernate.annotations.common.Version : HCANN000001: Hibernate Commons Annotations {5.1.0.Final} 2020-07-12 16:52:18.721 INFO 17100 --- [ main] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Starting... 2020-07-12 16:52:18.941 INFO 17100 --- [ main] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Start completed. 2020-07-12 16:52:18.967 INFO 17100 --- [ main] org.hibernate.dialect.Dialect : HHH000400: Using dialect: org.hibernate.dialect.MySQL5Dialect 2020-07-12 16:52:19.879 INFO 17100 --- [ main] o.h.e.t.j.p.i.JtaPlatformInitiator : HHH000490: Using JtaPlatform implementation: [org.hibernate.engine.transaction.jta.platform.internal.NoJtaPlatform] 2020-07-12 16:52:19.887 INFO 17100 --- [ main] j.LocalContainerEntityManagerFactoryBean : Initialized JPA EntityManagerFactory for persistence unit 'default' 【2020-07-12 16:52:21.298 INFO 17100 --- [ main] c.d.i.m.ImageMigrationApplication : Started ImageMigrationApplication in 5.766 seconds (JVM running for 6.471)】 【2020-07-12 16:52:21.933 INFO 17100 --- [ main] c.d.i.m.service.MigrationService : bucketName:image-1258993064 bucketLocation:ap-guangzhou】 2020-07-12 16:52:21.937 INFO 17100 --- [extShutdownHook] j.LocalContainerEntityManagerFactoryBean : Closing JPA EntityManagerFactory for persistence unit 'default' 2020-07-12 16:52:21.941 INFO 17100 --- [extShutdownHook] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Shutdown initiated... 2020-07-12 16:52:21.952 INFO 17100 --- [extShutdownHook] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Shutdown completed. Process finished with exit code 0
(2) 訪問本地數據庫失敗
接着繼續測試本地數據庫是否可以可以訪問,這裏就是測試是否可以獲取圖片表信息,在啓動類中添加調用方法,以下:
@SpringBootApplication public class ImageMigrationApplication { public static void main(String[] args) throws Exception { SpringApplication.run(ImageMigrationApplication.class, args); MigrationService migrationService = new MigrationService(); migrationService.getBucketList(); migrationService.getImgageList(); } }
出現 NPE 異常信息:
... 2020-07-12 17:05:15.862 INFO 4816 --- [ main] c.d.i.m.ImageMigrationApplication : Started ImageMigrationApplication in 5.266 seconds (JVM running for 6.076) 2020-07-12 17:05:16.862 INFO 4816 --- [ main] c.d.i.m.service.MigrationService : bucketName:image-1258993064 bucketLocation:ap-guangzhou 2020-07-12 17:05:16.867 INFO 4816 --- [extShutdownHook] j.LocalContainerEntityManagerFactoryBean : Closing JPA EntityManagerFactory for persistence unit 'default' 2020-07-12 17:05:16.870 INFO 4816 --- [extShutdownHook] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Shutdown initiated... 【Exception in thread "main" java.lang.NullPointerException at cn.dxystudy.image.migration.service.MigrationService.getImgageList(MigrationService.java:67) at cn.dxystudy.image.migration.ImageMigrationApplication.main(ImageMigrationApplication.java:20)】 2020-07-12 17:05:16.897 INFO 4816 --- [extShutdownHook] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Shutdown completed. Process finished with exit code 1
在 MigrationService 類中再寫了幾個調用 MigrationRepository 接口的其餘方法,從新測試,出現一樣的錯誤。
因此該錯誤定位在 Service 獲取不到 MigrationRepository 類的實例。
2.三、嘗試解決
(1)在啓動類中注入 MigrationService
@SpringBootApplication public class ImageMigrationApplication { @Autowired MigrationService migrationService; public static void main(String[] args) throws Exception { SpringApplication.run(ImageMigrationApplication.class, args); migrationService.getBucketList(); migrationService.getImgageList(); } }
結果:編譯不經過(Java基礎知識,屬低級錯誤)
Non-static field 'migrationService' cannot be referenced from a static context 不能從靜態上下文中引用非靜態字段「migrationService」
(2)在啓動類中注入 靜態MigrationService
@SpringBootApplication public class ImageMigrationApplication { @Autowired static MigrationService migrationService; public static void main(String[] args) throws Exception { SpringApplication.run(ImageMigrationApplication.class, args); migrationService.getBucketList(); migrationService.getImgageList(); } }
結果:編譯經過,運行異常,拋出NPE
Exception in thread "main" java.lang.NullPointerException at cn.dxystudy.image.migration.ImageMigrationApplication.main(ImageMigrationApplication.java:21)
(3)取消注入 MigrationRepository ,經過 new 建立實例。
private MigrationRepository migrationRepository = new MigrationRepository();
結果:編譯不經過(Java基礎知識,屬低級錯誤)
'MigrationRepository' is abstract; cannot be instantiated 「 MigrationRepository」是抽象的;沒法實例化
(4)嘗試將 MigrationRepository 懶加載設置爲false
@Repository @Lazy(false) public interface MigrationRepository extends JpaRepository<TbSurveyImageEntity, Long> { ... }
結果:NPE
Exception in thread "main" java.lang.NullPointerException at cn.dxystudy.image.migration.service.MigrationService.getImgageList(MigrationService.java:67) at cn.dxystudy.image.migration.ImageMigrationApplication.main(ImageMigrationApplication.java:21)
(5)實現 CommandLineRunner 類而且重寫 run 方法
參考官方文檔:Spring Boot 2.2.4.RELEASE Reference
@SpringBootApplication public class ImageMigrationApplication implements CommandLineRunner { public static void main(String[] args) throws Exception { SpringApplication.run(ImageMigrationApplication.class, args); } @Override public void run(String... args) throws Exception { MigrationService migrationService = new MigrationService(); migrationService.getBucketList(); migrationService.getImgageList(); } }
結果:運行異常,由於 NPE ,仍然找不到 MigrationRepository 實例
java.lang.IllegalStateException: Failed to execute CommandLineRunner at org.springframework.boot.SpringApplication.callRunner(SpringApplication.java:787) [spring-boot-2.2.4.RELEASE.jar:2.2.4.RELEASE] at org.springframework.boot.SpringApplication.callRunners(SpringApplication.java:768) [spring-boot-2.2.4.RELEASE.jar:2.2.4.RELEASE] at org.springframework.boot.SpringApplication.run(SpringApplication.java:322) [spring-boot-2.2.4.RELEASE.jar:2.2.4.RELEASE] at org.springframework.boot.SpringApplication.run(SpringApplication.java:1226) [spring-boot-2.2.4.RELEASE.jar:2.2.4.RELEASE] at org.springframework.boot.SpringApplication.run(SpringApplication.java:1215) [spring-boot-2.2.4.RELEASE.jar:2.2.4.RELEASE] at cn.dxystudy.image.migration.ImageMigrationApplication.main(ImageMigrationApplication.java:21) [classes/:na] Caused by: java.lang.NullPointerException: null at cn.dxystudy.image.migration.service.MigrationService.getImgageList(MigrationService.java:67) ~[classes/:na] at cn.dxystudy.image.migration.ImageMigrationApplication.run(ImageMigrationApplication.java:28) [classes/:na] at org.springframework.boot.SpringApplication.callRunner(SpringApplication.java:784) [spring-boot-2.2.4.RELEASE.jar:2.2.4.RELEASE] ... 5 common frames omitted
(6)實現 CommandLineRunner 類而且重寫 run 方法,注入 MigrationService 依賴
@SpringBootApplication public class ImageMigrationApplication implements CommandLineRunner { @Autowired MigrationTestService migrationTestService; public static void main(String[] args) throws Exception { SpringApplication.run(ImageMigrationApplication.class, args); } @Override public void run(String... args) throws Exception { migrationService.getBucketList(); migrationService.getImgageList(); } }
結果:運行成功,並打印出騰訊雲桶列表和數據庫中圖片總數
... 2020-07-12 17:35:55.316 INFO 24544 --- [ main] j.LocalContainerEntityManagerFactoryBean : Initialized JPA EntityManagerFactory for persistence unit 'default' 2020-07-12 17:35:56.771 INFO 24544 --- [ main] c.d.i.m.ImageMigrationTestApplication : Started ImageMigrationTestApplication in 5.434 seconds (JVM running for 6.159) 2020-07-12 17:35:57.502 INFO 24544 --- [ main] 【c.d.i.m.service.MigrationTestService : bucketName:image-1258993064 bucketLocation:ap-guangzhou】 Hibernate: select ... 【2020-07-12 17:35:57.791 INFO 24544 --- [ main] c.d.i.m.service.MigrationTestService : Total number of images:64】 2020-07-12 17:35:57.796 INFO 24544 --- [extShutdownHook] j.LocalContainerEntityManagerFactoryBean : Closing JPA EntityManagerFactory for persistence unit 'default' ...
繼續測試其餘方法,均運行成功。
3、深刻理解
3.1 Spring 的控制反轉(IoC)的應用
Spring 依賴兩個核心理念,一個是控制反轉(Inversion of Control,IoC)
,另外一個是面向切面編程(Aspect Oriented Programming,AOP)
。IoC 容器
是 Spring 的核心,能夠說 Spring 是一種基於 IoC 容器編程的框架。Spring Boot 是基於註解開發的 Spring IoC。
IoC 是一種經過描述來生成或者獲取對象的技術,這個技術不是 Spring 甚至不是 Java 獨有的。對於 Java 初學者更多的時候所熟悉的是使用 new 關鍵字來建立對象,而在 Spring 中則不是,它是經過描述來建立對象。Spring Boot 建議使用註解的描述(XML也能夠)來生成對象。
一個系統能夠生成各類對象,而且這些對象都須要進行管理。另外,對象之間並非孤立的,它們之間還可能存在依賴關係。爲此,Spring 還提供了依賴注入的功能,使得咱們可以經過描述來管理各個對象之間的關係。
例如,一個班級是由多個老師和同窗組成的,那麼班級就依賴於多個老師和同窗了。
爲描述上述的班級、同窗和老師這3個對象關係,這裏須要一個容器。在Spring中把每個須要的管理的對象成爲Spring Bean(簡稱Bean)
,而 Spring 管理這些 Bean 的容器,被稱爲 Spring IoC容器
(簡稱 IoC容器
)。
IoC 容器須要具有兩個基本功能:
- 經過描述管理 Bean ,包括髮布和獲取 Bean
- 經過描述完成 Bean 之間的依賴關係
3.2 IoC 容器簡介
Spring IoC 容器是一個管理 Bean 的容器,在 Spring 的定義中,它要求全部的 IoC 容器都須要實現接口 BeanFactory
(它是一個頂級容器接口)。
[源碼理解:略]
在 Spring IoC 容器中,容許按類型或者名稱獲取 Bean
,這對理解 Spring 的 依賴注入(Dependency Injection,DI)
是十分重要的。
默認狀況下,Bean
都是以單例存在的,也就是使用 getBean
方法返回的都是同一個對象。
因爲 BeanFactory
的功能還不夠強大,所以 Spring 在 BeanFactory
的基礎上,還設計一個更爲高級的接口 ApplicationContext
。它是 BeanFactory
的子接口之一,在 Spring 的體系中 BeanFactory
和 ApplicationContext
是最爲重要的接口設計,在現實中使用的大部分 Spring IoC 容器是 ApplicationContext
接口的實現類。ApplicationContext
接口經過繼承上級接口,進而繼承 BeanFactory
接口,可是在 BeanFactory
的基礎上,擴展了消息國際化接口(MessageSource
)、環境可配置接口(EnvironmentCapable
)、應用事件發佈接口(ApplicationEventPublisher
)和資源模式(ResourcePatternResolver
)接口,因此它 功能會更爲強大。
[Spring IoC容器的接口設計:略]
在 Spring Boot 當中主要經過註解來裝配 Bean 到 Spring IoC 容器中,相關的是 AnnotationConfigApplicationContext ,它是基於註解的 IoC 容器。
AnnotationConfigApplicationContext 會用到兩個註解(@Configuration
和@Bean
)來定義 Java 配置文件和Bean,並將 Java 配置信息傳遞給它的構造方法,這樣它就能夠讀取配置了,而後將配置裏的 Bean 裝配到 IoC 容器中。
舉個例子:
在同個包目錄下建立如下三個文件。
首先定義一個 Java 簡單對象(Plan Ordinary Java Object, POJO)
import lombok.Data; @Data public class User { private Long id; private String userName; private String note; // @Data: getter & setter }而後定義一個Java配置文件 AppConfig.java
// @Configuration 表明這是一個Java配置文件,Spring容器會根據它來生成IoC容器去裝配Bean @Configuration public class AppConfig { // @Bean 表明將 initUser 方法返回的POJO裝配到IoC容器中 @Bean(name = "user") // 屬性name定義這個Bean的名稱,若是沒有則將方法名稱做爲Bean的名稱 public User initUser () { User user = new User(); user.setId(10010L); user.setUserName("user_name_1"); user.setNote("note_1"); return user; } }使用
AnnotationConfigApplicationContext
來構建本身的 IoC容器import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.AnnotationConfigApplicationContext; import java.util.logging.Logger; public class IoCTest { private static Logger log = Logger.getLogger(String.valueOf(IoCTest.class)); public static void main(String[] args) { // 傳入java配置文件到構造方法中,讀取配置,而後裝配Bean到IoC容器中 ApplicationContext ctx = new AnnotationConfigApplicationContext(AppConfig.class); // 因而可使用getBean方法獲取對應的POJO User user = ctx.getBean(User.class); log.info(String.valueOf(user.getId())); } }運行結果:最後打印出了預期結果 10010
19:49:55.415 [main] DEBUG org.springframework.context.annotation.AnnotationConfigApplicationContext - Refreshing org.springframework.context.annotation.AnnotationConfigApplicationContext@161cd475 19:49:55.456 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'org.springframework.context.annotation.internalConfigurationAnnotationProcessor' 19:49:55.667 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'org.springframework.context.event.internalEventListenerProcessor' 19:49:55.671 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'org.springframework.context.event.internalEventListenerFactory' 19:49:55.673 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'org.springframework.context.annotation.internalAutowiredAnnotationProcessor' 19:49:55.678 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'org.springframework.context.annotation.internalCommonAnnotationProcessor' 19:49:55.689 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'org.springframework.context.annotation.internalPersistenceAnnotationProcessor' 19:49:55.696 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'appConfig' 19:49:55.706 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'user' 【七月 12, 2020 7:49:55 下午 cn.dxystudy.image.migration.spring.ioc.IoCTest main 信息: 10010】
3.3 裝配Bean
若是一個個的 Bean 使用註解 @Bean
注入 Spring IoC 容器中,那將是一件很麻煩的事情。好在 Spring 還容許進行掃描裝配 Bean 到 IoC 容器中,對於掃描裝配而言使用的註解是 @Component
和 @ComponentScan
。
-
@Component:標明哪一個類被掃描進入Spring IoC容器
-
@ComponentScan:標明採用何種策略去掃描裝配Bean
在 Spring 中,@Service
和 @Repository
註解都注入了 @Component
,因此在默認狀況下它會被 Spring 掃描裝配到 IoC 容器中。
[源碼理解:略]
舉個例子:
將上個例子的 User.java 移入到 config 包內(代碼省略包信息),而後進行修改:
import lombok.Data; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; @Data // 若是不配置name,那麼IoC容器就會把第一個字母做爲小寫,其餘不變做爲Bean名稱放入IoC容器中 @Component("user") public class User { // @Value指定具體的值,使得Spring IoC給予對應的屬性注入對應的值 @Value("1001") private Long id; @Value("user_name_1") private String userName; @Value("note_1") private String note; // @Data: getter & setter }爲了讓 Spring IoC 容器裝配這個類,須要改造 AppConfig 類
import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; // @Configuration 表明這是一個Java配置文件,Spring容器會根據它來生成IoC容器去裝配Bean @Configuration // 新增@ComponentScan註解,意味着它會進行掃描,可是它智慧掃描該類所在的當前包和其子包 @ComponentScan public class AppConfig { // 刪除以前使用@Bean標註的建立對象方法 }在原來的測試類中,從新導入 User 類,而後進行測試,測試結果:最後打印出了預期結果1001
20:37:52.162 [main] DEBUG org.springframework.context.annotation.AnnotationConfigApplicationContext - Refreshing org.springframework.context.annotation.AnnotationConfigApplicationContext@161cd475 20:37:52.191 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'org.springframework.context.annotation.internalConfigurationAnnotationProcessor' 20:37:52.302 [main] DEBUG org.springframework.context.annotation.ClassPathBeanDefinitionScanner - Identified candidate component class: file [C:\Users\22920\IdeaProjects\imagemigration\target\classes\cn\dxystudy\image\migration\spring\ioc\config\User.class] 20:37:52.407 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'org.springframework.context.event.internalEventListenerProcessor' 20:37:52.407 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'org.springframework.context.event.internalEventListenerFactory' 20:37:52.407 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'org.springframework.context.annotation.internalAutowiredAnnotationProcessor' 20:37:52.407 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'org.springframework.context.annotation.internalCommonAnnotationProcessor' 20:37:52.422 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'org.springframework.context.annotation.internalPersistenceAnnotationProcessor' 20:37:52.422 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'appConfig' 20:37:52.438 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'user' 【七月 12, 2020 8:37:52 下午 cn.dxystudy.image.migration.spring.ioc.IoCTest main 信息: 1001】爲了使得 User 類可以被掃描,我把它遷移到了本不改放置它的配置包,這樣顯然就不太合理。爲了更加合理,
@Component
還容許自定義掃描的包,這裏再也不討論。
現實的 Java 應用每每須要引入許多第三方的包,而且頗有可能但願把第三方的包的類對象也放入到 Spring IoC 容器中,這時可使用 @Bean
註解。
舉個例子:
引入 DBCP 數據源,在 pom.xml 文件上加入項目所須要的 DBCP 包和數據庫 MySQL 驅動程序的依賴<dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-dbcp2</artifactId> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connetor-java</artifactId> </dependency>接着使用它提供的機制來生成數據源。修改上個例子的 AppConfig.java 文件,增長 getDataSource 方法
import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; @Configuration @ComponentScan public class AppConfig { @Bean(name = "dataSorce") public DataSource getDataSorce () { Properties props = new Properties(); props.setProperty("dirver", "com.mysql.cj.jdbc.Driver"); props.setProperty("url", "jdbc:mysql://localhost:3306/[數據庫名字]?serverTimezone=GMT%2B8&useSSL=false"); props.setProperty("username", "[數據庫用戶名]"); props.setProperty("password", "[數據庫用戶密碼]"); DataSource dataSource = null; try { dataSource = BasicDataSourceFactory.createDataSource(props); } catch (Exception e) { e.printStackTrace(); } return dataSource; } }啓動測試類,結果:打印出建立 Bean 的信息
... 21:10:56.588 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'appConfig' 21:10:56.599 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'user' 21:10:56.636 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'dataSorce' ...
3.4 依賴注入
以上討論瞭如何將 Bean 裝配到 IoC 容器中和如何獲取 Bean ,這節討論 Bean 之間的依賴。
在 Spring IoC 的概念中,稱之爲依賴注入(Dependency Injection,DI)。
例如:
人類(Person)有時候利用一些動物(Animal)去完成一些事情,好比說狗(Dog)是用來看門的,貓(Cat)是用來抓老鼠的,鸚鵡(Parrot)是用來迎客的……因而作一些事情就依賴於那些可愛的動物了。
定義人類和動物接口
/** * 人類接口 */ public interface Person { // 使用動物來作些事情 public void use(); // 設置動物 public void setAnimal(Animal animal); }/** * 動物接口 */ public interface Animal { // 動物能夠作點事情 public void work(); }定義兩個實現類,都須要標註
@Component
import org.springframework.beans.factory.annotation.Autowired; /** * 普通人 */ @Component // 加入註解標明將裝配進Spring IoC容器 public class OrdinaryPeople implements Person{ // 這裏的Dog類是動物的一種,因此Spring IoC容器會把Dog的實例注入到OrdinaryPeople @Autowired private Animal animal; @Override public void use() { this.animal.work(); } @Override public void setAnimal(Animal animal) { this.animal = animal; } }import org.springframework.stereotype.Component; /** * 狗 */ @Component // 加入註解標明將裝配進Spring IoC容器 public class Dog implements Animal{ @Override public void work() { System.out.println("狗【" + Dog.class.getSimpleName() + "】是用來看門的"); } }建立配置類和場景類
import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; @Configuration @ComponentScan public class DIAppConfig { }import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.AnnotationConfigApplicationContext; /** * 在家裏有個普通人在用動物作些事情 */ public class Home { public static void main(String[] args) { ApplicationContext ctx = new AnnotationConfigApplicationContext(DIAppConfig.class); OrdinaryPeople ordinaryPeople = ctx.getBean(OrdinaryPeople.class); ordinaryPeople.use(); } }運行結果:運行成功而且打印出「狗【Dog】是用來看門的」。
說明經過註解
@Autowired
成功地將 Dog 注入到了 OrdinaryPeople 實例中。... 21:56:37.052 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'DIAppConfig' 21:56:37.061 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'dog' 21:56:37.061 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'ordinaryPeople' 狗【Dog】是用來看門的
還要要注意的是 @Autowired
是一個默認必須找到對應 Bean 的註解,若是不能肯定其標註屬性必定會存在而且容許這個被標註的屬性爲null,那麼就能夠配置 @Autowired
屬性required
爲false
。
@Autowired(required = false) // 設置爲非必須Bean
除了標註屬性外,還能夠標註方法
@Override @Autowired // 標註方法 public void setAnimal(Animal animal) { this.cat = animal; }
上面的例子,只是建立了一個動物——狗,而實際上還能夠有貓(Cat),若是又建立一個貓,
/** * 貓 */ @Component // 加入註解標明將裝配進Spring IoC容器 public class Cat implements Animal{ @Override public void work() { System.out.println("貓【" + Dog.class.getSimpleName() + "】是用來抓老鼠的"); } }則會在場景類中運行出錯拋出異常,由於 Spring IoC 不知道注入哪一個動物。產生注入失敗的根本緣由是按類型(by type)查找,這樣問題成爲歧義性。
... 【Caused by: org.springframework.beans.factory.NoUniqueBeanDefinitionException: No qualifying bean of type 'cn.dxystudy.image.migration.spring.di.Animal' available: expected single matching bean but found 2: cat,dog】 at org.springframework.beans.factory.config.DependencyDescriptor.resolveNotUnique(DependencyDescriptor.java:220) at org.springframework.beans.factory.support.DefaultListableBeanFactory.doResolveDependency(DefaultListableBeanFactory.java:1265) at org.springframework.beans.factory.support.DefaultListableBeanFactory.resolveDependency(DefaultListableBeanFactory.java:1207) at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor$AutowiredFieldElement.inject(AutowiredAnnotationBeanPostProcessor.java:640) ... 14 more若是須要狗看門,那麼能夠把屬性名稱轉化爲 dog ,注入修改成
@Autowired private Animal dog; // 其餘地方引用的一樣修改爲dog運行結果:運行成功而且打印出「狗【Dog】是用來看門的」
一樣的若是須要貓抓老鼠,那麼能夠把屬性名稱轉化爲cat,一樣也是運行成功而且打印出「貓【Dog】是用來抓老鼠的」
爲了使
@Autowired
可以繼續使用,將 OrdinaryPeople 的屬性名稱從 animal 修改爲 dog 或者 cat。顯然這是一個憋屈的作法,好好一個動物卻被咱們定義成了狗/貓。消除歧義性還能利用@Primary
和@Quelifier
兩個註解,這兩個註解從不一樣角度去解決歧義性問題。
- @Primary:修改優先權的註解(問題依然存在:當多個類型同時存在該註解時)
- @Quelifier:與@Autowired組合在一塊兒,經過類型和名稱一塊兒找到Bean
具體再也不討論。
默認的狀況是不帶參數構造方法下實現依賴注入。但事實上,有些類只有帶有參數的構造方法。爲了知足這個功能,可使用 @Autowired
註解對構造方法參數進行注入。
舉個例子:
修改 OrdinaryPeople 類來知足這個功能。
import ... /** * 普通人 */ @Component // 加入註解標明將裝配進Spring IoC容器 public class OrdinaryPeople implements Person{ private Animal animal; // @Autowired @Qualifier兩個組合註解爲了消除歧義性 public OrdinaryPeople (@Autowired @Qualifier("dog") Animal animal) { this.animal = animal; } @Override public void use() { this.animal.work(); } @Override public void setAnimal(Animal animal) { this.animal = animal; } }運行結果:運行成功而且打印出狗的預期結果
3.5 生命週期
以上只是關心如何正確地將 Bean 裝配到 IoC 容器中,而沒有關心 IoC 容器如何裝配和銷燬 Bean 的過程。
有時候也須要自定義初始化或者銷燬 Bean 的過程,以知足一些 Bean 的特殊初始化和銷燬的要求。
例如,在上面使用數據源的例子中,咱們但願在其關閉的時候調用 close 方法,以釋放數據庫的連接資源,這是在項目使用過程當中很常見的要求。
這節來了解 Spring IoC 初始化和銷燬 Bean 的過程,也就是 Bean 的聲明週期的過程,它大體分爲 Bean定義
、Bean的初始化
、Bean的生存期
和Bean的銷燬
4個部分。
其中Bean定義過程大體以下:
- Spring 經過配置(如
@ComponentScan
定義的掃描路徑)去找到帶有@Component
的類。【資源定位】 - 找到資源後,開始解析,而且將定義的信息保存起來。【Bean定義】(注意此時沒有初始化Bean即沒有Bean的實例,僅僅定義)
- 把 Bean 定義發佈到 Spring IoC 容器中。【發佈Bean定義】(仍是沒有 Bean 的實例,僅僅發佈定義)
這三步只是資源定位並將 Bean 的定義發佈到 IoC 容器的過程,尚未 Bean 實例的生成,更沒有完成依賴注入。
在默認狀況下,Spring 會繼續去完成 Bean 的實例化和依賴注入,這樣從 IoC 容器中就能夠獲得一個依賴注入完成的 Bean。可是有些 Bean 會受到變化的因素影響,這是倒但願是取出 Bean 的時候完成呢個初始化和依賴注入,也就是說讓那些 Bean 只是將定義發佈到 IoC 容器中而不作實例化和依賴注入,當要取出來的時候才作初始化和依賴注入等操做。
Spring 初始化 Bean :
資源定位
—> Bean定義
—> 發佈Bean定義
—> 實例化
—> 依賴注入
—> ……
@ComponentScan
中還有一個配置項 lazyInit
,只能夠配置 Boolean
值,且默認爲 false
,也就是默認不進行延遲初始化,所以在默認的狀況下 Spring 會對 Bean 進行實例化和依賴注入對應的屬性值。
舉個例子:
改造上個例子的 OrdinaryPeople
import ... /** * 普通人 */ @Component public class OrdinaryPeople implements Person{ private Animal animal; @Override public void use() { this.animal.work(); } @Override @Autowired @Qualifier("dog") public void setAnimal(Animal animal) { System.out.println("依賴注入"); this.animal = animal; } }在場景類 Home 中對
OrdinaryPeople ordinaryPeople = ctx.getBean(OrdinaryPeople.class);
這行打上斷點進行調試。運行結果:運行成功,而且運行到斷點以前打印出「依賴注入」。
... 23:25:43.849 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'cat' 23:25:43.849 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'dog' 23:25:43.850 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'ordinaryPeople' 依賴注入 狗【Dog】是用來看門的再在配置類 DIAppConfig 的
@ComponentScan
中加入lazyInit
配置,以下@ComponentScan(lazyInit = true)運行結果:運行成功,而且運行到斷點以後打印出「依賴注入」。這是由於把它修改成了延遲初始化, Spring 並不會在發佈Bean定義後立刻完成實例化和依賴注入。
若是僅僅是實例化和依賴注入仍是比較簡單的,還不能完成進行自定義的要求。爲了完成依賴注入的功能,Spring 在完成依賴注入以後,還進行一系列的流程來完成它的生命週期。
Spring Bean 的生命週期:
· ——> 初始化
——> 依賴注入
——> setBeanName方法
——> setBeanFactory方法
——> setApplicationContext方法
——> postProcessBeforeInitialization方法
——> 自定義初始化方法
——> afterPropertiesSet方法
——> postProcessAfterInitialization犯法
——> <生存期>
——> 自定義銷燬方法
——> destroy方法
——> ·
條件裝配 Bean 和 Bean 的做用域:略
4、總結反思
4.1 優化代碼
將騰訊雲存儲相關配置信息提取到配置文件 application.yml
中,並使用 @Value
註解對屬性賦值。
# 項目配置 migration: # 騰訊雲 qcloud: secretId: AKIDlIacP****G1WtHSOtGbg secretKey: YrvjGd7****bOyM9hmbBVx region-name: ap-guangzhou # 框架配置 spring: # 數據訪問配置 datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/image***?serverTimezone=GMT%2B8&useSSL=false username: r**t password: **** jpa: database: mysql hibernate: ddl-auto: update show-sql: true # 彩色日誌輸出 output: ansi: enabled: always
服務提供類 MigrationService.java
中將改造獲取騰訊雲對象存儲客戶端的方式,除了再寫本身的業務方法,再增長關閉鏈接的方法。
@Slf4j @Service public class MigrationService { @Autowired private MigrationRepository migrationRepository; /** * 騰訊雲對象存儲客戶端 */ private COSClient cosClient; @Autowired public void getConnection (@Value("${migration.qcloud.secretKey}") String secretKey ,@Value("${migration.qcloud.secretId}") String secretId ,@Value("${migration.qcloud.region-name}") String regionName) { COSCredentials cred = new BasicCOSCredentials(secretId, secretKey); Region region = new Region(regionName); ClientConfig clientConfig = new ClientConfig(region); cosClient = new COSClient(cred, clientConfig); log.info("Initialized COSClient"); } /** * 查詢存儲桶列表 * @return List<Bucket> 桶列表 */ public List<Bucket> getBucketList () { List<Bucket> buckets = cosClient.listBuckets(); for (Bucket bucketElement : buckets) { String bucketName = bucketElement.getName(); String bucketLocation = bucketElement.getLocation(); log.info("bucketName:" + bucketName + " bucketLocation:" + bucketLocation); } return buckets; } /** * 從騰訊雲對象存儲中獲取對象列表 */ public void getObjectListFromQcloud () { ... } /** * 獲取本地圖片列表,一次最多1000條數據 * @return 返回圖片列表 */ public List<TbSurveyImageEntity> getImgageList () { Pageable pageable = PageRequest.of(0, 1000); Page<TbSurveyImageEntity> imageEntityPage = migrationRepository.findAll(pageable); log.info("Total number of images:" + imageEntityPage.getTotalElements()); return imageEntityPage.getContent(); } /** * 將本地圖片上傳到騰訊雲對象存儲 */ public void upload2Qcloud () { ... } ... /** * 更新本地圖片信息,增長新的騰訊雲地址 */ private void updataImagePath (TbSurveyImageEntity imageEntity) { migrationRepository.save(imageEntity); } /** * 關閉鏈接 */ public void shutdown () { // 關閉客戶端(關閉後臺線程) log.info("Shuting down COSClient"); cosClient.shutdown(); } }
啓動類實現 CommandLineRunner
類,注入服務依賴後並重寫run方法,在run裏面調用服務方法。
@SpringBootApplication public class ImageMigrationApplication implements CommandLineRunner { @Autowired MigrationService migrationService; public static void main(String[] args) throws Exception { SpringApplication.run(ImageMigrationApplication.class, args); } @Override public void run(String... args) throws Exception { migrationService.getBucketList(); migrationService.getObjectListFromQcloud(); migrationService.getImgageList(); migrationService.upload2Qcloud(); ... migrationService.shutdown(); } }
測試運行,運行成功,返回預期效果。
4.2 總結
經過本次 Spring Boot 非web應用的工具開發,遇到了 Spring Bean 獲取不到的問題。剛開始使用 new 獲取對象實例,可是 java 的實例不歸 Spring Ioc 容器管理,Spring Ioc 容器裏面沒有實例的信息,而我又調用實例的方法,所以出現空指針異常。經過查閱文檔,若是要在啓動類中運行命令行,須要實現CommandLineRunner類,並注入MigrationService再重寫run方法來調用服務方法。注入依賴的時候不能是靜態 (static),由於當類加載器加載靜態變量時,Spring 上下文還沒有加載,因此類加載器不會在bean中正確注入靜態類,而且會失敗。優化注入騰訊雲客戶端的代碼,經過@Autowired標註方法獲取客戶端實例。
解決問題的過程關鍵就是在啓動類中注入 MigrationService 類,由於 MigrationService 也注入 MigrationRepository 類,@Autowired 有 Spring 描述 Bean 之間關係的做用,經過 new 獲取MigrationService 實例,這個實例不能獲取 Spring IoC 管理的東西,不知道 Spring IoC 注入 MigrationRepository 的實例信息,那麼就會拋出空指針異常。
整個學習和實踐的過程讓我深刻理解了 Spring IoC 。
參考文檔
[1] 騰訊雲. 文檔中心 > 對象存儲 > SDK 文檔 > Java SDK > 快速入門
[2] Spring Boot Reference Documentation(2.2.4.RELEASE)
[3] 《深刻淺出Spring Boot 2.x》楊開振