-
Spring 支持快速開發Java EE的框架
-
主要模塊:
- 支持IoC和AOP的容器
- 支持JDBC和ORM的數據訪問模塊
- 支持聲明式事務的模塊
- 支持基於Servlet的MVC開發
- 支持基於Reactive的WEB開發
- 集成JMS, JavaMail, JMX, 緩存等其他模塊
-
容器:
- 爲某種特定組件的運行提供一種必要支持的一個軟件環境
- 提供需要底層服務
-
Tomcat容器:
- 爲Servlet提供運行環境
- 提供類似Http解析的底層環境
-
Docker容器: 提供Linux環境, 以便運行一個Linux進程
-
IoC容器:
- 管理所有輕量級的JavaBean組件
- 組件生命週期管理
- 配置和組裝服務
- AOP支持
- AOP基礎上的聲明式事務服務
IoC原理
-
Inversion of Control: 控制反轉
-
在線書城例子:
BookServices
和UserServices
, 都需要讀取配置, 實例化Config
, 根據配置, 實例化DataSource
;BookServices
和UserServices
可以共享一個DataSource
;CarServlet
和HistoryServlet
也可以共享一個BookServices
和UserServices
. 誰來創建, 誰來使用, 不好處理.- 很多組件需要銷燬, 以便釋放資源.例如
DataSource
, 如果確保使用方已經全部銷燬, 不好處理. - 組件越來越多, 共享的組件會越來越複雜.
- 測試某個組件是複雜的, 必須在真是的數據庫環境下運行.
-
傳統開發: 生命週期與相互之間的依賴關係如果組件自己維護, 增加系統的複雜度, 導致組件之間極爲耦合, 給測試和維護帶來複雜
-
核心問題:
- 誰負責創建組件?
- 誰負責根據依賴關係組裝組件?
- 銷燬時, 如何按照依賴關係正確銷燬?
-
使用IoC解決問題: 組件的創建, 裝配控制權從組件本身移動到容器中
-
使用注入機制: 進行組件裝配
public class BookServices {
private DataSource dataSource;
public void setDataSource(DataSource dataSource) {
this.dataSource = dataSource;
}
}
-
好處:
BookServices
不再關係如何創建DataSource
, 因此不必編寫數據庫配置之類的DataSource
實例被注入到BookServices
, 也可以被注入到UserServices
, 共享一個組件變得非常簡單- 測試
BookServices
非常簡單, 因爲是注入的DataSource
, 可以使用內存數據庫, 而不是真是的MySQL配置
-
IoC依賴注入: 將組件的創建配置和使用相分離, 並且由IoC容器負責管理組件的生命週期.
-
使用XML文件聲明: 容器如何創建組件, 以及組件見的依賴關係.
<beans>
<bean id="dataSource" class="HiDataSource" />
<bean id="bookServices" class="bookServices">
<property name="dataSource" ref="dataSource" />
</bean>
<bean id="userServlet" class="UserServices">
<property name="dataSource" ref="dataSource" />
</bean>
</beans>
- 聲明創建三個JavaBean組件, 並把id爲
dataSource
組件, 通過屬性dataSource
(即調用setDataSource()方法)注入到另外兩個組件 - 所有的組件都稱爲JavaBean, 配置一個組件就是配置一個Bean
依賴注入方式
- 可以通過
set()
方法實現 - 也可以通過構造方法實現
public class BookServices {
private DataSource dataSource;
public BookServices(DataSource dataSource) {
this.dataSource = dataSource;
}
}
無入侵容器
- 應用程序無需實現Spring的特定接口, 組件不知道自己在Spring的容器中運行.
- 好處:
- 應用程序既可以在Spring的IoC容器中運行, 也可以自己編寫代碼自己組裝.
- 測試的時候, 並不依賴Spring容器, 可單獨進行測試, 大大提高開發效率.
裝配Bean
-
每個
<bean ...>
都有一個id
標識, 相當於Bean的唯一ID; -
在
userService
Bean中, 通過<property name="..." ref="...">
注入了另一個Bean; -
Bean的順序並不重要, Spring根據依賴關係會自動正確初始化;
-
*Spring容器通過讀取XML文件後, 使用反射完成
-
使用spring框架操作
-
ClassPathXmlApplicationContext
自動從classpath中查找指定的XML的配置文件
public class Main {
public static void main(String[] args) {
// 創建一個Spring的IoC容器, 然後配置加載文件
// 讓Spring容器爲我們創建並裝配好配置文件中指定的所有Bean
ApplicationContext context = new ClassPathXmlApplicationContext("application.xml");
// 獲取Bean:
UserService userService = context.getBean(UserService.class);
// 正常調用
User user = userService.Login("bob@example", "password");
System.out.println(user.getName());
}
}
- Spring還可以通過另一種IoC容器叫做
BeanFactory
使用Annotation配置
@Component // 聲明一個bean
public class UserService {
@Autowired // 自動注入
private MailService mailService;
public void setMailService(@Autowired MailService mailService) { // 自動注入
this.mailService = mailService;
}
}
- 一般寫在字段上, 通常使用package權限字段, 以便測試
/**
* AppConfig
*/
@Configuration // 配置類
@ComponentScan // 自動搜索當前類所在的包以及子包
public class AppConfig {
public static void main(String[] args) {
ApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
UserService userService = context.getBean(UserService.class);
User user = userService.Login("[email protected]", "password");
System.out.println(user.getName());
}
}
- 使用註解方式大大簡化Spring的配置
- 每個Bean被標註爲
@Component
並正確使用@Autowired
注入; - 配置類被標註爲
@Configuration
和@ComponentScan
; - 所有Bean均在指定包以及子包內.
- 配置
AppConfig
位於自定義的頂層包
- 每個Bean被標註爲
定製Bean
Scope
-
對於Spring容器來說, 一個Bean標記爲
Component
後, 會自動創建一個單例(Singleton) -
容器初始化時創建Bean, 容器關閉前銷燬Bean
-
容器運行期間調用
getBean(Class)
獲取到的Bean總是一個實例 -
使用
@Scope
註解, 聲明一個Prototype的Bean時, 每次調用getBean(Class)
每次都會返回一個新的實例
@Component
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE) // @Scope("prototype")
public class MailSession {
}
注入List
- 自動把所有的類型爲
Validator
的Bean裝配爲一個List注入進來
@Component
public class Validators {
@Autowired
List<Validator> validators;
public void validate(String email, String password, String name) {
for (Validator validator: validators) {
validator.validate(email, password, name);
}
}
}
- Spring是掃描classpath獲得到所有的Bean, 而List是有序的, 可以通過
@Order
註解寫明
@Component
@Order(1)
public class EmailValidator implements Validator{
@Override
public void validate(String email, String password, String name) {
if (!email.matches("^[a-z0-9]+\\@[a-z0-9]+\\.[a-z]{2,10}$")) {
throw new IllegalArgumentException("invalid email: " + email);
}
}
}
可選注入
@Autowired(required = false)
如果找到了, 就注入, 沒找到, 就忽略.
創建第三方Bean
- Bean不再我們自己的package管理之內, 在
@Configuration
類中編寫Java方法創建並返回
@Configuration
@ComponentScan
public class AppConfig {
@Bean // 創建一個Bean, 這裏的Bean是單例模式
ZoneId createZoneId() {
return ZoneId.of("Z");
}
}
初始化和銷燬
- 首先引入註解標準
<dependency>
<groupId>javax.annotation</groupId>
<artifactId>javax.annotation-api</artifactId>
<version>1.3.2</version>
</dependency>
@PostConstruct
public void init() {
System.out.println("Init mail service with zoneId = " + this.zoneId);
}
@PreDestroy
public void shutdown() {
System.out.println("Shut mail service");
}
-
Spring容器對Bean初始化流程:
- 調用構造方法創建
MailService
流程 - 根據
@Autowired
進行注入 - 調用標記有
PostConstruct
的init()
方法進行初始化
- 調用構造方法創建
-
Spring只根據Annotation查找無參數方法, 對方法名不作要求
使用別名
- 對於同一種Bean, 容器只能創建一實例, 某些情況下, 我們需要對同一種類型的Bean創建多個實例.
@Bean("z")
@Primary // 定義爲主要Bean
ZoneId createZoneOfZ() {
return ZoneId.of("Z");
}
@Bean
@Qualifier("UTC8")
ZoneId createZoneOFUTC8() {
return ZoneId.of("UTC+08:00");
}
@Autowired
@Qualifier("z")
private ZoneId zoneId;
使用FactoryBean
- 可以定義個工廠, 然後由工廠創建真正的Bean
@Component
public class ZoneIdFactoryBean implements FactoryBean<ZoneId>{
String zone = "Z";
@Override
public ZoneId getObject() throws Exception {
return ZoneId.of(zone);
}
@Override
public Class<?> getObjectType() {
return ZoneId.class;
}
}
- Bean實現了FactoryBean接口後, Spring會先實例化這個工廠, 然後調用
getObject()
創建真正的Bean. getObjectType()
可以指定創建的Bean的類型, 因爲指定類型不一定和實際類型一致, 可以是接口或者抽象類.
使用Resource
- 使用Spring容器, 我們可以把配置文件, 資源文件等注入進來, 方便使用
@Component
public class AppService {
// Value("file:/path/to/logo.txt")
@Value("classpath:/logo.txt")
private Resource resource;
private String logo;
@PostConstruct
public void init() throws IOException {
try (BufferedReader reader = new BufferedReader(
new InputStreamReader(
resource.getInputStream(),
StandardCharsets.UTF_8
)
)) {
this.logo = reader.lines().collect(Collectors.joining("\n"));
System.out.println(this.logo);
}
}
}
注入配置
- 通告註解寫到方法參數中
@Value("${app.zone:Z}")
String zoneId;
@Bean
ZoneId createZoneId(@Value("${app.zone:Z}") String zoneId) {
return ZoneId.of(zoneId);
}
- 通告簡單的JavaBean持有
@Component
public class SmtpConfig {
@Value("${smtp.host}")
private String host;
@Value("${smtp.port:25}")
private int port;
public String getHost() {
return host;
}
public int getPort() {
return port;
}
}
// 進行讀取
@Component
public class MailService {
@Value("#{smtpConfig.host}")
private String smtpHost;
@Value("#{smtpConfig.port}")
private int smtpPort;
}
使用條件裝配
-
Spring容器根據
@Profile
來決定是否創建: -
三種環境
- native: 開發
- test: 測試
- producation: 生產
-
程序運行時, 加上JVM蠶食, 可以指定啓動方法
-
並可以表示使用
test
環境,master
分支代碼
-Dspring.profiles.active=test,master
使用Conditional
public class OnSmtpEnvCondition implements Condition {
@Override
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
return false;
}
}
@Component
@Conditional(OnSmtpEnvCondition.class)
public class MailService {
}