一篇文章帶你瞭解、學習、體驗、回顧Spring基礎

Spring介紹

優點

  • Spring是一個免費開源的框架(容器)
  • Spring是一個輕量級、非入侵式的框架:從框架的角度可以理解爲:無需繼承框架提供的任何類
    這樣我們在更換框架時,之前寫過的代碼幾乎可以繼續使用。
  • 反轉控制(IoC)、面向切面編程(AOP)
  • 支持事務的處理,對框架整合支持

組成

在這裏插入圖片描述

框架結構

  • Data Access/Integration層包含有JDBC、ORM、OXM、JMS和Transaction模塊。

  • Web層包含了Web、Web-Servlet、WebSocket、Web-Porlet模塊。

  • AOP模塊提供了一個符合AOP聯盟標準的面向切面編程的實現。

  • **Core Container(核心容器):**包含有Beans、Core、Context和SpEL模塊。

     Bean -- 處理Bean的jar
     context -- 處理spring上下文的jar
     core -- Spring 核心jar
     expression -- Spring 表達式
    
  • Test模塊支持使用JUnit和TestNG對Spring組件進行測試。

Spring配置的可選方案

  • 在XML中進行顯式配置;
  • 在java中進行顯式配置;
  • 隱式的bean發現機制和自動裝配;

很多情形下選擇哪種配置方案很大程度上是個人喜好的原因,當然還可以進行各種配置方案之間的互配。在《Spring in action》書中,作者建議使用以下順序:

  1. 自動裝配的機制;
  2. 當必須要使用顯示配置bean的時候,推薦使用javaConfig,它的優勢是比XML顯示配置更安全且更加強大;
  3. 最後選擇XML配置;

Spring Ioc 與 DI

IoC:Inverse of Control(控制反轉) : 它不是什麼技術,而是一種設計思想,就是將原本在程序中手動創建對象的控制權,交由Spring框架來管理。

DI:Dependency Injection(依賴注入) : 指 Spring 創建對象的過程中,將對象依賴屬性(簡單值,集合,對象)通過配置設值給該對象

創建第一個Ioc程序

​ 首先,我們需要創建Spring的配置文件,Spring配置文件的名稱和位置沒有固定要求,一般建議把該文件放到src目錄下面,名稱可隨便寫,官方建議寫成applicationContext.xml。然後我們還需要在配置文件中引入約束,Spring學習階段的約束是schema約束。那麼問題來了,這個約束又該怎麼寫呢?可參考docs\spring-framework-reference\html目錄下的xsd-configuration.html文件,在其內容最後面找到如下內容。

在這裏插入圖片描述

​ 將其複製黏貼到配置文件applicationContext.xml中,這樣applicationContext.xml文件的內容就是下面的樣子了。

<?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>
  • 創建一個接口然後將其實現。

    package com.yy.dao;
    
    public interface UserDao {
        void saveUser();
    }
    
    import com.yy.dao.UserDao;
    
    public class UserDaoImpl implements UserDao {
        public void saveUser() {
            System.out.println("存儲了User對象....");
        }
    }
    
  • 然後將這個類交給Spring管理,即在文件中配置對象的創建。

    <bean name="userDao" class="com.yy.dao.Impl.UserDaoImpl"></bean>
    
  • 測試類進行測試

    我們先把獲取ApplicationContext對象的過程封裝成一個工具類,相對能簡化一下開發步驟。

    import org.springframework.context.ApplicationContext;
    import org.springframework.context.support.ClassPathXmlApplicationContext;
    
    public class applicationContextUtil {
        public static ApplicationContext getApplicationContext(){
            return new ClassPathXmlApplicationContext("applicationContext.xml");
        }
    }
    

    進行測試

    @Test
    public void saveUser(){
        ApplicationContext context = applicationContextUtil.getApplicationContext();
        UserDao userDao = (UserDao) context.getBean("userDao");
        userDao.saveUser();
    }
    
  • 結果

    存儲了User對象....
    
  • 總結:發現雖然我們並沒有創建UserDao對象但是我們依然通過applicationContext.xml裏面的配置,從而拿取了對象。

創建第一個DI程序

  • 首先創建一個實體類

    package com.yy.pojo;
    
    import lombok.Data;
    
    @Data
    public class User {
        private Integer id;
        private String username;
        private String address;
    }
    
  • 然後在applicationContext.xml中給實體類注入數據

    <bean name="user" class="com.yy.pojo.User">
        <property name="id" value="201601"/>
        <property name="username" value="大臉貓"/>
        <property name="address" value="清華大學"/>
    </bean>
    
  • 測試類

    @Test
    public void getUser(){
        ApplicationContext context = applicationContextUtil.getApplicationContext();
        User user = (User)context.getBean("user");
        System.out.println(user.toString());
    }
    
  • 測試結果

    User(id=201601, username=大臉貓, address=清華大學)
    

總結

IoC 和 DI 其實是同一個概念的不同角度描述,DI 相對 IoC 而言,明確描述了被注入對象依賴 IoC 容器配置依賴對象

DI實現過程:

  • context.getBean("user")先根據要求找到,xml或註解中相應的配置。
  • 看看user依賴的是哪個實體類,拿到類名。
  • 使用反射的API,基於類名實例化對應的對象實例
  • 將對象實例,通過構造函數或者setter,傳遞給user對象

Java Bean的相關問題

bean的id和name屬性的配置

​ 從之前編寫的Spring配置文件中,可以發現<bean>標籤中有一個id屬性,其實它還有一個和id屬性類似的name屬性,根據它倆的值均能得到配置對象。

**id屬性:**在Spring配置文件中會有多個bean標籤,但它們的id屬性值是不能相同的。Bean起名字時,在約束中採用的是ID約束(唯一約束),而且名字必須以字母開始,可以使用字母、數字、連字符、下劃線、句號、冒號等,但id屬性值絕對不能有特殊符號;

**name屬性:**沒有使用約束中的唯一約束,理論上name屬性值是可以出現重複的,但是這在實際開發中是不可能出現的,而且它裏面可以出現特殊字符。其實,在Spring和Struts1框架進行整合的時候,纔有可能會用到。

bean聲明週期的配置

​ 通過配置<bean>標籤上的init-method作爲Bean被初始化的時候執行的方法,配置destroy-method作爲bean被銷燬的時候執行的方法,要想bean被銷燬的時候執行destroy-method屬性中配置的方法,那麼bean得是單例創建的(默認即單例創建),而且只有工廠被關閉時,destroy-method屬性中配置的方法才能被執行。

  • 同樣實現一個接口:
package com.yy.dao.Impl;

import com.yy.dao.UserDao;

public class UserDaoImpl implements UserDao {
    public void saveUser() {
        System.out.println("存儲了User對象....");
    }

    public void start(){
        System.out.println("初始化...");
    }
    
    public void end(){
        System.out.println("銷燬...");
    }
}
  • 在配置文件中配置初始化和銷燬方法

    <bean name="userDao" class="com.yy.dao.Impl.UserDaoImpl" init-method="start" destroy-method="end"/>
    
  • 測試

    @Test
    public void saveUser(){
        ApplicationContext context = applicationContextUtil.getApplicationContext();
        UserDao userDao = (UserDao) context.getBean("userDao");
        userDao.saveUser();
    }
    
  • 結果

    初始化...
    存儲了User對象....
    
  • 我們發現實際上並沒有執行deploy方法,原因是ApplycationContext對象並沒有關閉。

    @Test
    public void saveUser(){
        AbstractApplicationContext context = (AbstractApplicationContext) applicationContextUtil.getApplicationContext();
        UserDao userDao = (UserDao) context.getBean("userDao");
        userDao.saveUser();
        context.close();
    }
    
  • 結果

    初始化...
    存儲了User對象....
    銷燬...
    

    ​ 要想bean被銷燬的時候執行destroy-method屬性中配置的方法,那麼前提bean得是單例創建的,默認即單例創建,就像下面這樣。

    <.....scope="singleton"/>	
    

    若是設置成多例模式結果則不同。

bean的作用範圍的配置

<bean>標籤上有一個scope屬性,它代表Bean的作用範圍。該屬性有五個屬性值,分別是:

  • singleton:scope屬性的默認值,spring會採用單例模式創建這個對象。

    單例模式時,創建的對象時相同的,多例模式則相反。

  • prototype:spring會採用多例模式創建這個對象。

  • request:應用在web項目中,spring創建這個類以後會將這個類存入到request域當中。

  • session:應用在web項目中,spring創建這個類以後會將這個類存入到session域當中。

  • globalsession:應用在web項目中,必須是prolet環境(你在一個地方存入數據以後,其它一些子系統當中就不需要進行登錄了)下才能使用,他要單點登錄(即SSO,single sign on)上,但如果沒有這種環境,你配置一個global session就相當於配置了一個session。

Spring中Bean的實例化方式

Bean交給Spring管理之後,它在創建這些Bean的時候,有以下三種方式:

  1. 無參構造方法的方式(默認)
  2. 靜態工廠實例化的方式
  3. 實例工廠實例化的方式

無參構造方法方式

​ 這種方式是Spring實例化Bean的默認方式,指的是在創建對象的時候,Spring會調用類裏面的無參數的構造方法實現。

​ 上面應用的實例全是無參構造方法方式。

靜態工廠實例化的方式

  • 首先創建一個Bean類

    package com.yy.spring;
    
    public class Bean {
        /**
         * 無參構造方法
         */
        public Bean(){
            System.out.println("Bean被執行");
        }
    }
    
  • 然後創建一個Bean工廠

    package com.yy.spring;
    
    public class BeanFactory {
        /**
         * 創建工廠
         * @return
         */
        public static Bean createBean(){
            System.out.println("BeanFactory被執行....");
            return  new Bean();
        }
    }
    
  • 配置文件

    <bean id="bean" class="com.yy.spring.BeanFactory" factory-method="createBean"></bean>
    
  • 測試結果

    BeanFactory被執行....
    Bean被執行
    

實例工廠實例化的方式

創建一個工廠類,在工廠類裏面提供一個普通的方法,這個方法返回類對象,調用工廠類的方法時,創建工廠類對象,使用對象調用方法即可

  • 首先創建一個Bean類

    public class Bean {
        /**
         * 無參構造方法
         */
        public Bean(){
            System.out.println("Bean被執行");
        }
    }
    
  • 然後創建一個Bean工廠

    public class BeanFactory {
        /**
         * 創建工廠
         * @return
         */
        public Bean createBean(){
            System.out.println("BeanFactory被執行....");
            return  new Bean();
        }
    }
    
  • 進行相關配置

    <bean id="BeanFactory" class="com.yy.spring.BeanFactory"></bean>
    <bean id="bean" factory-bean="BeanFactory" factory-method="createBean"></bean>
    
  • 測試結果

    BeanFactory被執行....
    Bean被執行
    

Spring中Bean的屬性注入

實際上,有關Bean的屬性注入共有2種方式

  • 有參構造函數注入
  • set方法注入

有參構造函數注入

  • 創建一個實體類

    package com.yy.spring.domain;
    
    public class Car {
        private String name;
        private Double price;
    
        public Car(String name,Double price){
            super();
            this.name = name;
            this.price = price;
        }
    }
    
  • 然後,在Spring配置文件中對以上JavaBean進行如下配置。

    <bean id="car" class="com.yy.spring.pojo.Car">
        <constructor-arg name="name" value="汽車"></constructor-arg>
        <constructor-arg name="price" value="221.2"></constructor-arg>
    </bean>
    

set方法的方式注入屬性

  • 同樣創建一個實體類在實體類中生成set方法

    public class Car {
        private String name;
        private Double price;
    
        public void setName(String name) {
            this.name = name;
        }
    
        public void setPrice(Double price) {
            this.price = price;
        }
    }
    
  • 在配置文件中進行相關的配置

    <bean id="car" class="com.yy.spring.domain.Car">
        <property name="name" value="汽車"></property>
        <property name="price" value="2.213"></property>
    </bean>
    

SpEL表達式

​ 在Spring3.0版本以後,提供了一種SpEL表達式語言的屬性注入方式。SpEL,即Spring Expression Language,翻譯過來就是Spring的表達式語言,其語法格式是#{SpEL}

使用了這種SpEL表達式語言的屬性注入方式,還可以調用其他類的屬性或者方法。

<bean name="car" class="com.yy.pojo.Car">
    <property name="name" value="蘭博基尼"/>
    <property name="price" value="22"/>
</bean>
<bean name="carInfo" class="com.yy.pojo.CarInfo">
    <property name="name" value="#{car.name}"/>
    <property name="price" value="#{22 + 7}"/>
</bean>

如上,carInfo中使用了SpEL表達式,在表達式中既可以引用其它bean中的數據也可以在其中書寫表達式

注意事項:CarInfo的屬性值是通過SpEl從Car類中的屬性值賦值而來,所以Car類中必須要有setget方法,set方法是爲了用於配置文件進行依賴注入爲Car的屬性進行賦值,get方法是爲了用於獲取Car屬性的值,進而爲CarInfo進行賦值,而CarInfo只需要接受Car的屬性值所以CarInfo類中只需要存在set方法即可

裝配集合

<bean id="complexAssembly" class="pojo.ComplexAssembly">
    <!-- 裝配Long類型的id -->
    <property name="id" value="1"/>
    
    <!-- 裝配List類型的list -->
    <property name="list">
        <list>
            <value>value-list-1</value>
            <value>value-list-2</value>
            <value>value-list-3</value>
        </list>
    </property>
    
    <!-- 裝配Map類型的map -->
    <property name="map">
        <map>
            <entry key="key1" value="value-key-1"/>
            <entry key="key2" value="value-key-2"/>
            <entry key="key3" value="value-key-2"/>
        </map>
    </property>
    
    <!-- 裝配Properties類型的properties -->
    <property name="properties">
        <props>
            <prop key="prop1">value-prop-1</prop>
            <prop key="prop2">value-prop-2</prop>
            <prop key="prop3">value-prop-3</prop>
        </props>
    </property>
    
    <!-- 裝配Set類型的set -->
    <property name="set">
        <set>
            <value>value-set-1</value>
            <value>value-set-2</value>
            <value>value-set-3</value>
        </set>
    </property>
    
    <!-- 裝配String[]類型的array -->
    <property name="array">
        <array>
            <value>value-array-1</value>
            <value>value-array-2</value>
            <value>value-array-3</value>
        </array>
    </property>
</bean>

Spring的分模塊開發的配置

Spring中的分模塊開發有兩種配置方式,

加載多個配置文件

Spring分模塊開發的第一種配置方式就是指在加載配置文件的時候,一次加載多個配置文件。

new ClassPathXmlApplicationContext("applicationContext.xml","TApplicationContext.xml");

引入多個配置文件

​ Spring分模塊開發的第二種配置方式就是指在一個配置文件中引入多個配置文件。這樣說的話,我們可以在applicationContext.xml文件中引入TApplicationContext.xml文件。

<import resource="TApplicationContext.xml"/>

Spring註解開發

用IoC進行註解開發,需要進行配置一個掃描組件,就告訴他Spring項目下哪些包使用IoC註解了。

<!--配置一個掃描組件,告訴spring一個註解使用的包-->
<!--當com.yy.spring包下面還有其它子包那麼那麼同樣會被掃描-->    
<context:component-scan base-package="com.yy.spring"></context:component-scan>
<!--當然還可以一次配置多個包,中間用逗號隔開-->
<context:component-scan base-package="com.yy.spring,com.yy.p"></context:component-scan>    
<!--還可以設置掃描範圍內哪些註解可不可以用-->    
<context:include-filter>
<context:exclude-filter>
    
<!--下面例子的作用是禁用@Service註解-->  
<!--use-default-filter的作用是fliter標籤可否用,默認可用,感覺這個標籤有些多餘-->    
<context:component-scan base-package="com.yy.spring" use-default-filters="true">
    <context:exclude-filter type="annotation" 			                          		                                 expression="org.springframework.stereotype.Service"/>
</context:component-scan>

簡單的註解實例

  • 配置掃描器

    <context:component-scan base-package="com.yy.pojo,com.yy.dao"/>
    
  • pojo類種加入註解

    import lombok.Data;
    import org.springframework.beans.factory.annotation.Value;
    import org.springframework.stereotype.Component;
    
    @Data
    @Component("car")//相當於<bean id="car" class="..."></bean>
    public class Car {
        @Value("mike")
        private String name;
        @Value("33.4")
        private Double price;
    }
    
  • 測試

    @Test
    public void getUser(){
        ApplicationContext context = applicationContextUtil.getApplicationContext();
        Car car = (Car) context.getBean("car");
        System.out.println(car.toString());
    }
    
  • 結果

    Car(name=mike, price=33.4)
    
  • 聲明容器的註解

    • @Component 註解是用於把 SgtPeppers類實例化注入到 Spring容器當中,相當於配置文件當中的<bean id="" class=""/>

      另外與它類似的註解還有:

    • @Controller:標註控制層組件

    • @Service:標註業務層組件

    • @Repository:標註數據層組件

    • @Component:泛指組件,當組件不好歸類的時候,我們可以使用這個註解進行標註

注入屬性註解

  • Autowired:自動按照類型注入。只要容器中有唯一的一個bean對象類型和要注入的變量類型匹配,就可以注入成功。 如果IoC容器中沒有任何bean的類型和要注入的變量類型匹配,則報錯。如果IoC容器中有多個類型匹配時:將變量名跟已經匹配的數據類型的key值比較,如果有唯一匹配則注入成功,否則失敗。
  • @Qualifier:在自動按照類型注入的基礎之上,再按照Bean的id注入。它在給字段注入時不能獨立使用,必須和@Autowire一起使用;但是給方法參數注入時,可以獨立使用。
  • @Resource:直接按照Bean的id注入。它也只能注入其他bean類型。
  • @Value:注入基本數據類型和String類型數據的 ,不能注入bean類型的

Scope作用範圍註解

@scope: 用於指定bean的作用域,一般就是singleton和prototype 。

生命週期註解

  • @PostConstruct: 用於指定初始化方法,相當於bean標籤中的init-method屬性。
  • @PreDestroy 作用: 用於指定銷燬方法。相當於bean標籤中的destroy-method屬性。

Spring從以下方式實現自動化裝配

  • 組件掃描(component scanning):Spring會自動發現應用上下文中所創建的bean;
  • 自動裝配(autowiring):Spring自動滿足bean之間的依賴;

用javaConfig的方式配置註解

在這裏使用一個以 CD播放的例子

  1. 首先創建一個 CompactDisc接口

    package com.spring;
    
    public interface CompactDisc {
        void play();
    }
    
  2. 創建一個SgtPeppers類實現CompactDisc接口

    package com.spring;
    
    import org.springframework.stereotype.Component;
    
    @Component
    public class SgtPepper implements CompactDisc {
        private String title = "Sgt. Pepper's Lonely Hearts Club Band";
        private String artist = "The Beatles";
    
        public void play() {
            System.out.println("Playing " + title + " by " + artist);
        }
    }
    
  3. 使用配置類開啓註解掃描

    package com.spring;
    
    import org.springframework.context.annotation.ComponentScan;
    import org.springframework.context.annotation.Configuration;
    
    @Configuration
    @ComponentScan
    public class CDPlayerConfig {
    }
    

    @Configuration用於定義配置類,被定義的類相當於.xml配置文件

    @ComponentScan會開啓掃描與配置類相同的包中的註解 此時的包就是 package com.spring

  4. 測試組件是否能夠掃描發現CompactDisc

    import com.spring.CompactDisc;
    import com.spring.CDPlayerConfig;
    import org.junit.Test;
    import org.junit.runner.RunWith;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.test.context.ContextConfiguration;
    import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
    
    import static org.junit.Assert.assertNotNull;
    
    @RunWith(SpringJUnit4ClassRunner.class)
    @ContextConfiguration(classes = CDPlayerConfig.class)
    public class CDPlayerTest {
        @Autowired
        private CompactDisc cd;
    
        @Test
        public void cdShouldNotBeNull(){
            assertNotNull(cd);
        }
    }
    

    @RunWith就是一個運行器

    @RunWith(SpringJUnit4ClassRunner.class)就是用Spring的測試環境來運行測試

    @ContextConfiguration通常與@RunWith一起使用來進行測試

    使用方式有以下幾種

    單個文件
    @ContextConfiguration(Locations=”../applicationContext.xml”)
    @ContextConfiguration(classes = SimpleConfiguration.class)

    多個文件時,可用{}

    @ContextConfiguration(locations = {“classpath*:/*.xml”,“classpath*:/*.xml”})

    assertNotNull(cd);是用戶判斷對象是否爲NULL,如果爲NULL則會報出異常說明@AuroWired註解注入失敗。

  5. 經測試掃描組件能夠發現CompactDisc

爲組件掃描的bean命名

​ Spring應用上下文中所有的bean都會給定一個ID,雖然我們在前面的例子中沒有給SgtPeppers bean設置ID,但是Spring會給它自動設置ID爲sgtPeppers(首字母小寫),當然也可以自己給bean設置ID,比如

import org.springframework.stereotype.Component;

@Component("beautifulBean")
public class SgtPepper implements CompactDisc {
	...
}

設置掃描組件基礎包

前面例子,用@ComponentScan掃描配置類所在的包爲基礎包,還有可以通過配置類指定不同的基礎包

例如:

直接在配置類註解中聲明基礎包的位置

import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;

@Configuration
@ComponentScan("com.spring")
public class CDPlayerConfig {}

如果想清晰的表明所設置的基礎包,可以通過basePackages進行設置

@ComponentScan(basePackages = "com.spring")

當然可以設置多個包,通過掃描一個數組

@ComponentScan(basePackages = {"com.spring","com.mybatis"})

除了使用basePackage屬性還可以使用basePackageClasses屬性來指定包中所含類或者是接口來實現基礎包。

@ComponentScan(basePackageClasses = {SgtPepper.class})

​ 當然也可通過逗號來設置多個包,這裏《Spring in action》作者建議在使用basePackageClasses屬性時,後面的類或者接口使用專門的類來作爲指定類,即這些類的作用只是給配置類掃描配置基礎包來使用的,防止在重構過程中某些指定的類被刪除掉導致出現問題。

通過bean添加註解實現自動裝配

自動裝配在Spring中我們常見的有兩種方式:構造器注入 和 Setter方法注入

package com.spring;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

@Component
public class CDPlayer {
    private CompactDisc cd;

    @Autowired
    public CDPlayer(CompactDisc cd){
        this.cd = cd;
    }

}

上爲構造器注入 下爲Setter方法注入
    
@Component
public class CDPlayer {
    private CompactDisc cd;

    @Autowired
    public void setCd(CompactDisc cd){
        this.cd = cd;
    }

}

​ 其實Setter方法沒有什麼任何特別的地方,其它方法也可以,比如說把setCd()改成insertCd()效果事相同的,而所謂的Setter方法只是一種規範。

​ 如果沒有匹配bean,那麼在應用上下文創建的時候,Spring會拋出異常,爲避免拋出異常的出現可以將@Autowired@required屬性設置爲false

​ java依賴注入規範中有一個@Inject註解其使用方法幾乎 相同 大多數場景可以相互替換。

代理模式

靜態代理模式

什麼是代理模式?就是使用代理的形式將一些業務外包給一些專門的人做,自己專注做核心業務。

​ 舉一個例子 - 本人要把自己的十幾套房子租出去,那麼我又不想去貼小廣告,該怎麼做呢?把鑰匙給房屋出租代理公司就行了,剩下的事情讓代理公司代理你去做。

首先我們爲創建一個Rent接口

package com.proxy;

public interface Rent {
    void rent();
}

然後創建一個Host類繼承Rent接口,代表房子主人要出租房子。

package com.proxy;

public class Host implements Rent{
    @Override
    public void rent() {
        System.out.println("房主同意租房子!");
    }
}

再創建一個代理讓他出租房子,代理的業務還有帶客戶看房子、收費用等

package com.proxy;

public class Proxy implements Rent{
    private Host host;

    public Proxy(Host host) {
        this.host = host;
    }

    @Override
    public void rent() {
        seeHost();
        host.rent();
        fare();
    }

    public void seeHost(){
        System.out.println("看房子!");
    }

    public void fare(){
        System.out.println("付房租");
    }
}

最後是用戶租房子

package com.proxy;

public class Client {
    public static void main(String[] args) {
        Host host = new Host();
        Proxy proxy = new Proxy(host);

        proxy.rent();
    }
}

執行結果

看房子!
房主同意租房子!
付房租

​ 由上面發現,房主所作的工作只是同意房子出租,剩下的工作都是由代理公司做的,但是房東所做的是最重要的內容。同樣在平常寫代碼時,比如一些安全、日誌等功能應使用代理的方式來實現,而且在給原有系統增加一些功能時我們的原則應該是儘量不要修改原來的代碼通過代理來實現功能增加。

靜態代理模式的好處:

  • 可以使真實角色操作更加純粹!不用關注一些公共業務。
  • 公共業務由代理進行,實現了業務分工。
  • 公共業務擴展時,方便幾種管理。

缺點:

  • 一個真實角色就會產生一個代理角色,代碼量會翻倍。

動態代理

​ 動態代理就是通過反射的方式實現比靜態代理更加便捷的代理,同時,Jdk提供了invocationHandler接口和Proxy類,藉助這兩個工具可以達到我們想要的效果。

JDK動態代理具體步驟:

  1. 通過實現InvocationHandler 接口創建自己的調用處理器;
  2. 通過爲 Proxy類指定 ClassLoader對象和一組 interface來創建動態代理類;
  3. 通過反射機制獲得動態代理類的構造函數,其唯一參數類型是調用處理器接口類型;
  4. 通過構造函數創建動態代理類實例,構造時調用處理器對象作爲參數被傳入。

通過實例來實現一個動態代理

  • 首先創建一個日誌代理類,來實現一個簡單的日誌打印。

    package dynamicproxy;
    
    
    import java.lang.reflect.InvocationHandler;
    import java.lang.reflect.Method;
    import java.lang.reflect.Proxy;
    
    /**
     * @author :苑勇
     * @date :Created in 2020/5/24 22:52
     * @description:日誌代理類
     */
    public class LogProxy implements InvocationHandler {
        //被代理對象
        Object targetObject;
    
        public LogProxy(Object targetObject) {
            this.targetObject = targetObject;
        }
    
        /**
         * 將被代理的對象傳入獲得它的類加載器和實現接口作爲Proxy.newProxyInstance方法的參數。
         * 第一個參數: targetObject.getClass().getClassLoader():targetObject對象的類加載器。
         * 第二個參數:targetObject.getClass().getInterfaces():targetObject對象的所有接口。
         * 第三個參數:this:也就是當前對象即實現了InvocationHandler接口的類的對象,在調用方法時會調用它的invoke方法。
         * @return
         */
        public Object createLogProxy(){
            Object objectProxy = Proxy.newProxyInstance(
                targetObject.getClass().getClassLoader(),   										targetObject.getClass().getInterfaces(),
                this);
            return objectProxy;
        }
    
        /**
         * 我們在測試類中通過上面createLogProxy函數獲取代理對象,然後調用它的函數實際上是執行的invoke函數
         * @param proxy 該參數是代理的真實對象
         * @param method 該參數是代理的方法
         * @param args 代理方法中接受的參數
         * @return
         * @throws Throwable
         */
        @Override
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            System.out.println("----------------------");
            System.out.println("   對"+method.getName()+"方法進行了權限檢驗");
            System.out.println("   參數個數:"+method.getParameterCount());
            System.out.println("   參數類型:"+method.getParameterTypes());
            System.out.println("   返回參數類型:"+method.getReturnType());
            System.out.println("   ......");
            System.out.print("   方法執行打印:");
            //執行真正的方法
            Object ret = method.invoke(targetObject,args);
            System.out.println("   方法執行完畢");
            System.out.println("----------------------");
            return ret;
        }
    }
    
  • 創建一個動態代理的接口,同時我們要注意的是動態代理的實現方式也是通過接口實現的。

    package dynamicproxy;
    
    /**
     * @author :苑勇
     * @date :Created in 2020/5/24 22:45
     * @description:動態代理測試接口
     */
    public interface ProgramingLanguage {
    
        Object JavaCoder(String name, Integer age);
        Object PHPCoder();
        void CppCoder();
    }
    
  • 對接口進行實現

    package dynamicproxy;
    
    /**
     * @author :苑勇
     * @date :Created in 2020/5/24 22:49
     * @description:動態代理測試類
     */
    public class ProgramingLanguageImpl implements ProgramingLanguage {
    
        @Override
        public Object JavaCoder(String name, Integer age) {
            System.out.println(name +"是一個"+age+"歲的java工程師");
            return null;
        }
    
        @Override
        public Object PHPCoder() {
            System.out.println("PHP是最好的編程語言");
            return null;
        }
    
        @Override
        public void CppCoder() {
            System.out.println("C++是最好的編程語言");
        }
    }
    
  • 創建一個測試類進行測試

    import dynamicproxy.LogProxy;
    import dynamicproxy.ProgramingLanguage;
    import dynamicproxy.ProgramingLanguageImpl;
    
    /**
     * @author :苑勇
     * @date :Created in 2020/5/24 23:02
     * @description:動態代理測試類
     */
    public class ProxyTest {
        public static void main(String[] args) {
            ProgramingLanguage proxy = (ProgramingLanguage) new LogProxy(new ProgramingLanguageImpl()).createLogProxy();
            proxy.CppCoder();
            proxy.JavaCoder("小明",23);
            proxy.PHPCoder();
        }
    }
    
  • 測試結果

    ----------------------
       對CppCoder方法進行了權限檢驗
       參數個數:0
       參數類型:[Ljava.lang.Class;@306a30c7
       返回參數類型:void
       ......
       方法執行打印:C++是最好的編程語言
       方法執行完畢
    ----------------------
    ----------------------
       對JavaCoder方法進行了權限檢驗
       參數個數:2
       參數類型:[Ljava.lang.Class;@b81eda8
       返回參數類型:class java.lang.Object
       ......
       方法執行打印:小明是一個23歲的java工程師
       方法執行完畢
    ----------------------
    ----------------------
       對PHPCoder方法進行了權限檢驗
       參數個數:0
       參數類型:[Ljava.lang.Class;@506c589e
       返回參數類型:class java.lang.Object
       ......
       方法執行打印:PHP是最好的編程語言
       方法執行完畢
    

``


切面編程-AOP

​ Spring的Aop就是將公共的業務 (日誌 , 安全等) 和領域業務結合起來 , 當執行領域業務時 , 將會把公共業務加進來 . 實現公共業務的重複利用 . 領域業務更純粹 , 程序猿專注領域業務 , 其本質還是動態代理 .

​ 比如說一個公司的工資計算,每個部門都有自己的基本工資和績效工資標準,因爲每個部門乾的工作不同,但是公司給每個部門的員工付出的社保、公積金比例是相同的,所以在公司月底進行工資計算時,我們可以把每個部門的基本工資和績效工資計算寫成獨立的類,然後社保、公積金的計算就可以使用面向切面的方式來進行計算,這樣就會減低代碼量、降低業務之間的耦合性。

以下名詞需要了解下:

  • 橫切關注點:跨越應用程序多個模塊的方法或功能。即是,與我們業務邏輯無關的,但是我們需要關注的部分,就是橫切關注點。如日誌 , 安全 , 緩存 , 事務等等 …
  • 切面(ASPECT):橫切關注點 被模塊化 的特殊對象。即,它是一個類。
  • 通知(Advice):切面必須要完成的工作。即,它是類中的一個方法。
  • 目標(Target):被通知對象。
  • 代理(Proxy):向目標對象應用通知之後創建的對象。
  • 切入點(PointCut):切面通知 執行的 “地點”的定義。
  • 連接點(JointPoint):與切入點匹配的執行點。

通過Spring API實現的AOP

  • 首先創建一個前置日誌的類,簡單實現一個前置日誌通知

    package com.aop;
    
    import org.springframework.aop.MethodBeforeAdvice;
    import org.springframework.stereotype.Component;
    
    import java.lang.reflect.Method;
    
    /**
     * @author :苑勇
     * @date :Created in 2020/5/25 16:45
     * @description:前置Log
     */
    @Component
    public class BeforeLog implements MethodBeforeAdvice {
        public void before(Method method, Object[] objects, Object o) throws Throwable {
            System.out.println("前置通知:" + o.getClass().getName() + "對象的" + method.getName() + "方法被執行了");
        }
    }
    
  • 再創建一個後置通知

    package com.aop;
    
    import org.springframework.aop.AfterReturningAdvice;
    import org.springframework.stereotype.Component;
    
    import java.lang.reflect.Method;
    
    /**
     * @author :苑勇
     * @date :Created in 2020/5/25 16:47
     * @description:後置Log
     */
    @Component
    public class AfterLog implements AfterReturningAdvice {
        @Override
        public void afterReturning(Object returnValue, Method method, Object[] objects, Object target) throws Throwable {
            System.out.println("後置通知:" + target.getClass().getName() + "對象的" + method.getName() + "方法被執行了");
        }
    }
    
  • 再創建接口和實現類用於測試準備

    package com.aop;
    
    /**
     * @author :苑勇
     * @date :Created in 2020/5/25 16:41
     * @description:AOP測試接口
     */
    public interface UserService {
        void add();
        void delete();
        void update();
        void query();
    }
    
    package com.aop;
    
    import org.springframework.stereotype.Component;
    
    /**
     * @author :苑勇
     * @date :Created in 2020/5/25 16:43
     * @description:AOP測試類
     */
    @Component("userService")
    public class UserServiceImpl implements UserService {
        @Override
        public void add() {
            System.out.println("增加一個用戶");
        }
    
        @Override
        public void delete() {
            System.out.println("刪除一個用戶");
        }
    
        @Override
        public void update() {
            System.out.println("修改一個用戶");
        }
    
        @Override
        public void query() {
            System.out.println("查詢一個用戶");
        }
    }
    
  • 在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"
           xmlns:aop="http://www.springframework.org/schema/aop"
           xmlns:context="http://www.springframework.org/schema/context"
           xsi:schemaLocation="http://www.springframework.org/schema/beans
           http://www.springframework.org/schema/beans/spring-beans.xsd
           http://www.springframework.org/schema/aop
           http://www.springframework.org/schema/aop/spring-aop.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd">
    
        <context:component-scan base-package="com.aop"/>
        <aop:config>
            <!--切入點 expression:表達式匹配要執行的方法-->
            <aop:pointcut id="pointcut" expression="execution(* com.aop.UserServiceImpl.*(..))"/>
            <!--執行環繞; advice-ref執行方法 . pointcut-ref切入點-->
            <aop:advisor advice-ref="beforeLog" pointcut-ref="pointcut"/>
            <aop:advisor advice-ref="afterLog" pointcut-ref="pointcut"/>
        </aop:config>
    
    </beans>
    

    上面涉及到了一個execution表達式:

    1、execution(): 表達式主體。

    2、第一個*號:方法返回類型, *號表示所有的類型。

    3、包名:表示需要攔截的包名。

    4、第二個*號:表示類名,*號表示所有的類。

    5、*(…):最後這個星號表示方法名,*號表示所有的方法,後面( )裏面表示方法的參數,兩個句點表示任何參數

  • 測試類進行測試

    import com.aop.UserService;
    import org.junit.Test;
    import org.springframework.context.ApplicationContext;
    import org.springframework.context.support.ClassPathXmlApplicationContext;
    
    /**
     * @author :苑勇
     * @date :Created in 2020/5/25 16:53
     * @description:測試類
     */
    public class AopTest {
        @Test
        public void test(){
            ApplicationContext context = new ClassPathXmlApplicationContext("applicationConfig.xml");
            UserService userService = (UserService) context.getBean("userService");
            userService.query();
        }
    }
    
  • 測試結果

    前置通知:com.aop.UserServiceImpl對象的query方法被執行了
    查詢一個用戶
    後置通知:com.aop.UserServiceImpl對象的query方法被執行了
    

自定義類來實現AOP

  • 創建一個通知類

    package com.aop.div;
    
    import org.springframework.stereotype.Component;
    
    /**
     * @author :苑勇
     * @date :Created in 2020/5/25 20:03
     * @description:通知測試類
     */
    @Component("advice")
    public class Advice {
        public void before(){
            System.out.println("前置通知");
        }
    
        public void after(){
            System.out.println("後置通知");
        }
    }
    
  • 創建一個實體類

    package com.aop.div;
    
    import org.springframework.stereotype.Component;
    
    /**
     * @author :苑勇
     * @date :Created in 2020/5/25 20:05
     * @description:
     */
    @Component("pointCut")
    public class PointCut {
        public void sayHello(){
            System.out.println("Hello");
        }
    }
    
  • 在XML中進行註冊

    <aop:config>
        <aop:aspect ref="advice">
            <aop:pointcut id="pointcut-1" expression="execution(* com.aop.div.PointCut.*(..))"/>
            <aop:before pointcut-ref="pointcut-1" method="before"/>
            <aop:after pointcut-ref="pointcut-1" method="after"/>
        </aop:aspect>
    </aop:config>
    
  • 測試類進行測試

    @Test
    public void testAdvice(){
        ApplicationContext context = new 							      				                                ClassPathXmlApplicationContext("applicationConfig.xml");
        PointCut pointCut = (PointCut) context.getBean("pointCut");
        pointCut.sayHello();
    }
    
  • 測試結果

    前置通知
    查詢一個用戶
    後置通知
    

    相比使用spring Api的方式,此方式顯得更見便捷,但是這種方式使用起來並不如上一種強大。

註解實現AOP

​ 在這裏我們不使用就不使用任何xml文件了直接完全用javaConfig+註解的方式來實現以開始提到的計算社保公積金的一個小程序

  • 首先創建javaConfig類

    package com.aop.annotation;
    
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.ComponentScan;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.context.annotation.EnableAspectJAutoProxy;
    
    /**
     * @author :苑勇
     * @date :Created in 2020/5/25 20:39
     * @description:使用Java進行環境配置
     */
    @Configuration
    @ComponentScan(basePackages = "com.aop.annotation")
    @EnableAspectJAutoProxy
    public class JavaConfig {
    }
    

    @EnableAspectJAutoProxy註解相當於xml中的<aop:aspectj-autoproxy/>它的作用是聲明自動爲spring容器中那些配置@aspectJ切面的bean創建代理

  • 創建接口和實現類

    package com.aop.annotation;
    
    import org.springframework.stereotype.Component;
    
    /**
     * @author :苑勇
     * @date :Created in 2020/5/25 20:46
     * @description:接口
     */
    @Component
    public interface Department {
        Double developerSalary(Double base,Double performance);
        Double salerSalary(Double base,Double performance);
    }
    
    
    package com.aop.annotation;
    
    import org.springframework.stereotype.Component;
    
    /**
     * @author :苑勇
     * @date :Created in 2020/5/25 20:48
     * @description:實體類
     */
    @Component
    public class DepartmentImpl implements Department{
    
        @Override
        public Double developerSalary(Double base, Double performance) {
            Double countSalary =  base + performance * 8;
            return countSalary;
        }
    
        @Override
        public Double salerSalary(Double base, Double performance) {
            Double countSalary = base + performance * 6;
            return countSalary;
        }
    }
    
  • 創建增強類

    package com.aop.annotation;
    
    import org.aspectj.lang.JoinPoint;
    import org.aspectj.lang.annotation.After;
    import org.aspectj.lang.annotation.Aspect;
    import org.aspectj.lang.reflect.MethodSignature;
    import org.springframework.aop.AfterReturningAdvice;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Component;
    
    import java.lang.reflect.InvocationTargetException;
    import java.lang.reflect.Method;
    
    /**
     * @author :苑勇
     * @date :Created in 2020/5/25 20:56
     * @description:社保繳納後置通知
     */
    @Component
    @Aspect
    public class SocialSecurityPay{
        private Double countSalary;
        private Double socialSePay;
        private Double acFund;
        private Double finalSalary;
    
    
        @After("execution(* com.aop.annotation.DepartmentImpl.*(..))")
        public void after(JoinPoint joinPoint) throws InvocationTargetException, IllegalAccessException {
            MethodSignature signature = (MethodSignature) joinPoint.getSignature();
            Method method = signature.getMethod();
    
            countSalary = (Double) method.invoke(joinPoint.getTarget(),joinPoint.getArgs());
            socialSePay = 0.06 * countSalary;
            acFund = 0.08 * countSalary;
            finalSalary = countSalary - socialSePay - acFund;
            System.out.println("------------------------------------------");
            System.out.println(method.getName()+"本月工資爲:" + countSalary);
            System.out.println(method.getName()+"公積金繳納:" + acFund);
            System.out.println(method.getName()+"社保繳納爲:" + socialSePay);
            System.out.println(method.getName()+"事發薪資爲:" + finalSalary);
        }
    }
    

    JoinPoint對象封裝了Spring Aop中切面方法的信息,在切面方法中添加JoinPoint參數,就可以獲取到封裝了該方法信息的JoinPoint對象.

    常用API

    方法名 功能
    Signature getSignature(); 獲取封裝了署名信息的對象,在該對象中可以獲取到目標方法名,所屬類的Class等信息
    Object[] getArgs(); 獲取傳入目標方法的參數對象
    Object getTarget(); 獲取被代理的對象
    Object getThis(); 獲取代理對象
  • 測試類進行測試

    package com.aop.annotation;
    
    import org.junit.Test;
    import org.junit.runner.RunWith;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.context.ApplicationContext;
    import org.springframework.context.annotation.AnnotationConfigApplicationContext;
    import org.springframework.context.annotation.ImportResource;
    import org.springframework.test.context.ContextConfiguration;
    import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
    
    import static org.junit.Assert.assertNotNull;
    
    /**
     * @author :苑勇
     * @date :Created in 2020/5/25 21:04
     * @description:測試類
     */
    public class AnnotationTest {
        @Test
        public void test(){
            AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(JavaConfig.class);
    
            Department department = (Department) context.getBean("departmentImpl");
            department.developerSalary(2000.0,20.2);
            department.salerSalary(2000.0,50.5);
        }
    }
    
  • 輸出結果

    ------------------------------------------
    developerSalary本月工資爲:2161.6
    developerSalary公積金繳納:172.928
    developerSalary社保繳納爲:129.696
    developerSalary事發薪資爲:1858.976
    ------------------------------------------
    salerSalary本月工資爲:2303.0
    salerSalary公積金繳納:184.24
    salerSalary社保繳納爲:138.18
    salerSalary事發薪資爲:1980.5800000000002
    
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章