Java筆記23 - Spring開發 - IoC容器

  • 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: 控制反轉

  • 在線書城例子:

    1. BookServicesUserServices, 都需要讀取配置, 實例化Config, 根據配置, 實例化DataSource;
    2. BookServicesUserServices可以共享一個DataSource; CarServletHistoryServlet也可以共享一個BookServicesUserServices. 誰來創建, 誰來使用, 不好處理.
    3. 很多組件需要銷燬, 以便釋放資源.例如DataSource, 如果確保使用方已經全部銷燬, 不好處理.
    4. 組件越來越多, 共享的組件會越來越複雜.
    5. 測試某個組件是複雜的, 必須在真是的數據庫環境下運行.
  • 傳統開發: 生命週期與相互之間的依賴關係如果組件自己維護, 增加系統的複雜度, 導致組件之間極爲耦合, 給測試和維護帶來複雜

  • 核心問題:

    1. 誰負責創建組件?
    2. 誰負責根據依賴關係組裝組件?
    3. 銷燬時, 如何按照依賴關係正確銷燬?
  • 使用IoC解決問題: 組件的創建, 裝配控制權從組件本身移動到容器中

  • 使用注入機制: 進行組件裝配

public class BookServices {
  private DataSource dataSource;
  public void setDataSource(DataSource dataSource) {
    this.dataSource = dataSource;
  }
}
  • 好處:

    1. BookServices不再關係如何創建DataSource, 因此不必編寫數據庫配置之類的
    2. DataSource實例被注入到BookServices, 也可以被注入到UserServices, 共享一個組件變得非常簡單
    3. 測試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;

  • userServiceBean中, 通過<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

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初始化流程:

    1. 調用構造方法創建MailService流程
    2. 根據@Autowired進行注入
    3. 調用標記有PostConstructinit()方法進行初始化
  • 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 {
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章