日韩av黄I国产麻豆传媒I国产91av视频在线观看I日韩一区二区三区在线看I美女国产在线I麻豆视频国产在线观看I成人黄色短片

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 >

lxf-spring开发

發(fā)布時間:2024/5/14 56 豆豆
生活随笔 收集整理的這篇文章主要介紹了 lxf-spring开发 小編覺得挺不錯的,現(xiàn)在分享給大家,幫大家做個參考.

1.什么是spring?

Spring是一個支持快速開發(fā)Java EE應(yīng)用程序的框架。它提供了一系列底層容器和基礎(chǔ)設(shè)施,并可以和大量常用的開源框架無縫集成,可以說是開發(fā)Java EE應(yīng)用程序的必備。

隨著Spring越來越受歡迎,在Spring Framework基礎(chǔ)上,又誕生了Spring Boot、Spring Cloud、Spring Data、Spring Security等一系列基于Spring Framework的項目。本章我們只介紹Spring Framework,即最核心的Spring框架

Spring Framework

Spring Framework主要包括幾個模塊:

  • 支持IoC和AOP的容器
  • 支持JDBC和ORM的數(shù)據(jù)訪問模塊;
  • 支持聲明式事務(wù)的模塊;
  • 支持基于Servlet的MVC開發(fā);
  • 支持基于Reactive的Web開發(fā);
  • 以及集成JMS、JavaMail、JMX、緩存等其他模塊。

Spring官網(wǎng)是spring.io,要注意官網(wǎng)有許多項目,我們這里說的Spring是指Spring Framework,可以直接從這里訪問最新版以及文檔,建議添加到瀏覽器收藏夾。

2.IoC容器

在學(xué)習Spring框架時,我們遇到的第一個也是最核心的概念就是容器

什么是容器?容器是一種為某種特定組件的運行提供必要支持一個軟件環(huán)境。例如,Tomcat就是一個Servlet容器,它可以為Servlet的運行提供運行環(huán)境。類似Docker這樣的軟件也是一個容器,它提供了必要的Linux環(huán)境以便運行一個特定的Linux進程。

通常來說,使用容器運行組件,除了提供一個組件運行環(huán)境之外,容器還提供了許多底層服務(wù)
例如,Servlet容器底層實現(xiàn)了TCP連接,解析HTTP協(xié)議等非常復(fù)雜的服務(wù),如果沒有容器來提供這些服務(wù),我們就無法編寫像Servlet這樣代碼簡單,功能強大的組件
早期的JavaEE服務(wù)器提供的EJB容器最重要的功能就是通過聲明式事務(wù)服務(wù),使得EJB組件的開發(fā)人員不必自己編寫冗長的事務(wù)處理代碼,所以極大地簡化了事務(wù)處理

Spring的核心就是提供了一個IoC容器,它可以管理所有輕量級的JavaBean組件,提供的底層服務(wù)包括組件的生命周期管理、配置和組裝服務(wù)、AOP支持,以及建立在AOP基礎(chǔ)上的聲明式事務(wù)服務(wù)等。

本章我們討論的IoC容器,主要介紹Spring容器如何對組件進行生命周期管理和配置組裝服務(wù)。

2.1IoC原理

Spring提供的容器又稱為IoC容器,什么是IoC?

IoC全稱Inversion of Control,直譯為控制反轉(zhuǎn)。那么何謂IoC?在理解IoC之前,我們先看看通常的Java組件是如何協(xié)作的。

我們假定一個在線書店,通過BookService獲取書籍:

public class BookService {private HikariConfig config = new HikariConfig();private DataSource dataSource = new HikariDataSource(config);public Book getBook(long bookId) {try (Connection conn = dataSource.getConnection()) {...return book;}} }

為了從數(shù)據(jù)庫查詢書籍,BookService持有一個DataSource。為了實例化一個HikariDataSource,又不得不實例化一個HikariConfig。

現(xiàn)在,我們繼續(xù)編寫UserService獲取用戶:

public class UserService {private HikariConfig config = new HikariConfig();private DataSource dataSource = new HikariDataSource(config);public User getUser(long userId) {try (Connection conn = dataSource.getConnection()) {...return user;}} }

因為UserService也需要訪問數(shù)據(jù)庫,因此,我們不得不也實例化一個HikariDataSource。

在處理用戶購買的CartServlet中,我們需要實例化UserService和BookService:

public class CartServlet extends HttpServlet {private BookService bookService = new BookService();private UserService userService = new UserService();protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {long currentUserId = getFromCookie(req);User currentUser = userService.getUser(currentUserId);Book book = bookService.getBook(req.getParameter("bookId"));cartService.addToCart(currentUser, book);...} }

類似的,在購買歷史HistoryServlet中,也需要實例化UserService和BookService:

public class HistoryServlet extends HttpServlet {private BookService bookService = new BookService();private UserService userService = new UserService(); }

上述每個組件都采用了一種簡單的通過new創(chuàng)建實例并持有的方式。仔細觀察,會發(fā)現(xiàn)以下缺點

  • 實例化一個組件其實很難,例如,BookService和UserService要創(chuàng)建HikariDataSource,實際上需要讀取配置才能先實例化HikariConfig,再實例化HikariDataSource。

  • 沒有必要讓BookService和UserService分別創(chuàng)建DataSource實例,完全可以共享同一個DataSource,但誰負責創(chuàng)建DataSource,誰負責獲取其他組件已經(jīng)創(chuàng)建的DataSource,不好處理。類似的,CartServlet和HistoryServlet也應(yīng)當共享BookService實例和UserService實例,但也不好處理。

  • 很多組件需要銷毀以便釋放資源,例如DataSource,但如果該組件被多個組件共享,如何確保它的使用方都已經(jīng)全部被銷毀

  • 隨著更多的組件被引入,例如,書籍評論,需要共享的組件寫起來會更困難,這些組件的依賴關(guān)系會越來越復(fù)雜。

  • 測試某個組件,例如BookService,是復(fù)雜的,因為必須要在真實的數(shù)據(jù)庫環(huán)境下執(zhí)行。

  • 從上面的例子可以看出,如果一個系統(tǒng)有大量的組件,其生命周期和相互之間的依賴關(guān)系如果由組件自身來維護,不但大大增加了系統(tǒng)的復(fù)雜度,而且會導(dǎo)致組件之間極為緊密的耦合,繼而給測試和維護帶來了極大的困難。

    因此,核心問題是:

  • 負責創(chuàng)建組件?================把new對象的權(quán)力交出去
  • 誰負責根據(jù)依賴關(guān)系組裝組件?======把對象之間的管理權(quán)力交出去
  • 銷毀時,如何按依賴順序正確銷毀
  • 解決這一問題的核心方案就是IoC。

    傳統(tǒng)的應(yīng)用程序中,控制權(quán)在程序本身,程序的控制流程完全由開發(fā)者控制,例如:

    CartServlet創(chuàng)建了BookService,在創(chuàng)建BookService的過程中又創(chuàng)建了DataSource組件。這種模式的缺點是,一個組件如果要使用另一個組件,必須先知道如何正確地創(chuàng)建它

    在IoC模式下,控制權(quán)發(fā)生了反轉(zhuǎn),即應(yīng)用程序轉(zhuǎn)移到了IoC容器所有組件不再由應(yīng)用程序自己創(chuàng)建和配置,而是由IoC容器負責,這樣,應(yīng)用程序只需要直接使用已經(jīng)創(chuàng)建好并且配置好的組件。為了能讓組件在IoC容器中被“裝配”出來,需要某種“注入”機制,例如,BookService自己并不會創(chuàng)建DataSource,而是等待外部通過setDataSource()方法來注入一個DataSource:

    public class BookService {private DataSource dataSource;public void setDataSource(DataSource dataSource) {this.dataSource = dataSource;} }

    不直接new一個DataSource,而是注入一個DataSource,這個小小的改動雖然簡單,卻帶來了一系列好處

  • BookService不再關(guān)心如何創(chuàng)建DataSource,因此,不必編寫讀取數(shù)據(jù)庫配置之類的代碼
  • DataSource實例被注入到BookService,同樣也可以注入到UserService,因此,共享一個組件非常簡單
  • 測試BookService更容易,因為注入的是DataSource,可以使用內(nèi)存數(shù)據(jù)庫,而不是真實的MySQL配置。
  • 因此,IoC又稱為依賴注入(DI:Dependency Injection),它解決了一個最主要的問題:將組件的創(chuàng)建+配置? 與? 組件的使用??相分離,并且,由IoC容器負責管理組件的生命周期

    因為IoC容器要負責實例化所有的組件,因此,有必要告訴容器如何創(chuàng)建組件以及各組件的依賴關(guān)系。一種最簡單的配置是通過XML文件來實現(xiàn),例如:

    <beans><bean id="dataSource" class="HikariDataSource" /><bean id="bookService" class="BookService"><property name="dataSource" ref="dataSource" /></bean><bean id="userService" class="UserService"><property name="dataSource" ref="dataSource" /></bean> </beans>

    如何new對象和管理對象之間的關(guān)系以及對象的屬性??====可通過設(shè)置xml配置文件,當加載配置文件時,就new出對象了。

    上述XML配置文件指示IoC容器創(chuàng)建3個JavaBean組件,并把id為dataSource的組件通過屬性dataSource(即調(diào)用setDataSource()方法)注入到另外兩個組件中。

    在Spring的IoC容器中,我們把所有組件統(tǒng)稱為JavaBean,即配置一個組件就是配置一個Bean。

    依賴注入方式

    我們從上面的代碼可以看到,依賴注入可以通過set()方法實現(xiàn)。但依賴注入也可以通過構(gòu)造方法實現(xiàn)。

    很多Java類都具有帶參數(shù)的構(gòu)造方法,如果我們把BookService改造為通過構(gòu)造方法注入,那么實現(xiàn)代碼如下:

    public class BookService {private DataSource dataSource;public BookService(DataSource dataSource) {this.dataSource = dataSource;} }

    Spring的IoC容器同時支持屬性注入和構(gòu)造方法注入,并允許混合使用。

    無侵入容器

    在設(shè)計上,Spring的IoC容器是一個高度可擴展的無侵入容器。所謂無侵入,是指應(yīng)用程序的組件無需實現(xiàn)Spring的特定接口,或者說,組件根本不知道自己在Spring的容器中運行。這種無侵入的設(shè)計有以下好處:

  • 應(yīng)用程序組件既可以在Spring的IoC容器中運行,也可以自己編寫代碼自行組裝配置;
  • 測試的時候并不依賴Spring容器,可單獨進行測試,大大提高了開發(fā)效率。
  • 2.2裝配Bean

    我們前面討論了為什么要使用Spring的IoC容器,因為讓容器來為我們創(chuàng)建并裝配Bean能獲得很大的好處,那么到底如何使用IoC容器?裝配好的Bean又如何使用

    我們來看一個具體的用戶注冊登錄的例子。整個工程的結(jié)構(gòu)如下:

    spring-ioc-appcontext ├── pom.xml └── src└── main├── java│?? └── com│?? └── itranswarp│?? └── learnjava│?? ├── Main.java│?? └── service│?? ├── MailService.java│?? ├── User.java│?? └── UserService.java└── resources└── application.xml

    首先,我們用Maven創(chuàng)建工程并引入spring-context依賴:

    • org.springframework:spring-context:6.0.0

    我們先編寫一個MailService,用于在用戶登錄和注冊成功后發(fā)送郵件通知:

    public class MailService {private ZoneId zoneId = ZoneId.systemDefault();public void setZoneId(ZoneId zoneId) {this.zoneId = zoneId;}public String getTime() {return ZonedDateTime.now(this.zoneId).format(DateTimeFormatter.ISO_ZONED_DATE_TIME);}public void sendLoginMail(User user) {System.err.println(String.format("Hi, %s! You are logged in at %s", user.getName(), getTime()));}public void sendRegistrationMail(User user) {System.err.println(String.format("Welcome, %s!", user.getName()));} }

    再編寫一個UserService,實現(xiàn)用戶注冊和登錄:

    public class UserService {private MailService mailService;public void setMailService(MailService mailService) {this.mailService = mailService;}private List<User> users = new ArrayList<>(List.of( // users:new User(1, "bob@example.com", "password", "Bob"), // bobnew User(2, "alice@example.com", "password", "Alice"), // alicenew User(3, "tom@example.com", "password", "Tom"))); // tompublic User login(String email, String password) {for (User user : users) {if (user.getEmail().equalsIgnoreCase(email) && user.getPassword().equals(password)) {mailService.sendLoginMail(user);return user;}}throw new RuntimeException("login failed.");}public User getUser(long id) {return this.users.stream().filter(user -> user.getId() == id).findFirst().orElseThrow();}public User register(String email, String password, String name) {users.forEach((user) -> {if (user.getEmail().equalsIgnoreCase(email)) {throw new RuntimeException("email exist.");}});User user = new User(users.stream().mapToLong(u -> u.getId()).max().getAsLong() + 1, email, password, name);users.add(user);mailService.sendRegistrationMail(user);return user;} }

    注意到UserService通過setMailService()注入了一個MailService。

    然后,我們需要編寫一個特定的application.xml配置文件告訴Spring的IoC容器應(yīng)該如何創(chuàng)建并組裝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"xsi:schemaLocation="http://www.springframework.org/schema/beanshttps://www.springframework.org/schema/beans/spring-beans.xsd"><bean id="userService" class="com.itranswarp.learnjava.service.UserService"><property name="mailService" ref="mailService" /></bean><bean id="mailService" class="com.itranswarp.learnjava.service.MailService" /> </beans>

    注意觀察上述配置文件,其中與XML Schema相關(guān)的部分格式是固定的,我們只關(guān)注兩個<bean ...>的配置:

    • 每個<bean ...>都有一個id標識,相當于Bean的唯一ID
    • 在userServiceBean中,通過<property name="..." ref="..." />注入了另一個Bean
    • Bean的順序不重要,Spring根據(jù)依賴關(guān)系會自動正確初始化。

    上述XML配置文件用Java代碼寫出來,就像這樣

    UserService userService = new UserService(); MailService mailService = new MailService(); userService.setMailService(mailService);

    只不過Spring容器是通過讀取XML文件后使用反射完成的

    如果注入的不是Bean,而是boolean、int、String這樣的數(shù)據(jù)類型,則通過value注入,例如,創(chuàng)建一個HikariDataSource:

    <bean id="dataSource" class="com.zaxxer.hikari.HikariDataSource"><property name="jdbcUrl" value="jdbc:mysql://localhost:3306/test" /><property name="username" value="root" /><property name="password" value="password" /><property name="maximumPoolSize" value="10" /><property name="autoCommit" value="true" /> </bean>

    最后一步,我們需要創(chuàng)建一個Spring的IoC容器實例,然后加載配置文件,讓Spring容器為我們創(chuàng)建并裝配好配置文件中指定的所有Bean,這只需要一行代碼

    ApplicationContext context = new ClassPathXmlApplicationContext("application.xml");

    接下來,我們就可以從Spring容器中“取出”裝配好的Bean然后使用它:

    // 獲取Bean: UserService userService = context.getBean(UserService.class); // 正常調(diào)用: User user = userService.login("bob@example.com", "password");

    完整的main()方法如下:

    public class Main {public static void main(String[] args) {ApplicationContext context = new ClassPathXmlApplicationContext("application.xml");UserService userService = context.getBean(UserService.class);User user = userService.login("bob@example.com", "password");System.out.println(user.getName());} }

    ApplicationContext

    我們從創(chuàng)建Spring容器的代碼

    ApplicationContext context = new ClassPathXmlApplicationContext("application.xml");

    可以看到,Spring容器就是ApplicationContext,它是一個接口有很多實現(xiàn)類,這里我們選擇ClassPathXmlApplicationContext,表示它會自動從classpath中查找指定的XML配置文件。

    獲得了ApplicationContext的實例,就獲得了IoC容器的引用。從ApplicationContext中我們可以根據(jù)Bean的ID獲取Bean,但更多的時候我們根據(jù)Bean的類型獲取Bean的引用

    UserService userService = context.getBean(UserService.class);

    Spring還提供另一種IoC容器叫BeanFactory,使用方式和ApplicationContext類似:

    BeanFactory factory = new XmlBeanFactory(new ClassPathResource("application.xml")); MailService mailService = factory.getBean(MailService.class);

    BeanFactory和ApplicationContext的區(qū)別在于,BeanFactory的實現(xiàn)是按需創(chuàng)建即第一次獲取Bean時才創(chuàng)建這個Bean,而ApplicationContext會一次性創(chuàng)建所有的Bean。實際上,ApplicationContext接口是從BeanFactory接口繼承而來的,并且,ApplicationContext提供了一些額外的功能,包括國際化支持、事件和通知機制等。通常情況下,我們總是使用ApplicationContext,很少會考慮使用BeanFactory。

    2.3使用Annotation配置

    使用Spring的IoC容器,實際上就是通過類似XML這樣的配置文件把我們自己的Bean的依賴關(guān)系描述出來,然后讓容器來創(chuàng)建并裝配Bean。一旦容器初始化完畢,我們就直接從容器中獲取Bean使用它們。

    使用XML配置的優(yōu)點所有的Bean都能一目了然地列出來,并通過配置注入能直觀地看到每個Bean的依賴。它的缺點是寫起來非常繁瑣每增加一個組件,就必須把新的Bean配置到XML中。

    有沒有其他更簡單的配置方式呢?

    有!我們可以使用Annotation配置,可以完全不需要XML讓Spring自動掃描Bean并組裝它們

    我們把上一節(jié)的示例改造一下,先刪除XML配置文件,然后,給UserService和MailService添加幾個注解。

    首先,我們給MailService添加一個@Component注解:

    @Component public class MailService {... }

    這個@Component注解就相當于定義了一個Bean,它有一個可選的名稱,默認是mailService,即小寫開頭的類名。

    然后,我們給UserService添加一個@Component注解和一個@Autowired注解

    @Component public class UserService {@AutowiredMailService mailService;... }

    使用@Autowired就相當于把指定類型的Bean注入到指定的字段中。和XML配置相比,@Autowired大幅簡化了注入,因為它不但可以寫在set()方法上,還可以直接寫在字段上,甚至可以寫在構(gòu)造方法中:

    @Component public class UserService {MailService mailService;public UserService(@Autowired MailService mailService) {this.mailService = mailService;}... }

    我們一般把@Autowired寫在字段上,通常使用package權(quán)限的字段,便于測試。

    最后,編寫一個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("bob@example.com", "password");System.out.println(user.getName());} }

    除了main()方法外,AppConfig標注了@Configuration,表示它是一個配置類,因為我們創(chuàng)建ApplicationContext時:

    ApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);

    使用的實現(xiàn)類是AnnotationConfigApplicationContext,必須傳入一個標注了@Configuration的類名。

    此外,AppConfig還標注了@ComponentScan,它告訴容器,自動搜索當前類所在的包以及子包,把所有標注為@Component的Bean自動創(chuàng)建出來,并根據(jù)@Autowired進行裝配

    整個工程結(jié)構(gòu)如下:

    spring-ioc-annoconfig ├── pom.xml └── src└── main└── java└── com└── itranswarp└── learnjava├── AppConfig.java└── service├── MailService.java├── User.java└── UserService.java

    使用Annotation配合自動掃描能大幅簡化Spring的配置,我們只需要保證:

    • 每個Bean被標注為@Component并正確使用@Autowired注入;
    • 配置類被標注為@Configuration和@ComponentScan;
    • 所有Bean均在指定包以及子包內(nèi)。

    使用@ComponentScan非常方便,但是,我們也要特別注意包的層次結(jié)構(gòu)。通常來說,啟動配置AppConfig位于自定義的頂層包(例如com.itranswarp.learnjava),其他Bean按類別放入子包。

    思考

    如果我們想給UserService注入HikariDataSource,但是這個類位于com.zaxxer.hikari包中,并且HikariDataSource也不可能有@Component注解,如何告訴IoC容器創(chuàng)建并配置HikariDataSource?或者換個說法,如何創(chuàng)建并配置一個第三方Bean?

    2.3定制Bean

    Scope

    對于Spring容器來說,當我們把一個Bean標記為@Component后,它就會自動為我們創(chuàng)建一個單例(Singleton),即容器初始化時創(chuàng)建Bean,容器關(guān)閉前銷毀Bean。在容器運行期間,我們調(diào)用getBean(Class)獲取到的Bean總是同一個實例

    還有一種Bean,我們每次調(diào)用getBean(Class),容器都返回一個新的實例,這種Bean稱為Prototype(原型),它的生命周期顯然和Singleton不同。聲明一個Prototype的Bean時,需要添加一個額外的@Scope注解:

    @Component @Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE) // @Scope("prototype") public class MailSession {... }

    注入List

    有些時候,我們會有一系列接口相同,不同實現(xiàn)類的Bean。例如,注冊用戶時,我們要對email、password和name這3個變量進行驗證。為了便于擴展,我們先定義驗證接口:

    public interface Validator {void validate(String email, String password, String name); }

    然后,分別使用3個Validator對用戶參數(shù)進行驗證:

    @Component public class EmailValidator implements Validator {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);}} }@Component public class PasswordValidator implements Validator {public void validate(String email, String password, String name) {if (!password.matches("^.{6,20}$")) {throw new IllegalArgumentException("invalid password");}} }@Component public class NameValidator implements Validator {public void validate(String email, String password, String name) {if (name == null || name.isBlank() || name.length() > 20) {throw new IllegalArgumentException("invalid name: " + name);}} }

    最后,我們通過一個Validators作為入口進行驗證:

    @Component public class Validators {@AutowiredList<Validator> validators;public void validate(String email, String password, String name) {for (var validator : this.validators) {validator.validate(email, password, name);}} }

    注意到Validators被注入了一個List<Validator>,Spring會自動把所有類型為Validator的Bean裝配為一個List注入進來,這樣一來,我們每新增一個Validator類型,就自動被Spring裝配到Validators中了,非常方便。

    因為Spring是通過掃描classpath獲取到所有的Bean,而List是有序的,要指定List中Bean的順序,可以加上@Order注解:

    @Component @Order(1) public class EmailValidator implements Validator {... }@Component @Order(2) public class PasswordValidator implements Validator {... }@Component @Order(3) public class NameValidator implements Validator {... }

    可選注入

    默認情況下,當我們標記了一個@Autowired后,Spring如果沒有找到對應(yīng)類型的Bean,它會拋出NoSuchBeanDefinitionException異常。

    可以給@Autowired增加一個required = false的參數(shù):

    @Component public class MailService {@Autowired(required = false)ZoneId zoneId = ZoneId.systemDefault();... }

    這個參數(shù)告訴Spring容器,如果找到一個類型為ZoneId的Bean,就注入,如果找不到,就忽略。

    這種方式非常適合有定義就使用定義,沒有就使用默認值的情況。

    創(chuàng)建第三方Bean

    如果一個Bean不在我們自己的package管理之內(nèi),例如ZoneId,如何創(chuàng)建它?

    答案是我們自己在@Configuration類中編寫一個Java方法創(chuàng)建并返回它,注意給方法標記一個@Bean注解:

    @Configuration @ComponentScan public class AppConfig {// 創(chuàng)建一個Bean:@BeanZoneId createZoneId() {return ZoneId.of("Z");} }

    Spring對標記為@Bean的方法只調(diào)用一次,因此返回的Bean仍然是單例。

    初始化和銷毀

    有些時候,一個Bean在注入必要的依賴后,需要進行初始化(監(jiān)聽消息等)。在容器關(guān)閉時,有時候還需要清理資源(關(guān)閉連接池等)。我們通常會定義一個init()方法進行初始化,定義一個shutdown()方法進行清理,然后,引入JSR-250定義的Annotation:

    • jakarta.annotation:jakarta.annotation-api:2.1.1

    在Bean的初始化和清理方法上標記@PostConstruct和@PreDestroy:

    @Component public class MailService {@Autowired(required = false)ZoneId zoneId = ZoneId.systemDefault();@PostConstructpublic void init() {System.out.println("Init mail service with zoneId = " + this.zoneId);}@PreDestroypublic void shutdown() {System.out.println("Shutdown mail service");} }

    Spring容器會對上述Bean做如下初始化流程:

    • 調(diào)用構(gòu)造方法創(chuàng)建MailService實例;
    • 根據(jù)@Autowired進行注入;
    • 調(diào)用標記有@PostConstruct的init()方法進行初始化。

    而銷毀時,容器會首先調(diào)用標記有@PreDestroy的shutdown()方法。

    Spring只根據(jù)Annotation查找無參數(shù)方法,對方法名不作要求。

    使用別名

    默認情況下,對一種類型的Bean,容器只創(chuàng)建一個實例。但有些時候,我們需要對一種類型的Bean創(chuàng)建多個實例。例如,同時連接多個數(shù)據(jù)庫,就必須創(chuàng)建多個DataSource實例。

    如果我們在@Configuration類中創(chuàng)建了多個同類型的Bean:

    @Configuration @ComponentScan public class AppConfig {@BeanZoneId createZoneOfZ() {return ZoneId.of("Z");}@BeanZoneId createZoneOfUTC8() {return ZoneId.of("UTC+08:00");} }

    Spring會報NoUniqueBeanDefinitionException異常,意思是出現(xiàn)了重復(fù)的Bean定義。

    這個時候,需要給每個Bean添加不同的名字:

    @Configuration @ComponentScan public class AppConfig {@Bean("z")ZoneId createZoneOfZ() {return ZoneId.of("Z");}@Bean@Qualifier("utc8")ZoneId createZoneOfUTC8() {return ZoneId.of("UTC+08:00");} }

    可以用@Bean("name")指定別名,也可以用@Bean+@Qualifier("name")指定別名。

    存在多個同類型的Bean時,注入ZoneId又會報錯:

    NoUniqueBeanDefinitionException: No qualifying bean of type 'java.time.ZoneId' available: expected single matching bean but found 2

    意思是期待找到唯一的ZoneId類型Bean,但是找到兩。因此,注入時,要指定Bean的名稱:

    @Component public class MailService {@Autowired(required = false)@Qualifier("z") // 指定注入名稱為"z"的ZoneIdZoneId zoneId = ZoneId.systemDefault();... }

    還有一種方法是把其中某個Bean指定為@Primary:

    @Configuration @ComponentScan public class AppConfig {@Bean@Primary // 指定為主要Bean@Qualifier("z")ZoneId createZoneOfZ() {return ZoneId.of("Z");}@Bean@Qualifier("utc8")ZoneId createZoneOfUTC8() {return ZoneId.of("UTC+08:00");} }

    這樣,在注入時,如果沒有指出Bean的名字,Spring會注入標記有@Primary的Bean。這種方式也很常用。例如,對于主從兩個數(shù)據(jù)源,通常將主數(shù)據(jù)源定義為@Primary:

    @Configuration @ComponentScan public class AppConfig {@Bean@PrimaryDataSource createMasterDataSource() {...}@Bean@Qualifier("slave")DataSource createSlaveDataSource() {...} }

    其他Bean默認注入的就是主數(shù)據(jù)源。如果要注入從數(shù)據(jù)源,那么只需要指定名稱即可。

    使用FactoryBean

    我們在設(shè)計模式的工廠方法中講到,很多時候,可以通過工廠模式創(chuàng)建對象。Spring也提供了工廠模式,允許定義一個工廠,然后由工廠創(chuàng)建真正的Bean。

    用工廠模式創(chuàng)建Bean需要實現(xiàn)FactoryBean接口。我們觀察下面的代碼:

    @Component public class ZoneIdFactoryBean implements FactoryBean<ZoneId> {String zone = "Z";@Overridepublic ZoneId getObject() throws Exception {return ZoneId.of(zone);}@Overridepublic Class<?> getObjectType() {return ZoneId.class;} }

    當一個Bean實現(xiàn)了FactoryBean接口后,Spring會先實例化這個工廠,然后調(diào)用getObject()創(chuàng)建真正的Bean。getObjectType()可以指定創(chuàng)建的Bean的類型,因為指定類型不一定與實際類型一致,可以是接口或抽象類。

    因此,如果定義了一個FactoryBean,要注意Spring創(chuàng)建的Bean實際上是這個FactoryBean的getObject()方法返回的Bean。為了和普通Bean區(qū)分,我們通常都以XxxFactoryBean命名。

    小結(jié)

    Spring默認使用Singleton創(chuàng)建Bean,也可指定Scope為Prototype;

    可將相同類型的Bean注入List或數(shù)組;

    可用@Autowired(required=false)允許可選注入;

    可用帶@Bean標注的方法創(chuàng)建Bean;

    可使用@PostConstruct和@PreDestroy對Bean進行初始化和清理;

    相同類型的Bean只能有一個指定為@Primary,其他必須用@Quanlifier("beanName")指定別名;

    注入時,可通過別名@Quanlifier("beanName")指定某個Bean;

    可以定義FactoryBean來使用工廠模式創(chuàng)建Bean。

    3.4使用Resource

    在Java程序中,我們經(jīng)常會讀取配置文件、資源文件等。使用Spring容器時,我們也可以把“文件”注入進來,方便程序讀取。

    例如,AppService需要讀取logo.txt這個文件,通常情況下,我們需要寫很多繁瑣的代碼,主要是為了定位文件,打開InputStream。

    Spring提供了一個org.springframework.core.io.Resource(注意不是jarkata.annotation.Resource或javax.annotation.Resource),它可以像String、int一樣使用@Value注入:

    @Component public class AppService {@Value("classpath:/logo.txt")private Resource resource;private String logo;@PostConstructpublic void init() throws IOException {try (var reader = new BufferedReader(new InputStreamReader(resource.getInputStream(), StandardCharsets.UTF_8))) {this.logo = reader.lines().collect(Collectors.joining("\n"));}} }

    注入Resource最常用的方式是通過classpath,即類似classpath:/logo.txt表示在classpath中搜索logo.txt文件,然后,我們直接調(diào)用Resource.getInputStream()就可以獲取到輸入流,避免了自己搜索文件的代碼。

    也可以直接指定文件的路徑,例如:

    @Value("file:/path/to/logo.txt") private Resource resource;

    但使用classpath是最簡單的方式。上述工程結(jié)構(gòu)如下:

    spring-ioc-resource ├── pom.xml └── src└── main├── java│?? └── com│?? └── itranswarp│?? └── learnjava│?? ├── AppConfig.java│?? └── AppService.java└── resources└── logo.txt

    使用Maven的標準目錄結(jié)構(gòu),所有資源文件放入src/main/resources即可。

    小結(jié)

    Spring提供了Resource類便于注入資源文件。

    最常用的注入是通過classpath以classpath:/path/to/file的形式注入

    3.5注入配置

    在開發(fā)應(yīng)用程序時,經(jīng)常需要讀取配置文件。最常用的配置方法是以key=value的形式寫在.properties文件中。

    例如,MailService根據(jù)配置的app.zone=Asia/Shanghai來決定使用哪個時區(qū)。要讀取配置文件,我們可以使用上一節(jié)講到的Resource來讀取位于classpath下的一個app.properties文件。但是,這樣仍然比較繁瑣。

    Spring容器還提供了一個更簡單的@PropertySource來自動讀取配置文件。我們只需要在@Configuration配置類上再添加一個注解:

    @Configuration @ComponentScan @PropertySource("app.properties") // 表示讀取classpath的app.properties public class AppConfig {@Value("${app.zone:Z}")String zoneId;@BeanZoneId createZoneId() {return ZoneId.of(zoneId);} }

    Spring容器看到@PropertySource("app.properties")注解后,自動讀取這個配置文件,然后,我們使用@Value正常注入:

    @Value("${app.zone:Z}") String zoneId;

    注意注入的字符串語法,它的格式如下:

    • "${app.zone}"表示讀取key為app.zone的value,如果key不存在,啟動將報錯;
    • "${app.zone:Z}"表示讀取key為app.zone的value,但如果key不存在,就使用默認值Z。

    這樣一來,我們就可以根據(jù)app.zone的配置來創(chuàng)建ZoneId。

    還可以把注入的注解寫到方法參數(shù)中:

    @Bean ZoneId createZoneId(@Value("${app.zone:Z}") String zoneId) {return ZoneId.of(zoneId); }

    可見,先使用@PropertySource讀取配置文件,然后通過@Value以${key:defaultValue}的形式注入,可以極大地簡化讀取配置的麻煩。

    另一種注入配置的方式是先通過一個簡單的JavaBean持有所有的配置,例如,一個SmtpConfig:

    @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;} }

    然后,在需要讀取的地方,使用#{smtpConfig.host}注入:

    @Component public class MailService {@Value("#{smtpConfig.host}")private String smtpHost;@Value("#{smtpConfig.port}")private int smtpPort; }

    注意觀察#{}這種注入語法,它和${key}不同的是,#{}表示從JavaBean讀取屬性。"#{smtpConfig.host}"的意思是,從名稱為smtpConfig的Bean讀取host屬性,即調(diào)用getHost()方法。一個Class名為SmtpConfig的Bean,它在Spring容器中的默認名稱就是smtpConfig,除非用@Qualifier指定了名稱。

    使用一個獨立的JavaBean持有所有屬性,然后在其他Bean中以#{bean.property}注入的好處是,多個Bean都可以引用同一個Bean的某個屬性。例如,如果SmtpConfig決定從數(shù)據(jù)庫中讀取相關(guān)配置項,那么MailService注入的@Value("#{smtpConfig.host}")仍然可以不修改正常運行。

    小結(jié)

    Spring容器可以通過@PropertySource自動讀取配置,并以@Value("${key}")的形式注入;

    可以通過${key:defaultValue}指定默認值;

    以#{bean.property}形式注入時,Spring容器自動把指定Bean的指定屬性值注入。

    3.使用AOP

    AOP是Aspect Oriented Programming,即面向切面編程。

    那什么是AOP?

    我們先回顧一下OOP:Object Oriented Programming,OOP作為面向?qū)ο缶幊痰哪J?#xff0c;獲得了巨大的成功,OOP的主要功能是數(shù)據(jù)封裝、繼承和多態(tài)。

    而AOP是一種新的編程方式,它和OOP不同,OOP把系統(tǒng)看作多個對象的交互,AOP把系統(tǒng)分解為不同的關(guān)注點,或者稱之為切面(Aspect)。

    要理解AOP的概念,我們先用OOP舉例,比如一個業(yè)務(wù)組件BookService,它有幾個業(yè)務(wù)方法:

    • createBook:添加新的Book;
    • updateBook:修改Book;
    • deleteBook:刪除Book。

    對每個業(yè)務(wù)方法,例如,createBook(),除了業(yè)務(wù)邏輯,還需要安全檢查、日志記錄和事務(wù)處理,它的代碼像這樣:

    public class BookService {public void createBook(Book book) {securityCheck();Transaction tx = startTransaction();try {// 核心業(yè)務(wù)邏輯tx.commit();} catch (RuntimeException e) {tx.rollback();throw e;}log("created book: " + book);} }

    繼續(xù)編寫updateBook(),代碼如下:

    public class BookService {public void updateBook(Book book) {securityCheck();Transaction tx = startTransaction();try {// 核心業(yè)務(wù)邏輯tx.commit();} catch (RuntimeException e) {tx.rollback();throw e;}log("updated book: " + book);} }

    對于安全檢查、日志、事務(wù)等代碼,它們會重復(fù)出現(xiàn)在每個業(yè)務(wù)方法中。使用OOP,我們很難將這些四處分散的代碼模塊化。

    考察業(yè)務(wù)模型可以發(fā)現(xiàn),BookService關(guān)心的是自身的核心邏輯,但整個系統(tǒng)還要求關(guān)注安全檢查、日志、事務(wù)等功能,這些功能實際上“橫跨”多個業(yè)務(wù)方法,為了實現(xiàn)這些功能,不得不在每個業(yè)務(wù)方法上重復(fù)編寫代碼。

    一種可行的方式是使用Proxy模式,將某個功能,例如,權(quán)限檢查,放入Proxy中:

    public class SecurityCheckBookService implements BookService {private final BookService target;public SecurityCheckBookService(BookService target) {this.target = target;}public void createBook(Book book) {securityCheck();target.createBook(book);}public void updateBook(Book book) {securityCheck();target.updateBook(book);}public void deleteBook(Book book) {securityCheck();target.deleteBook(book);}private void securityCheck() {...} }

    這種方式的缺點是比較麻煩,必須先抽取接口,然后,針對每個方法實現(xiàn)Proxy。

    另一種方法是,既然SecurityCheckBookService的代碼都是標準的Proxy樣板代碼,不如把權(quán)限檢查視作一種切面(Aspect),把日志、事務(wù)也視為切面,然后,以某種自動化的方式,把切面織入到核心邏輯中,實現(xiàn)Proxy模式。

    如果我們以AOP的視角來編寫上述業(yè)務(wù),可以依次實現(xiàn):

  • 核心邏輯,即BookService;
  • 切面邏輯,即:
  • 權(quán)限檢查的Aspect;
  • 日志的Aspect;
  • 事務(wù)的Aspect。
  • 然后,以某種方式,讓框架來把上述3個Aspect以Proxy的方式“織入”到BookService中,這樣一來,就不必編寫復(fù)雜而冗長的Proxy模式。

    AOP原理

    如何把切面織入到核心邏輯中?這正是AOP需要解決的問題。換句話說,如果客戶端獲得了BookService的引用,當調(diào)用bookService.createBook()時,如何對調(diào)用方法進行攔截,并在攔截前后進行安全檢查、日志、事務(wù)等處理就相當于完成了所有業(yè)務(wù)功能。

    在Java平臺上,對于AOP的織入,有3種方式:

  • 編譯期:在編譯時,由編譯器把切面調(diào)用編譯進字節(jié)碼,這種方式需要定義新的關(guān)鍵字并擴展編譯器,AspectJ就擴展了Java編譯器,使用關(guān)鍵字aspect來實現(xiàn)織入;
  • 類加載器:在目標類被裝載到JVM時,通過一個特殊的類加載器,對目標類的字節(jié)碼重新“增強”;
  • 運行期:目標對象和切面都是普通Java類,通過JVM的動態(tài)代理功能或者第三方庫實現(xiàn)運行期動態(tài)織入。
  • 最簡單的方式是第三種,Spring的AOP實現(xiàn)就是基于JVM的動態(tài)代理由于JVM的動態(tài)代理要求必須實現(xiàn)接口,如果一個普通類沒有業(yè)務(wù)接口,就需要通過CGLIB或者Javassist這些第三方庫實現(xiàn)。

    AOP技術(shù)看上去比較神秘,但實際上,它本質(zhì)就是一個動態(tài)代理,讓我們把一些常用功能如權(quán)限檢查、日志、事務(wù)等,從每個業(yè)務(wù)方法中剝離出來。

    需要特別指出的是,AOP對于解決特定問題,例如事務(wù)管理非常有用,這是因為分散在各處的事務(wù)代碼幾乎是完全相同的,并且它們需要的參數(shù)(JDBC的Connection)也是固定的。另一些特定問題,如日志,就不那么容易實現(xiàn),因為日志雖然簡單,但打印日志的時候,經(jīng)常需要捕獲局部變量,如果使用AOP實現(xiàn)日志,我們只能輸出固定格式的日志,因此,使用AOP時,必須適合特定的場景。

    3.1裝配AOP

    在AOP編程中,我們經(jīng)常會遇到下面的概念:

    • Aspect:切面,即一個橫跨多個核心邏輯的功能,或者稱之為系統(tǒng)關(guān)注點;
    • Joinpoint:連接點,即定義在應(yīng)用程序流程的何處插入切面的執(zhí)行;
    • Pointcut:切入點,即一組連接點的集合;
    • Advice:增強,指特定連接點上執(zhí)行的動作;
    • Introduction:引介,指為一個已有的Java對象動態(tài)地增加新的接口;
    • Weaving:織入,指將切面整合到程序的執(zhí)行流程中;
    • Interceptor:攔截器,是一種實現(xiàn)增強的方式;
    • Target Object:目標對象,即真正執(zhí)行業(yè)務(wù)的核心邏輯對象;
    • AOP Proxy:AOP代理,是客戶端持有的增強后的對象引用。

    看完上述術(shù)語,是不是感覺對AOP有了進一步的困惑?其實,我們不用關(guān)心AOP創(chuàng)造的“術(shù)語”,只需要理解AOP本質(zhì)上只是一種代理模式的實現(xiàn)方式,在Spring的容器中實現(xiàn)AOP特別方便。

    我們以UserService和MailService為例,這兩個屬于核心業(yè)務(wù)邏輯,現(xiàn)在,我們準備給UserService的每個業(yè)務(wù)方法執(zhí)行前添加日志,給MailService的每個業(yè)務(wù)方法執(zhí)行前后添加日志,在Spring中,需要以下步驟:

    首先,我們通過Maven引入Spring對AOP的支持:

    • org.springframework:spring-aspects:6.0.0

    上述依賴會自動引入AspectJ,使用AspectJ實現(xiàn)AOP比較方便,因為它的定義比較簡單。

    然后,我們定義一個LoggingAspect:

    @Aspect @Component public class LoggingAspect {// 在執(zhí)行UserService的每個方法前執(zhí)行:@Before("execution(public * com.itranswarp.learnjava.service.UserService.*(..))")public void doAccessCheck() {System.err.println("[Before] do access check...");}// 在執(zhí)行MailService的每個方法前后執(zhí)行:@Around("execution(public * com.itranswarp.learnjava.service.MailService.*(..))")public Object doLogging(ProceedingJoinPoint pjp) throws Throwable {System.err.println("[Around] start " + pjp.getSignature());Object retVal = pjp.proceed();System.err.println("[Around] done " + pjp.getSignature());return retVal;} }

    觀察doAccessCheck()方法,我們定義了一個@Before注解,后面的字符串是告訴AspectJ應(yīng)該在何處執(zhí)行該方法,這里寫的意思是:執(zhí)行UserService的每個public方法前執(zhí)行doAccessCheck()代碼。

    再觀察doLogging()方法,我們定義了一個@Around注解,它和@Before不同,@Around可以決定是否執(zhí)行目標方法,因此,我們在doLogging()內(nèi)部先打印日志,再調(diào)用方法,最后打印日志后返回結(jié)果。

    在LoggingAspect類的聲明處,除了用@Component表示它本身也是一個Bean外,我們再加上@Aspect注解,表示它的@Before標注的方法需要注入到UserService的每個public方法執(zhí)行前,@Around標注的方法需要注入到MailService的每個public方法執(zhí)行前后。

    緊接著,我們需要給@Configuration類加上一個@EnableAspectJAutoProxy注解:

    @Configuration @ComponentScan @EnableAspectJAutoProxy public class AppConfig {... }

    Spring的IoC容器看到這個注解,就會自動查找?guī)в?#64;Aspect的Bean,然后根據(jù)每個方法的@Before、@Around等注解把AOP注入到特定的Bean中。執(zhí)行代碼,我們可以看到以下輸出:

    [Before] do access check... [Around] start void com.itranswarp.learnjava.service.MailService.sendRegistrationMail(User) Welcome, test! [Around] done void com.itranswarp.learnjava.service.MailService.sendRegistrationMail(User) [Before] do access check... [Around] start void com.itranswarp.learnjava.service.MailService.sendLoginMail(User) Hi, Bob! You are logged in at 2020-02-14T23:13:52.167996+08:00[Asia/Shanghai] [Around] done void com.itranswarp.learnjava.service.MailService.sendLoginMail(User)

    這說明執(zhí)行業(yè)務(wù)邏輯前后,確實執(zhí)行了我們定義的Aspect(即LoggingAspect的方法)。

    有些童鞋會問,LoggingAspect定義的方法,是如何注入到其他Bean的呢?

    其實AOP的原理非常簡單。我們以LoggingAspect.doAccessCheck()為例,要把它注入到UserService的每個public方法中,最簡單的方法是編寫一個子類,并持有原始實例的引用:

    public UserServiceAopProxy extends UserService {private UserService target;private LoggingAspect aspect;public UserServiceAopProxy(UserService target, LoggingAspect aspect) {this.target = target;this.aspect = aspect;}public User login(String email, String password) {// 先執(zhí)行Aspect的代碼:aspect.doAccessCheck();// 再執(zhí)行UserService的邏輯:return target.login(email, password);}public User register(String email, String password, String name) {aspect.doAccessCheck();return target.register(email, password, name);}... }

    這些都是Spring容器啟動時為我們自動創(chuàng)建的注入了Aspect的子類,它取代了原始的UserService(原始的UserService實例作為內(nèi)部變量隱藏在UserServiceAopProxy中)。如果我們打印從Spring容器獲取的UserService實例類型,它類似UserService$$EnhancerBySpringCGLIB$$1f44e01c,實際上是Spring使用CGLIB動態(tài)創(chuàng)建的子類,但對于調(diào)用方來說,感覺不到任何區(qū)別。

    ?Spring對接口類型使用JDK動態(tài)代理,對普通類使用CGLIB創(chuàng)建子類。如果一個Bean的class是final,Spring將無法為其創(chuàng)建子類。

    可見,雖然Spring容器內(nèi)部實現(xiàn)AOP的邏輯比較復(fù)雜(需要使用AspectJ解析注解,并通過CGLIB實現(xiàn)代理類),但我們使用AOP非常簡單,一共需要三步:

  • 定義執(zhí)行方法,并在方法上通過AspectJ的注解告訴Spring應(yīng)該在何處調(diào)用此方法;
  • 標記@Component和@Aspect;
  • 在@Configuration類上標注@EnableAspectJAutoProxy。
  • 至于AspectJ的注入語法則比較復(fù)雜,請參考Spring文檔。

    Spring也提供其他方法來裝配AOP,但都沒有使用AspectJ注解的方式來得簡潔明了,所以我們不再作介紹。

    攔截器類型

    顧名思義,攔截器有以下類型:

    • @Before:這種攔截器先執(zhí)行攔截代碼,再執(zhí)行目標代碼。如果攔截器拋異常,那么目標代碼就不執(zhí)行了;

    • @After:這種攔截器先執(zhí)行目標代碼,再執(zhí)行攔截器代碼。無論目標代碼是否拋異常,攔截器代碼都會執(zhí)行;

    • @AfterReturning:和@After不同的是,只有當目標代碼正常返回時,才執(zhí)行攔截器代碼;

    • @AfterThrowing:和@After不同的是,只有當目標代碼拋出了異常時,才執(zhí)行攔截器代碼;

    • @Around:能完全控制目標代碼是否執(zhí)行,并可以在執(zhí)行前后、拋異常后執(zhí)行任意攔截代碼,可以說是包含了上面所有功能。

    小結(jié)

    在Spring容器中使用AOP非常簡單,只需要定義執(zhí)行方法,并用AspectJ的注解標注應(yīng)該在何處觸發(fā)并執(zhí)行。

    Spring通過CGLIB動態(tài)創(chuàng)建子類等方式來實現(xiàn)AOP代理模式,大大簡化了代碼。

    3.2使用注解裝配AOP

    上一節(jié)我們講解了使用AspectJ的注解,并配合一個復(fù)雜的execution(* xxx.Xyz.*(..))語法來定義應(yīng)該如何裝配AOP。

    在實際項目中,這種寫法其實很少使用。假設(shè)你寫了一個SecurityAspect:

    @Aspect @Component public class SecurityAspect {@Before("execution(public * com.itranswarp.learnjava.service.*.*(..))")public void check() {if (SecurityContext.getCurrentUser() == null) {throw new RuntimeException("check failed");}} }

    基本能實現(xiàn)無差別全覆蓋,即某個包下面的所有Bean的所有方法都會被這個check()方法攔截。

    還有的童鞋喜歡用方法名前綴進行攔截

    @Around("execution(public * update*(..))") public Object doLogging(ProceedingJoinPoint pjp) throws Throwable {// 對update開頭的方法切換數(shù)據(jù)源:String old = setCurrentDataSource("master");Object retVal = pjp.proceed();restoreCurrentDataSource(old);return retVal; }

    這種非精準打擊誤傷面更大,因為從方法前綴區(qū)分是否是數(shù)據(jù)庫操作是非常不可取的

    我們在使用AOP時,要注意到雖然Spring容器可以把指定的方法通過AOP規(guī)則裝配到指定的Bean的指定方法前后,但是,如果自動裝配時,因為不恰當?shù)姆秶?#xff0c;容易導(dǎo)致意想不到的結(jié)果,即很多不需要AOP代理的Bean也被自動代理了,并且,后續(xù)新增的Bean,如果不清楚現(xiàn)有的AOP裝配規(guī)則,容易被強迫裝配。

    使用AOP時,被裝配的Bean最好自己能清清楚楚地知道自己被安排了。例如,Spring提供的@Transactional就是一個非常好的例子。如果我們自己寫的Bean希望在一個數(shù)據(jù)庫事務(wù)中被調(diào)用,就標注上@Transactional:

    @Component public class UserService {// 有事務(wù):@Transactionalpublic User createUser(String name) {...}// 無事務(wù):public boolean isValidName(String name) {...}// 有事務(wù):@Transactionalpublic void updateUser(User user) {...} }

    或者直接在class級別注解,表示“所有public方法都被安排了”:

    @Component @Transactional public class UserService {... }

    通過@Transactional,某個方法是否啟用了事務(wù)就一清二楚了。因此,裝配AOP的時候,使用注解是最好的方式。

    我們以一個實際例子演示如何使用注解實現(xiàn)AOP裝配。為了監(jiān)控應(yīng)用程序的性能,我們定義一個性能監(jiān)控的注解:

    @Target(METHOD) @Retention(RUNTIME) public @interface MetricTime {String value(); }

    在需要被監(jiān)控的關(guān)鍵方法上標注該注解:

    @Component public class UserService {// 監(jiān)控register()方法性能:@MetricTime("register")public User register(String email, String password, String name) {...}... }

    然后,我們定義MetricAspect:

    @Aspect @Component public class MetricAspect {@Around("@annotation(metricTime)")public Object metric(ProceedingJoinPoint joinPoint, MetricTime metricTime) throws Throwable {String name = metricTime.value();long start = System.currentTimeMillis();try {return joinPoint.proceed();} finally {long t = System.currentTimeMillis() - start;// 寫入日志或發(fā)送至JMX:System.err.println("[Metrics] " + name + ": " + t + "ms");}} }

    注意metric()方法標注了@Around("@annotation(metricTime)"),它的意思是,符合條件的目標方法是帶有@MetricTime注解的方法,因為metric()方法參數(shù)類型是MetricTime(注意參數(shù)名是metricTime不是MetricTime),我們通過它獲取性能監(jiān)控的名稱。

    有了@MetricTime注解,再配合MetricAspect,任何Bean,只要方法標注了@MetricTime注解,就可以自動實現(xiàn)性能監(jiān)控。運行代碼,輸出結(jié)果如下:

    Welcome, Bob! [Metrics] register: 16ms

    3.3AOP避坑指南

    無論是使用AspectJ語法,還是配合Annotation,使用AOP,實際上就是讓Spring自動為我們創(chuàng)建一個Proxy,使得調(diào)用方能無感知地調(diào)用指定方法,但運行期卻動態(tài)“織入”了其他邏輯,因此,AOP本質(zhì)上就是一個代理模式。

    因為Spring使用了CGLIB來實現(xiàn)運行期動態(tài)創(chuàng)建Proxy,如果我們沒能深入理解其運行原理和實現(xiàn)機制,就極有可能遇到各種詭異的問題。

    我們來看一個實際的例子。

    假設(shè)我們定義了一個UserService的Bean:

    @Component public class UserService {// 成員變量:public final ZoneId zoneId = ZoneId.systemDefault();// 構(gòu)造方法:public UserService() {System.out.println("UserService(): init...");System.out.println("UserService(): zoneId = " + this.zoneId);}// public方法:public ZoneId getZoneId() {return zoneId;}// public final方法:public final ZoneId getFinalZoneId() {return zoneId;} }

    再寫個MailService,并注入UserService:

    @Component public class MailService {@AutowiredUserService userService;public String sendMail() {ZoneId zoneId = userService.zoneId;String dt = ZonedDateTime.now(zoneId).toString();return "Hello, it is " + dt;} }

    最后用main()方法測試一下:

    @Configuration @ComponentScan public class AppConfig {public static void main(String[] args) {ApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);MailService mailService = context.getBean(MailService.class);System.out.println(mailService.sendMail());} }

    查看輸出,一切正常:

    UserService(): init... UserService(): zoneId = Asia/Shanghai Hello, it is 2020-04-12T10:23:22.917721+08:00[Asia/Shanghai]

    下一步,我們給UserService加上AOP支持,就添加一個最簡單的LoggingAspect:

    @Aspect @Component public class LoggingAspect {@Before("execution(public * com..*.UserService.*(..))")public void doAccessCheck() {System.err.println("[Before] do access check...");} }

    別忘了在AppConfig上加上@EnableAspectJAutoProxy。再次運行,不出意外的話,會得到一個NullPointerException:

    Exception in thread "main" java.lang.NullPointerException: zoneat java.base/java.util.Objects.requireNonNull(Objects.java:246)at java.base/java.time.Clock.system(Clock.java:203)at java.base/java.time.ZonedDateTime.now(ZonedDateTime.java:216)at com.itranswarp.learnjava.service.MailService.sendMail(MailService.java:19)at com.itranswarp.learnjava.AppConfig.main(AppConfig.java:21)

    仔細跟蹤代碼,會發(fā)現(xiàn)null值出現(xiàn)在MailService.sendMail()內(nèi)部的這一行代碼:

    @Component public class MailService {@AutowiredUserService userService;public String sendMail() {ZoneId zoneId = userService.zoneId;System.out.println(zoneId); // null...} }

    我們還故意在UserService中特意用final修飾了一下成員變量:

    @Component public class UserService {public final ZoneId zoneId = ZoneId.systemDefault();... }

    用final標注的成員變量為null?逗我呢?

    怎么肥四?

    為什么加了AOP就報NPE,去了AOP就一切正常?final字段不執(zhí)行,難道JVM有問題?為了解答這個詭異的問題,我們需要深入理解Spring使用CGLIB生成Proxy的原理:

    第一步,正常創(chuàng)建一個UserService的原始實例,這是通過反射調(diào)用構(gòu)造方法實現(xiàn)的,它的行為和我們預(yù)期的完全一致;

    第二步,通過CGLIB創(chuàng)建一個UserService的子類,并引用了原始實例和LoggingAspect:

    public UserService$$EnhancerBySpringCGLIB extends UserService {UserService target;LoggingAspect aspect;public UserService$$EnhancerBySpringCGLIB() {}public ZoneId getZoneId() {aspect.doAccessCheck();return target.getZoneId();} }

    如果我們觀察Spring創(chuàng)建的AOP代理,它的類名總是類似UserService$$EnhancerBySpringCGLIB$$1c76af9d(你沒看錯,Java的類名實際上允許$字符)。為了讓調(diào)用方獲得UserService的引用,它必須繼承自UserService。然后,該代理類會覆寫所有public和protected方法,并在內(nèi)部將調(diào)用委托給原始的UserService實例。

    這里出現(xiàn)了兩個UserService實例:

    一個是我們代碼中定義的原始實例,它的成員變量已經(jīng)按照我們預(yù)期的方式被初始化完成:

    UserService original = new UserService();

    第二個UserService實例實際上類型是UserService$$EnhancerBySpringCGLIB,它引用了原始的UserService實例:

    UserService$$EnhancerBySpringCGLIB proxy = new UserService$$EnhancerBySpringCGLIB(); proxy.target = original; proxy.aspect = ...

    注意到這種情況僅出現(xiàn)在啟用了AOP的情況,此刻,從ApplicationContext中獲取的UserService實例是proxy,注入到MailService中的UserService實例也是proxy。

    那么最終的問題來了:proxy實例的成員變量,也就是從UserService繼承的zoneId,它的值是null。

    原因在于,UserService成員變量的初始化:

    public class UserService {public final ZoneId zoneId = ZoneId.systemDefault();... }

    在UserService$$EnhancerBySpringCGLIB中,并未執(zhí)行。原因是,沒必要初始化proxy的成員變量,因為proxy的目的是代理方法。

    實際上,成員變量的初始化是在構(gòu)造方法中完成的。這是我們看到的代碼:

    public class UserService {public final ZoneId zoneId = ZoneId.systemDefault();public UserService() {} }

    這是編譯器實際編譯的代碼:

    public class UserService {public final ZoneId zoneId;public UserService() {super(); // 構(gòu)造方法的第一行代碼總是調(diào)用super()zoneId = ZoneId.systemDefault(); // 繼續(xù)初始化成員變量} }

    然而,對于Spring通過CGLIB動態(tài)創(chuàng)建的UserService$$EnhancerBySpringCGLIB代理類,它的構(gòu)造方法中,并未調(diào)用super(),因此,從父類繼承的成員變量,包括final類型的成員變量,統(tǒng)統(tǒng)都沒有初始化。

    有的童鞋會問:Java語言規(guī)定,任何類的構(gòu)造方法,第一行必須調(diào)用super(),如果沒有,編譯器會自動加上,怎么Spring的CGLIB就可以搞特殊?

    這是因為自動加super()的功能是Java編譯器實現(xiàn)的,它發(fā)現(xiàn)你沒加,就自動給加上,發(fā)現(xiàn)你加錯了,就報編譯錯誤。但實際上,如果直接構(gòu)造字節(jié)碼,一個類的構(gòu)造方法中,不一定非要調(diào)用super()。Spring使用CGLIB構(gòu)造的Proxy類,是直接生成字節(jié)碼,并沒有源碼-編譯-字節(jié)碼這個步驟,因此:

    ?Spring通過CGLIB創(chuàng)建的代理類,不會初始化代理類自身繼承的任何成員變量,包括final類型的成員變量!

    再考察MailService的代碼:

    @Component public class MailService {@AutowiredUserService userService;public String sendMail() {ZoneId zoneId = userService.zoneId;System.out.println(zoneId); // null...} }

    如果沒有啟用AOP,注入的是原始的UserService實例,那么一切正常,因為UserService實例的zoneId字段已經(jīng)被正確初始化了。

    如果啟動了AOP,注入的是代理后的UserService$$EnhancerBySpringCGLIB實例,那么問題大了:獲取的UserService$$EnhancerBySpringCGLIB實例的zoneId字段,永遠為null。

    那么問題來了:啟用了AOP,如何修復(fù)?

    修復(fù)很簡單,只需要把直接訪問字段的代碼,改為通過方法訪問

    @Component public class MailService {@AutowiredUserService userService;public String sendMail() {// 不要直接訪問UserService的字段:ZoneId zoneId = userService.getZoneId();...} }

    無論注入的UserService是原始實例還是代理實例,getZoneId()都能正常工作,因為代理類會覆寫getZoneId()方法,并將其委托給原始實例:

    public UserService$$EnhancerBySpringCGLIB extends UserService {UserService target = ......public ZoneId getZoneId() {return target.getZoneId();} }

    注意到我們還給UserService添加了一個public+final的方法:

    @Component public class UserService {...public final ZoneId getFinalZoneId() {return zoneId;} }

    如果在MailService中,調(diào)用的不是getZoneId(),而是getFinalZoneId(),又會出現(xiàn)NullPointerException,這是因為,代理類無法覆寫final方法這一點繞不過JVM的ClassLoader檢查),該方法返回的是代理類的zoneId字段,即null。

    實際上,如果我們加上日志,Spring在啟動時會打印一個警告:

    10:43:09.929 [main] DEBUG org.springframework.aop.framework.CglibAopProxy - Final method [public final java.time.ZoneId xxx.UserService.getFinalZoneId()] cannot get proxied via CGLIB: Calls to this method will NOT be routed to the target instance and might lead to NPEs against uninitialized fields in the proxy instance.

    上面的日志大意就是,因為被代理的UserService有一個final方法getFinalZoneId(),這會導(dǎo)致其他Bean如果調(diào)用此方法,無法將其代理到真正的原始實例,從而可能發(fā)生NPE異常。

    因此,正確使用AOP,我們需要一個避坑指南:

  • 訪問被注入的Bean時,總是調(diào)用方法而非直接訪問字段;
  • 編寫B(tài)ean時,如果可能會被代理,就不要編寫public final方法。
  • 這樣才能保證有沒有AOP,代碼都能正常工作。

    思考

    為什么Spring刻意不初始化Proxy繼承的字段?

    如果一個Bean不允許任何AOP代理,應(yīng)該怎么做來“保護”自己在運行期不會被代理?

    總結(jié)

    以上是生活随笔為你收集整理的lxf-spring开发的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網(wǎng)站內(nèi)容還不錯,歡迎將生活随笔推薦給好友。