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

1. Spring 簡介

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

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

通常來講,初學者主要掌握 Spring 四個方面的功能:mysql

  • Ioc/DI
  • AOP
  • 事務
  • JdbcTemplate

2. Spring 下載

正常來講,咱們在項目中添加 Maven 依賴就能夠直接使用 Spring 了,若是須要單獨下載 jar,下載地址以下:linux

下載成功後,Spring 中的組件,大體上提供了以下功能:web

3.1 Ioc

3.1.1 Ioc 概念

Ioc (Inversion of Control),中文叫作控制反轉。這是一個概念,也是一種思想。控制反轉,實際上就是指對一個對象的控制權的反轉。例如,以下代碼:spring

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 對象,得從新建立,也就是說,對象的建立、初始化、銷燬等操做,通通都要開發者本身來完成。若是可以將這些操做交給容器來管理,開發者就能夠極大的從對象的建立中解脫出來。sql

使用 Spring 以後,咱們能夠將對象的建立、初始化、銷燬等操做交給 Spring 容器來管理。就是說,在項目啓動時,全部的 Bean 都將本身註冊到 Spring 容器中去(若是有必要的話),而後若是其餘 Bean 須要使用到這個 Bean ,則不須要本身去 new,而是直接去 Spring 容器去要。數據庫

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

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="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">

</beans>
複製代碼

在這個文件中,咱們能夠配置全部須要註冊到 Spring 容器的 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/beans http://www.springframework.org/schema/beans/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="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/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="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/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、user一、user二、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. 數據庫操做
  4. ....

這些操做中,無一例外,都有不少模板化的代碼,而解決模板化代碼,消除臃腫就是 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", "www.javaboy.org");
    }
    @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", "www.javaboy.org");
    }
    @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);
    }
}
複製代碼

關注微信公衆號【江南一點雨】,回覆 spring,獲取本文電子版,或者訪問 spring.javaboy.org 查看本文電子書。

相關文章
相關標籤/搜索