Spring 學習,看松哥這一篇萬餘字乾貨就夠了!

westwolf發表於2021-09-09

1. Spring 簡介

我們常說的 Spring 實際上是指 Spring Framework,而 Spring Framework 只是 Spring 家族中的一個分支而已。那麼 Spring 家族都有哪些東西呢?

圖片描述

Spring 是為了解決企業級應用開發的複雜性而建立的。在 Spring 之前,有一個重量級的工具叫做 EJB,使用 Spring 可以讓 Java Bean 之間進行有效的解耦,而這個操作之前只有 EJB 才能完成,EJB 過於臃腫,使用很少。Spring 不僅僅侷限於服務端的開發,在測試性和松耦合方面都有很好的表現。

一般來說,初學者主要掌握 Spring 四個方面的功能:

  • Ioc/DI
  • AOP
  • 事務
  • JdbcTemplate

2. Spring 下載

正常來說,我們在專案中新增 Maven 依賴就可以直接使用 Spring 了,如果需要單獨下載 jar,下載地址如下:

下載成功後,Spring 中的元件,大致上提供瞭如下功能:

圖片描述

3.1 Ioc

3.1.1 Ioc 概念

Ioc (Inversion of Control),中文叫做控制反轉。這是一個概念,也是一種思想。控制反轉,實際上就是指對一個物件的控制權的反轉。例如,如下程式碼:

public class Book {
    private Integer id;
    private String name;
    private Double price;
//省略getter/setter
}
public class User {
    private Integer id;
    private String name;
    private Integer age;

    public void doSth() {
        Book book = new Book();
        book.setId(1);
        book.setName("故事新編");
        book.setPrice((double) 20);
    }
}

在這種情況下,Book 物件的控制權在 User 物件裡邊,這樣,Book 和 User 高度耦合,如果在其他物件中需要使用 Book 物件,得重新建立,也就是說,物件的建立、初始化、銷燬等操作,統統都要開發者自己來完成。如果能夠將這些操作交給容器來管理,開發者就可以極大的從物件的建立中解脫出來。

使用 Spring 之後,我們可以將物件的建立、初始化、銷燬等操作交給 Spring 容器來管理。就是說,在專案啟動時,所有的 Bean 都將自己註冊到 Spring 容器中去(如果有必要的話),然後如果其他 Bean 需要使用到這個 Bean ,則不需要自己去 new,而是直接去 Spring 容器去要。

透過一個簡單的例子看下這個過程。

3.1.2 Ioc 初體驗

首先建立一個普通的 Maven 專案,然後引入 spring-context 依賴,如下:

<dependencies>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-context</artifactId>
        <version>5.1.9.RELEASE</version>
    </dependency>
</dependencies>

接下來,在 resources 目錄下建立一個 spring 的配置檔案(注意,一定要先新增依賴,後建立配置檔案,否則建立配置檔案時,沒有模板選項):

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns=""
       xmlns:xsi=""
       xsi:schemaLocation=" /spring-beans.xsd">

</beans>

在這個檔案中,我們可以配置所有需要註冊到 Spring 容器的 Bean:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns=""
       xmlns:xsi=""
       xsi:schemaLocation=" /spring-beans.xsd">

    <bean class="org.javaboy.Book" id="book"/>
</beans>

class 屬性表示需要註冊的 bean 的全路徑,id 則表示 bean 的唯一標記,也開可以 name 屬性作為 bean 的標記,在超過 99% 的情況下,id 和 name 其實是一樣的,特殊情況下不一樣。

接下來,載入這個配置檔案:

public class Main {
    public static void main(String[] args) {
        ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml");
    }
}

執行 main 方法,配置檔案就會被自動載入,進而在 Spring 中初始化一個 Book 例項。此時,我們顯式的指定 Book 類的無參構造方法,並在無參構造方法中列印日誌,可以看到無參構造方法執行了,進而證明物件已經在 Spring 容器中初始化了。

最後,透過 getBean 方法,可以從容器中去獲取物件:

public class Main {
    public static void main(String[] args) {
        ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml");
        Book book = (Book) ctx.getBean("book");
        System.out.println(book);
    }
}

載入方式,除了ClassPathXmlApplicationContext 之外(去 classpath 下查詢配置檔案),另外也可以使用 FileSystemXmlApplicationContext ,FileSystemXmlApplicationContext 會從作業系統路徑下去尋找配置檔案。

public class Main {
    public static void main(String[] args) {
        FileSystemXmlApplicationContext ctx = new FileSystemXmlApplicationContext("F:\workspace5\workspace\spring\spring-ioc\src\main\resources\applicationContext.xml");
        Book book = (Book) ctx.getBean("book");
        System.out.println(book);
    }
}

3.2 Bean 的獲取

在上一小節中,我們透過 ctx.getBean 方法來從 Spring 容器中獲取 Bean,傳入的引數是 Bean 的 name 或者 id 屬性。除了這種方式之外,也可以直接透過 Class 去獲取一個 Bean。

public class Main {
    public static void main(String[] args) {
        ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml");
        Book book = ctx.getBean(Book.class);
        System.out.println(book);
    }
}

這種方式有一個很大的弊端,如果存在多個例項,這種方式就不可用,例如,xml 檔案中存在兩個 Bean:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns=""
       xmlns:xsi=""
       xsi:schemaLocation=" /spring-beans.xsd">

    <bean class="org.javaboy.Book" id="book"/>
    <bean class="org.javaboy.Book" id="book2"/>
</beans>

此時,如果透過 Class 去查詢 Bean,會報如下錯誤:

圖片描述

所以,一般建議使用 name 或者 id 去獲取 Bean 的例項。

3.3 屬性的注入

在 XML 配置中,屬性的注入存在多種方式。

3.3.1 構造方法注入

透過 Bean 的構造方法給 Bean 的屬性注入值。

1.第一步首先給 Bean 新增對應的構造方法:

public class Book {
    private Integer id;
    private String name;
    private Double price;

    public Book() {
        System.out.println("-------book init----------");
    }

    public Book(Integer id, String name, Double price) {
        this.id = id;
        this.name = name;
        this.price = price;
    }
}

2.在 xml 檔案中注入 Bean

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns=""
       xmlns:xsi=""
       xsi:schemaLocation=" /spring-beans.xsd">

    <bean class="org.javaboy.Book" id="book">
        <constructor-arg index="0" value="1"/>
        <constructor-arg index="1" value="三國演義"/>
        <constructor-arg index="2" value="30"/>
    </bean>
</beans>

這裡需要注意的是,constructor-arg 中的 index 和 Book 中的構造方法引數一一對應。寫的順序可以顛倒,但是 index 的值和 value 要一一對應。

另一種構造方法中的屬性注入,則是透過直接指定引數名來注入:

<bean class="org.javaboy.Book" id="book2">
    <constructor-arg name="id" value="2"/>
    <constructor-arg name="name" value="紅樓夢"/>
    <constructor-arg name="price" value="40"/>
</bean>

如果有多個構造方法,則會根據給出引數個數以及引數型別,自動匹配到對應的構造方法上,進而初始化一個物件。

3.3.2 set 方法注入

除了構造方法之外,我們也可以透過 set 方法注入值。

<bean class="org.javaboy.Book" id="book3">
    <property name="id" value="3"/>
    <property name="name" value="水滸傳"/>
    <property name="price" value="30"/>
</bean>

set 方法注入,有一個很重要的問題,就是屬性名。很多人會有一種錯覺,覺得屬性名就是你定義的屬性名,這個是不對的。在所有的框架中,凡是涉及到反射注入值的,屬性名統統都不是 Bean 中定義的屬性名,而是透過 Java 中的內省機制分析出來的屬性名,簡單說,就是根據 get/set 方法分析出來的屬性名。

3.3.3 p 名稱空間注入

p 名稱空間注入,使用的比較少,它本質上也是呼叫了 set 方法。

<bean class="org.javaboy.Book" id="book4" p:id="4" p:bookName="西遊記" p:price="33"></bean>

3.3.4 外部 Bean 的注入

有時候,我們使用一些外部 Bean,這些 Bean 可能沒有構造方法,而是透過 Builder 來構造的,這個時候,就無法使用上面的方式來給它注入值了。

例如在 OkHttp 的網路請求中,原生的寫法如下:

public class OkHttpMain {
    public static void main(String[] args) {
        OkHttpClient okHttpClient = new OkHttpClient.Builder()
                .build();
        Request request = new Request.Builder()
                .get()
                .url("http://b.hiphotos.baidu.com/image/h%3D300/sign=ad628627aacc7cd9e52d32d909032104/32fa828ba61ea8d3fcd2e9ce9e0a304e241f5803.jpg")
                .build();
        Call call = okHttpClient.newCall(request);
        call.enqueue(new Callback() {
            @Override
            public void onFailure(@NotNull Call call, @NotNull IOException e) {
                System.out.println(e.getMessage());
            }

            @Override
            public void onResponse(@NotNull Call call, @NotNull Response response) throws IOException {
                FileOutputStream out = new FileOutputStream(new File("E:\123.jpg"));
                int len;
                byte[] buf = new byte[1024];
                InputStream is = response.body().byteStream();
                while ((len = is.read(buf)) != -1) {
                    out.write(buf, 0, len);
                }
                out.close();
                is.close();
            }
        });
    }
}

這個 Bean 有一個特點,OkHttpClient 和 Request 兩個例項都不是直接 new 出來的,在呼叫 Builder 方法的過程中,都會給它配置一些預設的引數。這種情況,我們可以使用 靜態工廠注入或者例項工廠注入來給 OkHttpClient 提供一個例項。

1.靜態工廠注入

首先提供一個 OkHttpClient 的靜態工廠:

public class OkHttpUtils {
    private static OkHttpClient OkHttpClient;
    public static OkHttpClient getInstance() {
        if (OkHttpClient == null) {
            OkHttpClient = new OkHttpClient.Builder().build();
        }
        return OkHttpClient;
    }
}

在 xml 檔案中,配置該靜態工廠:

<bean class="org.javaboy.OkHttpUtils" factory-method="getInstance" id="okHttpClient"></bean>

這個配置表示 OkHttpUtils 類中的 getInstance 是我們需要的例項,例項的名字就叫 okHttpClient。然後,在 Java 程式碼中,獲取到這個例項,就可以直接使用了。

public class OkHttpMain {
    public static void main(String[] args) {
        ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml");
        OkHttpClient okHttpClient = ctx.getBean("okHttpClient", OkHttpClient.class);
        Request request = new Request.Builder()
                .get()
                .url("http://b.hiphotos.baidu.com/image/h%3D300/sign=ad628627aacc7cd9e52d32d909032104/32fa828ba61ea8d3fcd2e9ce9e0a304e241f5803.jpg")
                .build();
        Call call = okHttpClient.newCall(request);
        call.enqueue(new Callback() {
            @Override
            public void onFailure(@NotNull Call call, @NotNull IOException e) {
                System.out.println(e.getMessage());
            }

            @Override
            public void onResponse(@NotNull Call call, @NotNull Response response) throws IOException {
                FileOutputStream out = new FileOutputStream(new File("E:\123.jpg"));
                int len;
                byte[] buf = new byte[1024];
                InputStream is = response.body().byteStream();
                while ((len = is.read(buf)) != -1) {
                    out.write(buf, 0, len);
                }
                out.close();
                is.close();
            }
        });
    }
}

2.例項工廠注入

例項工廠就是工廠方法是一個例項方法,這樣,工廠類必須例項化之後才可以呼叫工廠方法。

這次的工廠類如下:

public class OkHttpUtils {
    private OkHttpClient OkHttpClient;
    public OkHttpClient getInstance() {
        if (OkHttpClient == null) {
            OkHttpClient = new OkHttpClient.Builder().build();
        }
        return OkHttpClient;
    }
}

此時,在 xml 檔案中,需要首先提供工廠方法的例項,然後才可以呼叫工廠方法:

<bean class="org.javaboy.OkHttpUtils" id="okHttpUtils"/>
<bean class="okhttp3.OkHttpClient" factory-bean="okHttpUtils" factory-method="getInstance" id="okHttpClient"></bean>

自己寫的 Bean 一般不會使用這兩種方式注入,但是,如果需要引入外部 jar,外部 jar 的類的初始化,有可能需要使用這兩種方式。

3.4 複雜屬性的注入

3.4.1 物件注入

<bean class="org.javaboy.User" id="user">
    <property name="cat" ref="cat"/>
</bean>
<bean class="org.javaboy.Cat" id="cat">
    <property name="name" value="小白"/>
    <property name="color" value="白色"/>
</bean>

可以透過 xml 注入物件,透過 ref 來引用一個物件。

3.4.2 陣列注入

陣列注入和集合注入在 xml 中的配置是一樣的。如下:

<bean class="org.javaboy.User" id="user">
    <property name="cat" ref="cat"/>
    <property name="favorites">
        <array>
            <value>足球</value>
            <value>籃球</value>
            <value>乒乓球</value>
        </array>
    </property>
</bean>
<bean class="org.javaboy.Cat" id="cat">
    <property name="name" value="小白"/>
    <property name="color" value="白色"/>
</bean>

注意,array 節點,也可以被 list 節點代替。

當然,array 或者 list 節點中也可以是物件。

<bean class="org.javaboy.User" id="user">
    <property name="cat" ref="cat"/>
    <property name="favorites">
        <list>
            <value>足球</value>
            <value>籃球</value>
            <value>乒乓球</value>
        </list>
    </property>
    <property name="cats">
        <list>
            <ref bean="cat"/>
            <ref bean="cat2"/>
            <bean class="org.javaboy.Cat" id="cat3">
                <property name="name" value="小花"/>
                <property name="color" value="花色"/>
            </bean>
        </list>
    </property>
</bean>
<bean class="org.javaboy.Cat" id="cat">
    <property name="name" value="小白"/>
    <property name="color" value="白色"/>
</bean>
<bean class="org.javaboy.Cat" id="cat2">
    <property name="name" value="小黑"/>
    <property name="color" value="黑色"/>
</bean>

注意,即可以透過 ref 使用外部定義好的 Bean,也可以直接在 list 或者 array 節點中定義 bean。

3.4.3 Map 注入

<property name="map">
    <map>
        <entry key="age" value="99"/>
        <entry key="name" value="javaboy"/>
    </map>
</property>

3.4.4 Properties 注入

<property name="info">
    <props>
        <prop key="age">99</prop>
        <prop key="name">javaboy</prop>
    </props>
</property>

以上 Demo,定義的 User 如下:

public class User {
    private Integer id;
    private String name;
    private Integer age;
    private Cat cat;
    private String[] favorites;
    private List<Cat> cats;
    private Map<String,Object> map;
    private Properties info;

    @Override
    public String toString() {
        return "User{" +
                "id=" + id +
                ", name='" + name + ''' +
                ", age=" + age +
                ", cat=" + cat +
                ", favorites=" + Arrays.toString(favorites) +
                ", cats=" + cats +
                ", map=" + map +
                ", info=" + info +
                '}';
    }

    public Properties getInfo() {
        return info;
    }

    public void setInfo(Properties info) {
        this.info = info;
    }

    public Map<String, Object> getMap() {
        return map;
    }

    public void setMap(Map<String, Object> map) {
        this.map = map;
    }

    public List<Cat> getCats() {
        return cats;
    }

    public void setCats(List<Cat> cats) {
        this.cats = cats;
    }

    public String[] getFavorites() {
        return favorites;
    }

    public void setFavorites(String[] favorites) {
        this.favorites = favorites;
    }

    public User() {
    }

    public User(Integer id, String name, Integer age, Cat cat) {
        this.id = id;
        this.name = name;
        this.age = age;
        this.cat = cat;
    }

    public Cat getCat() {
        return cat;
    }

    public void setCat(Cat cat) {
        this.cat = cat;
    }

    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Integer getAge() {
        return age;
    }

    public void setAge(Integer age) {
        this.age = age;
    }
}

3.5 Java 配置

在 Spring 中,想要將一個 Bean 註冊到 Spring 容器中,整體上來說,有三種不同的方式。

  • XML 注入,如前文所說
  • Java 配置(透過 Java 程式碼將 Bean 註冊到 Spring 容器中)
  • 自動化掃描

這裡我們來看 Java 配置。

Java 配置這種方式在 Spring Boot 出現之前,其實很少使用,自從有了 Spring Boot,Java 配置開發被廣泛使用,因為在 Spring Boot 中,不使用一行 XML 配置。

例如我有如下一個 Bean:

public class SayHello {
    public String sayHello(String name) {
        return "hello " + name;
    }
}

在 Java 配置中,我們用一個 Java 配置類去代替之前的 applicationContext.xml 檔案。

@Configuration
public class JavaConfig {
    @Bean
    SayHello sayHello() {
        return new SayHello();
    }
}

首先在配置類上有一個 @Configuration 註解,這個註解表示這個類不是一個普通類,而是一個配置類,它的作用相當於 applicationContext.xml。
然後,定義方法,方法返回物件,方法上新增 @Bean 註解,表示將這個方法的返回值注入的 Spring 容器中去。也就是說,@Bean 所對應的方法,就相當於 applicationContext.xml 中的 bean 節點。

既然是配置類,我們需要在專案啟動時載入配置類。

public class Main {
    public static void main(String[] args) {
        AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(JavaConfig.class);
        SayHello hello = ctx.getBean(SayHello.class);
        System.out.println(hello.sayHello("javaboy"));
    }
}

注意,配置的載入,是使用 AnnotationConfigApplicationContext 來實現。

關於 Java 配置,這裡有一個需要注意的問題:Bean 的名字是什麼?

Bean 的預設名稱是方法名。以上面的案例為例,Bean 的名字是 sayHello。
如果開發者想自定義方法名,也是可以的,直接在 @Bean 註解中進行過配置。如下配置表示修改 Bean 的名字為 javaboy:

@Configuration
public class JavaConfig {
    @Bean("javaboy")
    SayHello sayHello() {
        return new SayHello();
    }
}

3.6 自動化配置

在我們實際開發中,大量的使用自動配置。

自動化配置既可以透過 Java 配置來實現,也可以透過 xml 配置來實現。

3.6.1 準備工作

例如我有一個 UserService,我希望在自動化掃描時,這個類能夠自動註冊到 Spring 容器中去,那麼可以給該類新增一個 @Service,作為一個標記。

和 @Service 註解功能類似的註解,一共有四個:

  • @Component
  • @Repository
  • @Service
  • @Controller

這四個中,另外三個都是基於 @Component 做出來的,而且從目前的原始碼來看,功能也是一致的,那麼為什麼要搞三個呢?主要是為了在不同的類上面新增時方便。

  • 在 Service 層上,新增註解時,使用 @Service
  • 在 Dao 層,新增註解時,使用 @Repository
  • 在 Controller 層,新增註解時,使用 @Controller
  • 在其他元件上新增註解時,使用 @Component
@Service
public class UserService {
    public List<String> getAllUser() {
        List<String> users = new ArrayList<>();
        for (int i = 0; i < 10; i++) {
            users.add("javaboy:" + i);
        }
        return users;
    }
}

新增完成後,自動化掃描有兩種方式,一種就是透過 Java 程式碼配置自動化掃描,另一種則是透過 xml 檔案來配置自動化掃描。

3.6.2 Java 程式碼配置自動掃描

@Configuration
@ComponentScan(basePackages = "org.javaboy.javaconfig.service")
public class JavaConfig {
}

然後,在專案啟動中載入配置類,在配置類中,透過 @ComponentScan 註解指定要掃描的包(如果不指定,預設情況下掃描的是配置類所在的包下載的 Bean 以及配置類所在的包下的子包下的類),然後就可以獲取 UserService 的例項了:

public class Main {
    public static void main(String[] args) {
        AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(JavaConfig.class);
        UserService userService = ctx.getBean(UserService.class);
        System.out.println(userService.getAllUser());
    }
}

這裡有幾個問題需要注意:

1.Bean 的名字叫什麼?

預設情況下,Bean 的名字是類名首字母小寫。例如上面的 UserService,它的例項名,預設就是 userService。如果開發者想要自定義名字,就直接在 @Service 註解中新增即可。

2.有幾種掃描方式?

上面的配置,我們是按照包的位置來掃描的。也就是說,Bean 必須放在指定的掃描位置,否則,即使你有 @Service 註解,也掃描不到。

除了按照包的位置來掃描,還有另外一種方式,就是根據註解來掃描。例如如下配置:

@Configuration
@ComponentScan(basePackages = "org.javaboy.javaconfig",useDefaultFilters = true,excludeFilters = {@ComponentScan.Filter(type = FilterType.ANNOTATION,classes = Controller.class)})
public class JavaConfig {
}

這個配置表示掃描 org.javaboy.javaconfig 下的所有 Bean,但是除了 Controller。

3.6.3 XML 配置自動化掃描

<context:component-scan base-package="org.javaboy.javaconfig"/>

上面這行配置表示掃描 org.javaboy.javaconfig 下的所有 Bean。當然也可以按照類來掃描。

XML 配置完成後,在 Java 程式碼中載入 XML 配置即可。

public class XMLTest {
    public static void main(String[] args) {
        ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml");
        UserService userService = ctx.getBean(UserService.class);
        List<String> list = userService.getAllUser();
        System.out.println(list);
    }
}

也可以在 XML 配置中按照註解的型別進行掃描:

<context:component-scan base-package="org.javaboy.javaconfig" use-default-filters="true">
    <context:exclude-filter type="annotation" expression="org.springframework.stereotype.Controller"/>
</context:component-scan>

3.6.4 物件注入

自動掃描時的物件注入有三種方式:

  1. @Autowired
  2. @Resources
  3. @Injected

@Autowired 是根據型別去查詢,然後賦值,這就有一個要求,這個型別只可以有一個物件,否則就會報錯。@Resources 是根據名稱去查詢,預設情況下,定義的變數名,就是查詢的名稱,當然開發者也可以在 @Resources 註解中手動指定。所以,如果一個類存在多個例項,那麼就應該使用 @Resources 去注入,如果非常使用 @Autowired,也是可以的,此時需要配合另外一個註解,@Qualifier,在 @Qualifier 中可以指定變數名,兩個一起用(@Qualifier 和 @Autowired)就可以實現透過變數名查詢到變數。

@Service
public class UserService {

    @Autowired
    UserDao userDao;
    public String hello() {
        return userDao.hello();
    }

    public List<String> getAllUser() {
        List<String> users = new ArrayList<>();
        for (int i = 0; i < 10; i++) {
            users.add("javaboy:" + i);
        }
        return users;
    }
}

3.7 條件註解

條件註解就是在滿足某一個條件的情況下,生效的配置。

3.7.1 條件註解

首先在 Windows 中如何獲取作業系統資訊?Windows 中檢視資料夾目錄的命令是 dir,Linux 中檢視資料夾目錄的命令是 ls,我現在希望當系統執行在 Windows 上時,自動列印出 Windows 上的目錄展示命令,Linux 執行時,則自動展示 Linux 上的目錄展示命令。

首先定義一個顯示資料夾目錄的介面:

public interface ShowCmd {
    String showCmd();
}

然後,分別實現 Windows 下的例項和 Linux 下的例項:

public class WinShowCmd implements ShowCmd {
    @Override
    public String showCmd() {
        return "dir";
    }
}
public class LinuxShowCmd implements ShowCmd {
    @Override
    public String showCmd() {
        return "ls";
    }
}

接下來,定義兩個條件,一個是 Windows 下的條件,另一個是 Linux 下的條件。

public class WindowsCondition implements Condition {
    @Override
    public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
        return context.getEnvironment().getProperty("os.name").toLowerCase().contains("windows");
    }
}
public class LinuxCondition implements Condition {
    @Override
    public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
        return context.getEnvironment().getProperty("os.name").toLowerCase().contains("linux");
    }
}

接下來,在定義 Bean 的時候,就可以去配置條件註解了。

@Configuration
public class JavaConfig {
    @Bean("showCmd")
    @Conditional(WindowsCondition.class)
    ShowCmd winCmd() {
        return new WinShowCmd();
    }

    @Bean("showCmd")
    @Conditional(LinuxCondition.class)
    ShowCmd linuxCmd() {
        return new LinuxShowCmd();
    }
}

這裡,一定要給兩個 Bean 取相同的名字,這樣在呼叫時,才可以自動匹配。然後,給每一個 Bean 加上條件註解,當條件中的 matches 方法返回 true 的時候,這個 Bean 的定義就會生效。

public class JavaMain {
    public static void main(String[] args) {
        AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(JavaConfig.class);
        ShowCmd showCmd = (ShowCmd) ctx.getBean("showCmd");
        System.out.println(showCmd.showCmd());
    }
}

條件註解有一個非常典型的使用場景,就是多環境切換。

3.7.2 多環境切換

開發中,如何在 開發/生產/測試 環境之間進行快速切換?Spring 中提供了 Profile 來解決這個問題,Profile 的底層就是條件註解。這個從 @Profile 註解的定義就可以看出來:

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Conditional(ProfileCondition.class)
public @interface Profile {

	/**
	 * The set of profiles for which the annotated component should be registered.
	 */
	String[] value();

}
class ProfileCondition implements Condition {

	@Override
	public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
		MultiValueMap<String, Object> attrs = metadata.getAllAnnotationAttributes(Profile.class.getName());
		if (attrs != null) {
			for (Object value : attrs.get("value")) {
				if (context.getEnvironment().acceptsProfiles(Profiles.of((String[]) value))) {
					return true;
				}
			}
			return false;
		}
		return true;
	}

}

我們定義一個 DataSource:

public class DataSource {
    private String url;
    private String username;
    private String password;

    @Override
    public String toString() {
        return "DataSource{" +
                "url='" + url + ''' +
                ", username='" + username + ''' +
                ", password='" + password + ''' +
                '}';
    }

    public String getUrl() {
        return url;
    }

    public void setUrl(String url) {
        this.url = url;
    }

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }
}

然後,在配置 Bean 時,透過 @Profile 註解指定不同的環境:

@Bean("ds")
@Profile("dev")
DataSource devDataSource() {
    DataSource dataSource = new DataSource();
    dataSource.setUrl("jdbc:mysql://127.0.0.1:3306/dev");
    dataSource.setUsername("root");
    dataSource.setPassword("123");
    return dataSource;
}
@Bean("ds")
@Profile("prod")
DataSource prodDataSource() {
    DataSource dataSource = new DataSource();
    dataSource.setUrl("jdbc:mysql://192.158.222.33:3306/dev");
    dataSource.setUsername("jkldasjfkl");
    dataSource.setPassword("jfsdjflkajkld");
    return dataSource;
}

最後,在載入配置類,注意,需要先設定當前環境,然後再去載入配置類:

public class JavaMain {
    public static void main(String[] args) {
        AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();
        ctx.getEnvironment().setActiveProfiles("dev");
        ctx.register(JavaConfig.class);
        ctx.refresh();
        DataSource ds = (DataSource) ctx.getBean("ds");
        System.out.println(ds);
    }
}

這個是在 Java 程式碼中配置的。環境的切換,也可以在 XML 檔案中配置,如下配置在 XML 檔案中,必須放在其他節點後面。

<beans profile="dev">
    <bean class="org.javaboy.DataSource" id="dataSource">
        <property name="url" value="jdbc:mysql:///devdb"/>
        <property name="password" value="root"/>
        <property name="username" value="root"/>
    </bean>
</beans>
<beans profile="prod">
    <bean class="org.javaboy.DataSource" id="dataSource">
        <property name="url" value="jdbc:mysql://111.111.111.111/devdb"/>
        <property name="password" value="jsdfaklfj789345fjsd"/>
        <property name="username" value="root"/>
    </bean>
</beans>

啟動類中設定當前環境並載入配置:

public class Main {
    public static void main(String[] args) {
        ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext();
        ctx.getEnvironment().setActiveProfiles("prod");
        ctx.setConfigLocation("applicationContext.xml");
        ctx.refresh();
        DataSource dataSource = (DataSource) ctx.getBean("dataSource");
        System.out.println(dataSource);
    }
}

3.8 其他

3.8.1 Bean 的作用域

在 XML 配置中註冊的 Bean,或者用 Java 配置註冊的 Bean,如果我多次獲取,獲取到的物件是否是同一個?

public class Main {
    public static void main(String[] args) {
        ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml");
        User user = ctx.getBean("user", User.class);
        User user2 = ctx.getBean("user", User.class);
        System.out.println(user==user2);
    }
}

如上,從 Spring 容器中多次獲取同一個 Bean,預設情況下,獲取到的實際上是同一個例項。當然我們可以自己手動配置。

<bean class="org.javaboy.User" id="user" scope="prototype" />

透過在 XML 節點中,設定 scope 屬性,我們可以調整預設的例項個數。scope 的值為 singleton(預設),表示這個 Bean 在 Spring 容器中,是以單例的形式存在,如果 scope 的值為 prototype,表示這個 Bean 在 Spring 容器中不是單例,多次獲取將拿到多個不同的例項。

除了 singleton 和 prototype 之外,還有兩個取值,request 和 session。這兩個取值在 web 環境下有效。這是在 XML 中的配置,我們也可以在 Java 中配置。

@Configuration
public class JavaConfig {
    @Bean
    @Scope("prototype")
    SayHello sayHello() {
        return new SayHello();
    }
}

在 Java 程式碼中,我們可以透過 @Scope 註解指定 Bean 的作用域。

當然,在自動掃描配置中,也可以指定 Bean 的作用域。

@Repository
@Scope("prototype")
public class UserDao {
    public String hello() {
        return "userdao";
    }
}

3.8.2 id 和 name 的區別

在 XML 配置中,我們可以看到,即可以透過 id 給 Bean 指定一個唯一識別符號,也可以透過 name 來指定,大部分情況下這兩個作用是一樣的,有一個小小區別:

name 支援取多個。多個 name 之間,用 , 隔開:

<bean class="org.javaboy.User" name="user,user1,user2,user3" scope="prototype"/>

此時,透過 user、user1、user2、user3 都可以獲取到當前物件:

public class Main {
    public static void main(String[] args) {
        ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml");
        User user = ctx.getBean("user", User.class);
        User user2 = ctx.getBean("user2", User.class);
        System.out.println(user);
        System.out.println(user2);
    }
}

而 id 不支援有多個值。如果強行用 , 隔開,它還是一個值。例如如下配置:

<bean class="org.javaboy.User" id="user,user1,user2,user3" scope="prototype" />

這個配置表示 Bean 的名字為 user,user1,user2,user3,具體呼叫如下:

public class Main {
    public static void main(String[] args) {
        ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml");
        User user = ctx.getBean("user,user1,user2,user3", User.class);
        User user2 = ctx.getBean("user,user1,user2,user3", User.class);
        System.out.println(user);
        System.out.println(user2);
    }
}

3.8.3 混合配置

混合配置就是 Java 配置+XML 配置。混用的話,可以在 Java 配置中引入 XML 配置。

@Configuration
@ImportResource("classpath:applicationContext.xml")
public class JavaConfig {
}

在 Java 配置中,透過 @ImportResource 註解可以匯入一個 XML 配置。

4. Aware 介面

Aware 介面,從字面上理解就是感知捕獲。單純的一個 Bean 是沒有知覺的。

在 3.6.4 節的場景中,之所以 UserDao 能夠注入到 UserService ,有一個前提,就是它兩個都是被 Spring 容器管理的。如果直接 new 一個 UserService,這是沒用的,因為 UserService 沒有被 Spring 容器管理,所以也不會給它裡邊注入 Bean。

在實際開發中,我們可能會遇到一些類,需要獲取到容器的詳細資訊,那就可以透過 Aware 介面來實現。

Aware 是一個空介面,有很多實現類:

圖片描述

這些實現的介面,有一些公共特性:

  1. 都是以 Aware 結尾
  2. 都繼承自 Aware
  3. 介面內均定義了一個 set 方法

每一個子介面均提供了一個 set 方法,方法的引數就是當前 Bean 需要感知的內容,因此我們需要在 Bean 中宣告相關的成員變數來接受這個引數。接收到這個引數後,就可以透過這個引數獲取到容器的詳細資訊了。

@Component
public class SayHello implements ApplicationContextAware {
    private ApplicationContext applicationContext;
    public String sayHello(String name) {
        //判斷容器中是否存在某個 Bean
        boolean userDao = applicationContext.containsBean("userDao333");
        System.out.println(userDao);
        return "hello " + name;
    }
    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.applicationContext = applicationContext;
    }
}

5.1 Aop

Aop(Aspect Oriented Programming),面向切面程式設計,這是對物件導向思想的一種補充。

面向切面程式設計,就是在程式執行時,不改變程式原始碼的情況下,動態的增強方法的功能,常見的使用場景非常多:

  1. 日誌
  2. 事務
  3. 資料庫操作

這些操作中,無一例外,都有很多模板化的程式碼,而解決模板化程式碼,消除臃腫就是 Aop 的強項。

在 Aop 中,有幾個常見的概念:

概念 說明
切點 要新增程式碼的地方,稱作切點
通知(增強) 通知就是向切點動態新增的程式碼
切面 切點+通知
連線點 切點的定義

5.1.1 Aop 的實現

在 Aop 實際上集基於 Java 動態代理來實現的。

Java 中的動態代理有兩種實現方式:

  • cglib
  • jdk

5.2 動態代理

基於 JDK 的動態代理。

1.定義一個計算器介面:

public interface MyCalculator {
    int add(int a, int b);
}

2.定義計算機介面的實現:

public class MyCalculatorImpl implements MyCalculator {
    public int add(int a, int b) {
        return a+b;
    }
}

3.定義代理類

public class CalculatorProxy {
    public static Object getInstance(final MyCalculatorImpl myCalculator) {
        return Proxy.newProxyInstance(CalculatorProxy.class.getClassLoader(), myCalculator.getClass().getInterfaces(), new InvocationHandler() {
            /**
             * @param proxy 代理物件
             * @param method 代理的方法
             * @param args 方法的引數
             * @return
             * @throws Throwable
             */
            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                System.out.println(method.getName()+"方法開始執行啦...");
                Object invoke = method.invoke(myCalculator, args);
                System.out.println(method.getName()+"方法執行結束啦...");
                return invoke;
            }
        });
    }
}

Proxy.newProxyInstance 方法接收三個引數,第一個是一個 classloader,第二個是代理多項實現的介面,第三個是代理物件方法的處理器,所有要額外新增的行為都在 invoke 方法中實現。

5.3 五種通知

Spring 中的 Aop 的通知型別有 5 種:

  • 前置通知
  • 後置通知
  • 異常通知
  • 返回通知
  • 環繞通知

具體實現,這裡的案例和 5.2 中的一樣,依然是給計算器的方法增強功能。

首先,在專案中,引入 Spring 依賴(這次需要引入 Aop 相關的依賴):

<dependencies>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-context</artifactId>
        <version>5.1.9.RELEASE</version>
    </dependency>
    <dependency>
        <groupId>org.aspectj</groupId>
        <artifactId>aspectjweaver</artifactId>
        <version>1.9.5</version>
    </dependency>
    <dependency>
        <groupId>org.aspectj</groupId>
        <artifactId>aspectjrt</artifactId>
        <version>1.9.5</version>
    </dependency>
</dependencies>

接下來,定義切點,這裡介紹兩種切點的定義方式:

  • 使用自定義註解
  • 使用規則

其中,使用自定義註解標記切點,是侵入式的,所以這種方式在實際開發中不推薦,僅作為了解,另一種使用規則來定義切點的方式,無侵入,一般推薦使用這種方式。

自定義註解

首先自定義一個註解:

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Action {
}

然後在需要攔截的方法上,新增該註解,在 add 方法上新增了 @Action 註解,表示該方法將會被 Aop 攔截,而其他未新增該註解的方法則不受影響。

@Component
public class MyCalculatorImpl {
    @Action
    public int add(int a, int b) {
        return a + b;
    }

    public void min(int a, int b) {
        System.out.println(a + "-" + b + "=" + (a - b));
    }
}

接下來,定義增強(通知、Advice):

@Component
@Aspect//表示這是一個切面
public class LogAspect {

    /**
     * @param joinPoint 包含了目標方法的關鍵資訊
     * @Before 註解表示這是一個前置通知,即在目標方法執行之前執行,註解中,需要填入切點
     */
    @Before(value = "@annotation(Action)")
    public void before(JoinPoint joinPoint) {
        Signature signature = joinPoint.getSignature();
        String name = signature.getName();
        System.out.println(name + "方法開始執行了...");
    }

    /**
     * 後置通知
     * @param joinPoint 包含了目標方法的所有關鍵資訊
     * @After 表示這是一個後置通知,即在目標方法執行之後執行
     */
    @After("@annotation(Action)")
    public void after(JoinPoint joinPoint) {
        Signature signature = joinPoint.getSignature();
        String name = signature.getName();
        System.out.println(name + "方法執行結束了...");
    }

    /**
     * @param joinPoint
     * @@AfterReturning 表示這是一個返回通知,即有目標方法有返回值的時候才會觸發,該註解中的 returning 屬性表示目標方法返回值的變數名,這個需要和引數一一對應嗎,注意:目標方法的返回值型別要和這裡方法返回值引數的型別一致,否則攔截不到,如果想攔截所有(包括返回值為 void),則方法返回值引數可以為 Object
     */
    @AfterReturning(value = "@annotation(Action)",returning = "r")
    public void returing(JoinPoint joinPoint,Integer r) {
        Signature signature = joinPoint.getSignature();
        String name = signature.getName();
        System.out.println(name + "方法返回:"+r);
    }

    /**
     * 異常通知
     * @param joinPoint
     * @param e 目標方法所丟擲的異常,注意,這個引數必須是目標方法所丟擲的異常或者所丟擲的異常的父類,只有這樣,才會捕獲。如果想攔截所有,引數型別宣告為 Exception
     */
    @AfterThrowing(value = "@annotation(Action)",throwing = "e")
    public void afterThrowing(JoinPoint joinPoint,Exception e) {
        Signature signature = joinPoint.getSignature();
        String name = signature.getName();
        System.out.println(name + "方法拋異常了:"+e.getMessage());
    }

    /**
     * 環繞通知
     *
     * 環繞通知是集大成者,可以用環繞通知實現上面的四個通知,這個方法的核心有點類似於在這裡透過反射執行方法
     * @param pjp
     * @return 注意這裡的返回值型別最好是 Object ,和攔截到的方法相匹配
     */
    @Around("@annotation(Action)")
    public Object around(ProceedingJoinPoint pjp) {
        Object proceed = null;
        try {
            //這個相當於 method.invoke 方法,我們可以在這個方法的前後分別新增日誌,就相當於是前置/後置通知
            proceed = pjp.proceed();
        } catch (Throwable throwable) {
            throwable.printStackTrace();
        }
        return proceed;
    }
}

通知定義完成後,接下來在配置類中,開啟包掃描和自動代理:

@Configuration
@ComponentScan
@EnableAspectJAutoProxy//開啟自動代理
public class JavaConfig {
}

然後,在 Main 方法中,開啟呼叫:

public class Main {
    public static void main(String[] args) {
        AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(JavaConfig.class);
        MyCalculatorImpl myCalculator = ctx.getBean(MyCalculatorImpl.class);
        myCalculator.add(3, 4);
        myCalculator.min(3, 4);
    }
}

再來回顧 LogAspect 切面,我們發現,切點的定義不夠靈活,之前的切點是直接寫在註解裡邊的,這樣,如果要修改切點,每個方法上都要修改,因此,我們可以將切點統一定義,然後統一呼叫。

@Component
@Aspect//表示這是一個切面
public class LogAspect {

    /**
     * 可以統一定義切點
     */
    @Pointcut("@annotation(Action)")
    public void pointcut() {

    }

    /**
     * @param joinPoint 包含了目標方法的關鍵資訊
     * @Before 註解表示這是一個前置通知,即在目標方法執行之前執行,註解中,需要填入切點
     */
    @Before(value = "pointcut()")
    public void before(JoinPoint joinPoint) {
        Signature signature = joinPoint.getSignature();
        String name = signature.getName();
        System.out.println(name + "方法開始執行了...");
    }

    /**
     * 後置通知
     * @param joinPoint 包含了目標方法的所有關鍵資訊
     * @After 表示這是一個後置通知,即在目標方法執行之後執行
     */
    @After("pointcut()")
    public void after(JoinPoint joinPoint) {
        Signature signature = joinPoint.getSignature();
        String name = signature.getName();
        System.out.println(name + "方法執行結束了...");
    }

    /**
     * @param joinPoint
     * @@AfterReturning 表示這是一個返回通知,即有目標方法有返回值的時候才會觸發,該註解中的 returning 屬性表示目標方法返回值的變數名,這個需要和引數一一對應嗎,注意:目標方法的返回值型別要和這裡方法返回值引數的型別一致,否則攔截不到,如果想攔截所有(包括返回值為 void),則方法返回值引數可以為 Object
     */
    @AfterReturning(value = "pointcut()",returning = "r")
    public void returing(JoinPoint joinPoint,Integer r) {
        Signature signature = joinPoint.getSignature();
        String name = signature.getName();
        System.out.println(name + "方法返回:"+r);
    }

    /**
     * 異常通知
     * @param joinPoint
     * @param e 目標方法所丟擲的異常,注意,這個引數必須是目標方法所丟擲的異常或者所丟擲的異常的父類,只有這樣,才會捕獲。如果想攔截所有,引數型別宣告為 Exception
     */
    @AfterThrowing(value = "pointcut()",throwing = "e")
    public void afterThrowing(JoinPoint joinPoint,Exception e) {
        Signature signature = joinPoint.getSignature();
        String name = signature.getName();
        System.out.println(name + "方法拋異常了:"+e.getMessage());
    }

    /**
     * 環繞通知
     *
     * 環繞通知是集大成者,可以用環繞通知實現上面的四個通知,這個方法的核心有點類似於在這裡透過反射執行方法
     * @param pjp
     * @return 注意這裡的返回值型別最好是 Object ,和攔截到的方法相匹配
     */
    @Around("pointcut()")
    public Object around(ProceedingJoinPoint pjp) {
        Object proceed = null;
        try {
            //這個相當於 method.invoke 方法,我們可以在這個方法的前後分別新增日誌,就相當於是前置/後置通知
            proceed = pjp.proceed();
        } catch (Throwable throwable) {
            throwable.printStackTrace();
        }
        return proceed;
    }
}

但是,大家也注意到,使用註解是侵入式的,我們還可以繼續最佳化,改為非侵入式的。重新定義切點,新切點的定義就不在需要 @Action 註解了,要攔截的目標方法上也不用新增 @Action 註解。下面這種方式是更為通用的攔截方式:

@Component
@Aspect//表示這是一個切面
public class LogAspect {

    /**
     * 可以統一定義切點
     */
    @Pointcut("@annotation(Action)")
    public void pointcut2() {

    }
    /**
     * 可以統一定義切點
     * 第一個 * 表示要攔截的目標方法返回值任意(也可以明確指定返回值型別
     * 第二個 * 表示包中的任意類(也可以明確指定類
     * 第三個 * 表示類中的任意方法
     * 最後面的兩個點表示方法引數任意,個數任意,型別任意
     */
    @Pointcut("execution(* org.javaboy.aop.commons.*.*(..))")
    public void pointcut() {

    }

    /**
     * @param joinPoint 包含了目標方法的關鍵資訊
     * @Before 註解表示這是一個前置通知,即在目標方法執行之前執行,註解中,需要填入切點
     */
    @Before(value = "pointcut()")
    public void before(JoinPoint joinPoint) {
        Signature signature = joinPoint.getSignature();
        String name = signature.getName();
        System.out.println(name + "方法開始執行了...");
    }

    /**
     * 後置通知
     * @param joinPoint 包含了目標方法的所有關鍵資訊
     * @After 表示這是一個後置通知,即在目標方法執行之後執行
     */
    @After("pointcut()")
    public void after(JoinPoint joinPoint) {
        Signature signature = joinPoint.getSignature();
        String name = signature.getName();
        System.out.println(name + "方法執行結束了...");
    }

    /**
     * @param joinPoint
     * @@AfterReturning 表示這是一個返回通知,即有目標方法有返回值的時候才會觸發,該註解中的 returning 屬性表示目標方法返回值的變數名,這個需要和引數一一對應嗎,注意:目標方法的返回值型別要和這裡方法返回值引數的型別一致,否則攔截不到,如果想攔截所有(包括返回值為 void),則方法返回值引數可以為 Object
     */
    @AfterReturning(value = "pointcut()",returning = "r")
    public void returing(JoinPoint joinPoint,Integer r) {
        Signature signature = joinPoint.getSignature();
        String name = signature.getName();
        System.out.println(name + "方法返回:"+r);
    }

    /**
     * 異常通知
     * @param joinPoint
     * @param e 目標方法所丟擲的異常,注意,這個引數必須是目標方法所丟擲的異常或者所丟擲的異常的父類,只有這樣,才會捕獲。如果想攔截所有,引數型別宣告為 Exception
     */
    @AfterThrowing(value = "pointcut()",throwing = "e")
    public void afterThrowing(JoinPoint joinPoint,Exception e) {
        Signature signature = joinPoint.getSignature();
        String name = signature.getName();
        System.out.println(name + "方法拋異常了:"+e.getMessage());
    }

    /**
     * 環繞通知
     *
     * 環繞通知是集大成者,可以用環繞通知實現上面的四個通知,這個方法的核心有點類似於在這裡透過反射執行方法
     * @param pjp
     * @return 注意這裡的返回值型別最好是 Object ,和攔截到的方法相匹配
     */
    @Around("pointcut()")
    public Object around(ProceedingJoinPoint pjp) {
        Object proceed = null;
        try {
            //這個相當於 method.invoke 方法,我們可以在這個方法的前後分別新增日誌,就相當於是前置/後置通知
            proceed = pjp.proceed();
        } catch (Throwable throwable) {
            throwable.printStackTrace();
        }
        return proceed;
    }
}

5.4 XML 配置 Aop

<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-context</artifactId>
    <version>5.1.9.RELEASE</version>
</dependency>
<dependency>
    <groupId>org.aspectj</groupId>
    <artifactId>aspectjweaver</artifactId>
    <version>1.9.5</version>
</dependency>
<dependency>
    <groupId>org.aspectj</groupId>
    <artifactId>aspectjrt</artifactId>
    <version>1.9.5</version>
</dependency>

接下來,定義通知/增強,但是單純定義自己的行為即可,不再需要註解:

public class LogAspect {

    public void before(JoinPoint joinPoint) {
        Signature signature = joinPoint.getSignature();
        String name = signature.getName();
        System.out.println(name + "方法開始執行了...");
    }
    
    public void after(JoinPoint joinPoint) {
        Signature signature = joinPoint.getSignature();
        String name = signature.getName();
        System.out.println(name + "方法執行結束了...");
    }

    public void returing(JoinPoint joinPoint,Integer r) {
        Signature signature = joinPoint.getSignature();
        String name = signature.getName();
        System.out.println(name + "方法返回:"+r);
    }
    
    public void afterThrowing(JoinPoint joinPoint,Exception e) {
        Signature signature = joinPoint.getSignature();
        String name = signature.getName();
        System.out.println(name + "方法拋異常了:"+e.getMessage());
    }
    
    public Object around(ProceedingJoinPoint pjp) {
        Object proceed = null;
        try {
            //這個相當於 method.invoke 方法,我們可以在這個方法的前後分別新增日誌,就相當於是前置/後置通知
            proceed = pjp.proceed();
        } catch (Throwable throwable) {
            throwable.printStackTrace();
        }
        return proceed;
    }
}

接下來在 spring 中配置 Aop:

<bean class="org.javaboy.aop.LogAspect" id="logAspect"/>
<aop:config>
    <aop:pointcut id="pc1" expression="execution(* org.javaboy.aop.commons.*.*(..))"/>
    <aop:aspect ref="logAspect">
        <aop:before method="before" pointcut-ref="pc1"/>
        <aop:after method="after" pointcut-ref="pc1"/>
        <aop:after-returning method="returing" pointcut-ref="pc1" returning="r"/>
        <aop:after-throwing method="afterThrowing" pointcut-ref="pc1" throwing="e"/>
        <aop:around method="around" pointcut-ref="pc1"/>
    </aop:aspect>
</aop:config>

最後,在 Main 方法中載入配置檔案:

public class Main {
    public static void main(String[] args) {
        ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml");
        MyCalculatorImpl myCalculator = ctx.getBean(MyCalculatorImpl.class);
        myCalculator.add(3, 4);
        myCalculator.min(5, 6);
    }
}

6. JdbcTemplate

JdbcTemplate 是 Spring 利用 Aop 思想封裝的 JDBC 操作工具。

6.1 準備

建立一個新專案,新增如下依賴:

<dependencies>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-jdbc</artifactId>
        <version>5.1.9.RELEASE</version>
    </dependency>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-context</artifactId>
        <version>5.1.9.RELEASE</version>
    </dependency>
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <version>8.0.17</version>
    </dependency>
</dependencies>

準備資料庫:

CREATE DATABASE /*!32312 IF NOT EXISTS*/`test01` /*!40100 DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci */ /*!80016 DEFAULT ENCRYPTION='N' */;

USE `test01`;

/*Table structure for table `user` */

DROP TABLE IF EXISTS `user`;

CREATE TABLE `user` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `username` varchar(255) COLLATE utf8mb4_general_ci DEFAULT NULL,
  `address` varchar(255) COLLATE utf8mb4_general_ci DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;

準備一個實體類:

public class User {
    private Integer id;
    private String username;
    private String address;

    @Override
    public String toString() {
        return "User{" +
                "id=" + id +
                ", username='" + username + ''' +
                ", address='" + address + ''' +
                '}';
    }

    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public String getAddress() {
        return address;
    }

    public void setAddress(String address) {
        this.address = address;
    }
}

6.2 Java 配置

提供一個配置類,在配置類中配置 JdbcTemplate:

@Configuration
public class JdbcConfig {
    @Bean
    DataSource dataSource() {
        DriverManagerDataSource dataSource = new DriverManagerDataSource();
        dataSource.setDriverClassName("com.mysql.cj.jdbc.Driver");
        dataSource.setUsername("root");
        dataSource.setPassword("123");
        dataSource.setUrl("jdbc:mysql:///test01");
        return dataSource;
    }
    @Bean
    JdbcTemplate jdbcTemplate() {
        return new JdbcTemplate(dataSource());
    }
}

這裡,提供兩個 Bean,一個是 DataSource 的 Bean,另一個是 JdbcTemplate 的 Bean,JdbcTemplate 的配置非常容易,只需要 new 一個 Bean 出來,然後配置一下 DataSource 就i可以。

public class Main {

    private JdbcTemplate jdbcTemplate;

    @Before
    public void before() {
        AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(JdbcConfig.class);
        jdbcTemplate = ctx.getBean(JdbcTemplate.class);
    }

    @Test
    public void insert() {
        jdbcTemplate.update("insert into user (username,address) values (?,?);", "javaboy", "");
    }
    @Test
    public void update() {
        jdbcTemplate.update("update user set username=? where id=?", "javaboy123", 1);

    }
    @Test
    public void delete() {
        jdbcTemplate.update("delete from user where id=?", 2);
    }

    @Test
    public void select() {
        User user = jdbcTemplate.queryForObject("select * from user where id=?", new BeanPropertyRowMapper<User>(User.class), 1);
        System.out.println(user);
    }
}

在查詢時,如果使用了 BeanPropertyRowMapper,要求查出來的欄位必須和 Bean 的屬性名一一對應。如果不一樣,則不要使用 BeanPropertyRowMapper,此時需要自定義 RowMapper 或者給查詢的欄位取別名。

  1. 給查詢出來的列取別名:
@Test
public void select2() {
    User user = jdbcTemplate.queryForObject("select id,username as name,address from user where id=?", new BeanPropertyRowMapper<User>(User.class), 1);
    System.out.println(user);
}

2.自定義 RowMapper

@Test
public void select3() {
    User user = jdbcTemplate.queryForObject("select * from user where id=?", new RowMapper<User>() {
        public User mapRow(ResultSet resultSet, int i) throws SQLException {
            int id = resultSet.getInt("id");
            String username = resultSet.getString("username");
            String address = resultSet.getString("address");
            User u = new User();
            u.setId(id);
            u.setName(username);
            u.setAddress(address);
            return u;
        }
    }, 1);
    System.out.println(user);
}

查詢多條記錄,方式如下:

@Test
public void select4() {
    List<User> list = jdbcTemplate.query("select * from user", new BeanPropertyRowMapper<>(User.class));
    System.out.println(list);
}

6.3 XML 配置

以上配置,也可以透過 XML 檔案來實現。透過 XML 檔案實現只是提供 JdbcTemplate 例項,剩下的程式碼還是 Java 程式碼,就是 JdbcConfig 被 XML 檔案代替而已。

<bean class="org.springframework.jdbc.datasource.DriverManagerDataSource" id="dataSource">
    <property name="username" value="root"/>
    <property name="password" value="123"/>
    <property name="url" value="jdbc:mysql:///test01?serverTimezone=Asia/Shanghai"/>
    <property name="driverClassName" value="com.mysql.cj.jdbc.Driver"/>
</bean>
<bean class="org.springframework.jdbc.core.JdbcTemplate" id="jdbcTemplate">
    <property name="dataSource" ref="dataSource"/>
</bean>

配置完成後,載入該配置檔案,並啟動:

public class Main {

    private JdbcTemplate jdbcTemplate;

    @Before
    public void before() {
        ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml");
        jdbcTemplate = ctx.getBean(JdbcTemplate.class);
    }

    @Test
    public void insert() {
        jdbcTemplate.update("insert into user (username,address) values (?,?);", "javaboy", "");
    }
    @Test
    public void update() {
        jdbcTemplate.update("update user set username=? where id=?", "javaboy123", 1);

    }
    @Test
    public void delete() {
        jdbcTemplate.update("delete from user where id=?", 2);
    }

    @Test
    public void select() {
        User user = jdbcTemplate.queryForObject("select * from user where id=?", new BeanPropertyRowMapper<User>(User.class), 1);
        System.out.println(user);
    }
    @Test
    public void select4() {
        List<User> list = jdbcTemplate.query("select * from user", new BeanPropertyRowMapper<>(User.class));
        System.out.println(list);
    }

    @Test
    public void select2() {
        User user = jdbcTemplate.queryForObject("select id,username as name,address from user where id=?", new BeanPropertyRowMapper<User>(User.class), 1);
        System.out.println(user);
    }

    @Test
    public void select3() {
        User user = jdbcTemplate.queryForObject("select * from user where id=?", new RowMapper<User>() {
            public User mapRow(ResultSet resultSet, int i) throws SQLException {
                int id = resultSet.getInt("id");
                String username = resultSet.getString("username");
                String address = resultSet.getString("address");
                User u = new User();
                u.setId(id);
                u.setName(username);
                u.setAddress(address);
                return u;
            }
        }, 1);
        System.out.println(user);
    }

}

7. 事務

Spring 中的事務主要是利用 Aop 思想,簡化事務的配置,可以透過 Java 配置也可以透過 XML 配置。

準備工作:

我們透過一個轉賬操作來看下 Spring 中的事務配置。

首先準備 SQL:

CREATE DATABASE /*!32312 IF NOT EXISTS*/`test01` /*!40100 DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci */ /*!80016 DEFAULT ENCRYPTION='N' */;

USE `test01`;

/*Table structure for table `account` */

DROP TABLE IF EXISTS `account`;

CREATE TABLE `account` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `username` varchar(255) COLLATE utf8mb4_general_ci DEFAULT NULL,
  `money` int(11) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;

/*Data for the table `account` */

insert  into `account`(`id`,`username`,`money`) values (1,'zhangsan',1000),(2,'lisi',1000);

然後配置 JdbcTemplate ,JdbcTemplate 的配置和第 6 小節一致。

然後,提供轉賬操作的方法:

@Repository
public class UserDao {
    @Autowired
    JdbcTemplate jdbcTemplate;

    public void addMoney(String username, Integer money) {
        jdbcTemplate.update("update account set money=money+? where username=?", money, username);
    }

    public void minMoney(String username, Integer money) {
        jdbcTemplate.update("update account set money=money-? where username=?", money, username);
    }
}
@Service
public class UserService {
    @Autowired
    UserDao userDao;
    public void updateMoney() {
        userDao.addMoney("zhangsan", 200);
        int i = 1 / 0;
        userDao.minMoney("lisi", 200);
    }
}

最後,在 XML 檔案中,開啟自動化掃描:

<context:component-scan base-package="org.javaboy"/>
<bean class="org.springframework.jdbc.datasource.DriverManagerDataSource" id="dataSource">
    <property name="username" value="root"/>
    <property name="password" value="123"/>
    <property name="url" value="jdbc:mysql:///test01?serverTimezone=Asia/Shanghai"/>
    <property name="driverClassName" value="com.mysql.cj.jdbc.Driver"/>
</bean>
<bean class="org.springframework.jdbc.core.JdbcTemplate" id="jdbcTemplate">
    <property name="dataSource" ref="dataSource"/>
</bean>

7.1 XML 配置

XML 中配置事務一共分為三個步驟:

1.配置 TransactionManager

<bean class="org.springframework.jdbc.datasource.DataSourceTransactionManager" id="transactionManager">
    <property name="dataSource" ref="dataSource"/>
</bean>

2.配置事務要處理的方法

<tx:advice id="txAdvice" transaction-manager="transactionManager">
    <tx:attributes>
        <tx:method name="update*"/>
        <tx:method name="insert*"/>
        <tx:method name="add*"/>
        <tx:method name="delete*"/>
    </tx:attributes>
</tx:advice>

注意,一旦配置了方法名稱規則之後,service 中的方法一定要按照這裡的名稱規則來,否則事務配置不會生效

3.配置 Aop

<aop:config>
    <aop:pointcut id="pc1" expression="execution(* org.javaboy.service.*.*(..))"/>
    <aop:advisor advice-ref="txAdvice" pointcut-ref="pc1"/>
</aop:config>

4.測試

@Before
public void before() {
    ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml");
    jdbcTemplate = ctx.getBean(JdbcTemplate.class);
    userService = ctx.getBean(UserService.class);
}
@Test
public void test1() {
    userService.updateMoney();
}

7.2 Java 配置

如果要開啟 Java 註解配置,在 XML 配置中新增如下配置:

<tx:annotation-driven transaction-manager="transactionManager" />

這行配置,可以代替下面兩個配置:

<tx:advice id="txAdvice" transaction-manager="transactionManager">
    <tx:attributes>
        <tx:method name="update*"/>
        <tx:method name="insert*"/>
        <tx:method name="add*"/>
        <tx:method name="delete*"/>
    </tx:attributes>
</tx:advice>
<aop:config>
    <aop:pointcut id="pc1" expression="execution(* org.javaboy.service.*.*(..))"/>
    <aop:advisor advice-ref="txAdvice" pointcut-ref="pc1"/>
</aop:config>

然後,在需要新增事務的方法上,新增 @Transactional 註解,表示該方法開啟事務,當然,這個註解也可以放在類上,表示這個類中的所有方法都開啟事務。

@Service
public class UserService {
    @Autowired
    UserDao userDao;
    @Transactional
    public void updateMoney() {
        userDao.addMoney("zhangsan", 200);
        int i = 1 / 0;
        userDao.minMoney("lisi", 200);
    }
}

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/4662/viewspace-2824535/,如需轉載,請註明出處,否則將追究法律責任。

相關文章