1. 程式人生 > 其它 >LAMP的介紹與搭建

LAMP的介紹與搭建

Spring

資料: https://pan.baidu.com/s/1aS4B69iA8-AtXqT7D9obXA 提取碼: rczx

  • Spring 是一個開源框架

  • Spring為簡化企業級開發而生,使用Spring,javaBean就可以實現很多以前要靠EJB才能實現的功能。同樣的功能,在EJB中要通過繁瑣的配置和複雜的程式碼才能實現,而在Spring中卻非常的優雅和簡潔。

  • Spring是一個IOC(DI)和AOP容器框架

  • Spring的優良特性

    1. 非侵入式
      基於Spring開發的應用中的物件可以不依賴於Spring的API
    2. 依賴注入
      DI——Dependency Injection,反轉控制(IOC)最經典的實現
    3. 面向切面程式設計
      Aspect Oriented Programming——AOP
    4. 容器
      Spring是一個容器,因為它包含並且管理應用物件的生命週期
    5. 元件化
      Spring實現了使用簡單的元件配置組合成一個複雜的應用。在Spring中可以使用XML和Java註解組合這些物件
    6. 一站式
      在IOC和AOP的基礎上可以整合各種企業應用的開源框架和優秀的第三方類庫(實際上Spring自身也提供了表述層的SpringMVC和持久層的SpringJDBC)

Spring 模組化分

  1. Test
    Spring單元測試模組

  2. Core Container

    核心容器(IOC),黑色代表這部分的功能由哪些jar包組成。要使用這部分的完整功能,這些jar包都要匯入。

  3. AOP、Aspects
    面向切面程式設計模組

  4. Data Access/Integration
    資料訪問

  5. Web
    Spring開發web應用模組

IOC和DI

  • IOC 容器
    可以用來整合大多數第三方框架
  • AOP 面向切面程式設計
    通常用來完成一些,宣告式事務。例如操作資料庫等…

IOC

IOC全稱(Inversion of control或控制反轉)。

  • 控制
    指資源的獲取方式,獲取方式又分為一下兩點

    1. 主動式:
      要什麼資源都需要自己建立。這種方式在建立簡單物件時非常方便, 但在建立複雜物件時,是非常麻煩的。

      class TestClass {
        Object obj = new Object();
      }
      
    2. 被動式
      資源的獲取不是我們自己建立,而是交給容器建立和設定。

      class TestClass {
        Object obj;
        
        public void test() {
          obj.xxx();
        }
      }
      

      容器管理所有的元件。假設TestClass受容器管理,Object也受容器管理,容器可以自動的檢查出哪些元件需要用到另一些元件。容器會幫我們創建出TestClass物件並把Object物件賦值過去。

  • 反轉
    主動獲取資源變為被動的接受資源

DI

DI(Dependency Injection或依賴注入)。容器能知道哪些元件執行的時候需要使用到另外一個元件。容器會通過反射的方式將容器中準備好的Object注入到TestClass

以前是自己new物件,現在所有的物件交給容器建立。「給容器中註冊元件

只要是容器管理的元件,都能使用容器提供的強大功能。

HelloWorld

導包

需要匯入所有核心jar包

commons-logging-1.1.3.jar
spring-beans-4.0.0.RELEASE.jar
spring-context-4.0.0.RELEASE.jar
spring-core-4.0.0.RELEASE.jar
spring-expression-4.0.0.RELEASE.jar

編寫配置檔案

spring的配置檔案中,集合了spring的ioc容器管理的所有元件。以下為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標籤可以註冊一個元件

package top.clover.spring.bean;
public class Person {

    private String name;

    private Integer age;

    private String gender;

    private String email;

}

在Bean標籤中,有一個class,它表示你要註冊哪一個元件。這裡需要的是一個全類名。

在這個bean標籤中,還有一個id,這是這個元件的唯一標識。

<?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元件 -->
    <bean class="top.clover.spring.bean.Person" id="person"/>
</beans>

如果你還想為元件賦值,可以使用property標籤。name表示屬性名,value表示屬性值。

<bean class="top.clover.spring.bean.Person" id="person">
  <property name="age" value="19"/>
  <property name="name" value="clover"/>
  <property name="gender" value="男"/>
  <property name="email" value="[email protected]"/>
</bean>

除了呼叫set進行賦值外,還可以使用constructor-arg標籤呼叫有參構造器進行賦值。

public Person(String name, Integer age, String gender, String email) {
  this.name = name;
  this.age = age;
  this.gender = gender;
  this.email = email;
}
<bean class="top.clover.spring.bean.Person" id="person">
    <constructor-arg name="name" value="Clover"/>
    <constructor-arg name="age" value="19"/>
    <constructor-arg name="gender" value="男"/>
    <constructor-arg name="email" value="[email protected]"/>
</bean>

constructor-arg也可以使用索引進行賦值

<bean class="top.clover.spring.bean.Person" id="person1">
  <constructor-arg index="0" value="Clover"/>
  <constructor-arg index="1" value="19"/>
  <constructor-arg index="2" value="男"/>
  <constructor-arg index="3" value="[email protected]"/>
</bean>

編寫測試類

在配置檔案寫好後,可以使用JUnit來進行測試

@Test
public void getPersonBean() {
  ApplicationContext app = new ClassPathXmlApplicationContext("ioc.xml");
  Person bean = app.getBean(Person.class);
  System.out.println(bean.getAge());
}

ApplicationContext代表ioc容器。在spring中,有很多種容器,其中有兩個最為常用org.springframework.context.support.ClassPathXmlApplicationContextorg.springframework.context.support.FileSystemXmlApplicationContext

ClassPathXmlApplicationContext表示在xml配置檔案在當前專案類路徑下,FileSystemXmlApplicationContext 獲取其它位置的xml位置檔案。

app.getBean(Person.class);

可以通過類的方式獲取到對應的bean,也可以通過剛剛定義的id去獲取

app.getBean("person");

總結

  • 容器中物件的建立都是在容器建立完成的時候就已經建立好了。
  • 同一個元件在ioc容器中是單例項的
  • 如果元件並不在ioc中,則會出現NoSuchBeanDefinitionException異常,無論是以id的形式還是class的形式獲取。
  • getBean(Class<?>)的時候,如果ioc中存在多個同類型元件時,則會丟擲NoUniqueBeanDefinitionException異常
  • 如果使用property對bean賦值,那麼該屬性必須存在set方法。

P名稱空間

ioc可以通過p名稱空間進行賦值,在xml中名稱空間是用來防止標籤重複的。在beans標籤中新增xmlns:p="http://www.springframework.org/schema/p"。新增後就可以使用p名稱空間進行賦值。

<?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:p="http://www.springframework.org/schema/p"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
  <bean class="top.clover.spring.bean.Person" id="person"
        p:age="19" p:name="Clover"
        p:email="[email protected]" p:gender="男"/>
</beans>

複雜屬性賦值

null值

在java中,除了基本型別,其它型別在不賦值的情況下預設值都是null。如果需要給一個屬性null值,不能通過property標籤的value屬性來賦值,可以通過null標籤。

<bean class="top.clover.spring.bean.Person">
  <property name="name">
    <null/>
  </property>
</bean>

自定義複雜型別

public class Person {

  private String name;

  private Integer age;

  private String gender;

  private String email;

  private Car car;

  getter/setter....

}

Person中,存在一個Car型別,這個型別屬於我們自定義的複雜型別。可以使用兩種方式進行賦值

使用其它Bean

propertu中有一個ref屬性,這個屬性值必須是某一個bean的id,spring可以通過這個屬性將指定的bean對指定的屬性進行賦值。

<bean class="top.clover.spring.domain.Car" id="MyCar">
  <property name="cardName" value="小轎車"/>
  <property name="color" value="五彩斑斕的黑"/>
</bean>
<bean class="top.clover.spring.bean.Person">
  <property name="car" ref="MyCar"/>
</bean>
@Test
public void testBeanPropertyRef() {
  ApplicationContext app = new ClassPathXmlApplicationContext("ioc.xml");

  Person bean = app.getBean(Person.class);
  Car car = bean.getCar();
  System.out.println("汽車:" + car.getCardName());
  System.out.println("顏色:" + car.getColor());
}
汽車:小轎車
顏色:五彩斑斕的黑

引用內部bean

<bean class="top.clover.spring.bean.Person">
  <property name="car">
    <bean class="top.clover.spring.domain.Car" id="MyCar">
      <property name="cardName" value="小轎車"/>
      <property name="color" value="五彩斑斕的黑"/>
    </bean>
  </property>
</bean>

這種方式相當於car = new Car();。與外部無關,所以getBean的時候會找不到這個car

List型別

在ioc中對List型別進行賦值,可以使用list標籤。

<bean class="top.clover.spring.bean.Person">
  <property name="car">
    <list>
      <bean class="top.clover.spring.domain.Car" >
        <property name="cardName" value="小轎車"/>
        <property name="color" value="五彩斑斕的黑"/>
      </bean>
      <bean class="top.clover.spring.domain.Car" >
        <property name="cardName" value="小轎車"/>
        <property name="color" value="五彩斑斕的黑"/>
      </bean>
      <bean class="top.clover.spring.domain.Car" >
        <property name="cardName" value="小轎車"/>
        <property name="color" value="五彩斑斕的黑"/>
      </bean>
    </list>

  </property>
</bean>

除了內部Bean的方式,還可以使用ref標籤引用外部Bean。

<bean class="top.clover.spring.domain.Car" id="MyCar">
  <property name="cardName" value="小轎車"/>
  <property name="color" value="五彩斑斕的黑"/>
</bean>

<!-- 註冊一個bean元件 -->
<bean class="top.clover.spring.bean.Person">
  <property name="car">
    <list>
      <ref bean="MyCar"></ref>
      <ref bean="MyCar"></ref>
      <ref bean="MyCar"></ref>
      <ref bean="MyCar"></ref>
    </list>
  </property>
</bean>

Map型別

同樣的,使用List型別有對應的list標籤,那麼Map型別也有一個map標籤。spring底層使用的是LinkedHashMap

<bean class="top.clover.spring.bean.Person">
  <property name="maps">
    <map>
      <entry key="name" value="Clover" />
      <entry key="age" value="19" />
    </map>
  </property>
</bean>

一個entry對於一個KV,map也提供了一個value-ref屬性用於引用外部Bean。用法與其它ref屬性一致。

Util名稱空間

使用這個名稱空間,需要在beans中新增xmlns:util="http://www.springframework.org/schema/util",還需要在xsi中新增兩個與util名稱相關的連結http://www.springframework.org/schema/util, http://www.springframework.org/schema/util/spring-util-4.0.xsd。可以為MapListsetproperties等型別建立對應的Bean。

以map為例,util:map相當於new 一個LinkedHashMap。不需要再使用map標籤

<util:map id="diyMap">
  <entry key="name" value="clover"/>
  <entry key="age" value="19"/>
</util:map>
ApplicationContext app = new ClassPathXmlApplicationContext("ioc.xml");
Map<String, Object> diyMap =(Map<String, Object>) app.getBean("diyMap");
System.out.println(diyMap);
{name=clover, age=19}

Bean之間的依賴

<bean class="top.clover.spring.bean.Person" id="person"></bean>
<bean class="top.clover.spring.bean.Book" id="book"></bean>

Bean的建立預設是按照配置檔案中配置的先後順序。

person建立了...
book被建立...

如果你註冊的bean需要依賴到某個bean時,若是你的bean先被建立,可能會發生不可預料的錯誤。那麼這時候就需要用到spring提供的depends-on屬性。它需要一個id,也可以通過,隔開傳多個id。表示我當前這個bean依賴到了xxx。

<bean class="top.clover.spring.bean.Person" id="person" depends-on="book"></bean>
<bean class="top.clover.spring.bean.Book" id="book"></bean>
book被建立...
person建立了...

Bean的多例項和單例項

在spring中,bean預設是單例項,若想講bean組冊為多例項,需要使用bean標籤提供的scope屬性。該屬性有兩個屬性值

  • prototype

    表示當前bean是多例項的

    1. 容器啟動預設不會去建立bean
    2. 多例項bean只有在獲取的時候被建立
    3. 每次獲取都會建立一個新的物件
  • singleton
    表示當前bean是單例項的

    1. 在容器啟動完成之前就已經建立好物件儲存在容器中了。
    2. 在任何時候獲取都是獲取之前建立好的那個物件。

靜態工廠和例項工廠

public class AirPlane {

  /**
     * 機長名稱
     */
  private String captainName;

  /**
     * 副駕駛
     */
  private String coPilot;

  /**
     * 發動機
     */
  private String engine;

  /**
     * 載客量
     */
  private Integer total;

}

工廠模式

工廠是一個專門幫我們建立物件的類

靜態工廠

工廠本身不需要建立物件。都是通過靜態方法呼叫:obj = xxxFactory.getxxx(String className)

public class AirPlaneStaticFactory {

  /**
     * 飛機工廠
     * @param captainName 機長名稱
     * @return 飛機例項
     */
  public static AirPlane getAirPlane(String captainName) {
    AirPlane airPlane = new AirPlane();
    airPlane.setCaptainName(captainName);
    airPlane.setCoPilot("Clover");
    airPlane.setEngine("太行");
    airPlane.setTotal(300);
    return airPlane;
  }

}

通過IOC呼叫靜態工廠註冊Bean

建立好工廠後,需要通過bean去呼叫這個工廠,最後將工廠的返回值註冊為一個Bean物件。

bean標籤class屬性指向工廠全類名,需要配合factory-method屬性讓ioc呼叫工廠方法。如果工廠方法需要傳參,可以使用constructor-arg賦值。

<bean id="AirPlane" class="top.clover.spring.factory.AirPlaneStaticFactory" factory-method="getAirPlane">
  <constructor-arg name="captainName" value="張三"/>
</bean>
@Test
public void testAirPlaneStaticFactory() {
    AirPlane airPlaneBean = (AirPlane)app.getBean("AirPlaneByAirPlaneStaticFactory");
    System.out.println("通過靜態工廠建立的bean物件:" + airPlaneBean);
}
通過靜態工廠建立的bean物件:AirPlane{captainName='張三', coPilot='Clover', engine='太行', total=300}

例項工廠

工廠本身需要建立物件:obj = new xxxFactory();

public class AirPlaneInstanceFactory {

  /**
     * 飛機工廠
     *
     * @param captainName 機長名稱
     * @return 飛機例項
     */
  public AirPlane getAirPlane(String captainName) {
    AirPlane airPlane = new AirPlane();
    airPlane.setCaptainName(captainName);
    airPlane.setCoPilot("Clover");
    airPlane.setEngine("太行");
    airPlane.setTotal(300);
    return airPlane;
  }

}

通過IOC呼叫例項工廠註冊Bean

  1. 先配置出例項工廠物件
  2. 配置要建立的AirPlane使用哪個工廠建立
<!-- 建立一個例項工廠 -->
<bean id="airPlaneInstanceFactory" class="top.clover.spring.factory.AirPlaneInstanceFactory"/>
<!-- 使用指定工廠建立物件 -->
<bean id="airPlaneByAirPlaneInstanceFactory" factory-bean="airPlaneInstanceFactory" factory-method="getAirPlane">
  <constructor-arg name="captainName" value="李四"/>
</bean>

通過factory-bean屬性來指定使用哪個工廠例項建立bean

@Test
public void testAirPlaneInstanceFactory() {
  AirPlane airPlaneBean = (AirPlane)app.getBean("airPlaneByAirPlaneInstanceFactory");
  System.out.println("通過例項工廠建立的bean物件:" + airPlaneBean);
}
通過例項工廠建立的bean物件:AirPlane{captainName='李四', coPilot='Clover', engine='太行', total=300}

FactoryBean

FactoryBean是Spring規定的一個介面,只要是這個介面介面的實現類,Spring都認為是一個工廠。

  • getObject 獲取工廠建立的物件
  • getObjectType 獲取物件型別
  • isSingleton 當前工廠註冊的Bean是否為單例

FactoryBean建立的物件不管是單例項還是多例項,都是在獲取的時候才建立物件。

public class MyFactoryBean implements FactoryBean<AirPlane> {
 
  @Override
  public AirPlane getObject() throws Exception {
    AirPlane airPlane = new AirPlane();
    airPlane.setCaptainName("王五");
    airPlane.setCoPilot("Clover");
    airPlane.setEngine("太行");
    airPlane.setTotal(300);
    return airPlane;
  }

  @Override
  public Class<?> getObjectType() {
    return AirPlane.class;
  }

  @Override
  public boolean isSingleton() {
    return true;
  }

}

實現了FactoryBean介面後還需要在配置檔案中進行註冊

<bean id="airPlaneBySpringFactoryBean" class="top.clover.spring.bean.MyFactoryBean"/>
@Test
public void testMyFactoryBean() {
  AirPlane airPlaneBean = (AirPlane)app.getBean("airPlaneBySpringFactoryBean");
  System.out.println(
    "通過FactoryBean建立的bean物件:" + airPlaneBean);
}
通過FactoryBean建立的bean物件:AirPlane{captainName='王五', coPilot='Clover', engine='太行', total=300}

Bean的後置處理器

  • 編寫後置處理器實現類BeanPostProcessor
  • 將後置處理器註冊在配置檔案中
public class MyBeanPostProcessor implements BeanPostProcessor {

  /**
     * bean在初始化的時候會呼叫這個方法
     * @param bean bean物件
     * @param beanName bean註冊的名稱
     * @return 要註冊的bean物件
     */
  @Override
  public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
    System.out.println("「"+ bean +"」bean準備開始初始化了 --->> " + beanName);
    return bean;
  }

  /**
     * 初始化方法之後被呼叫
     * @param bean bean物件
     * @param beanName bean名稱
     * @return 要註冊的bean物件
     */
  @Override
  public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
    System.out.println("「"+ bean +"」bean初始化方法呼叫完了 --->> " + beanName);
    return bean;
  }

}

在配置檔案註冊

<bean class="top.clover.spring.bean.MyBeanPostProcessor"/>
「AirPlane{captainName='張三', coPilot='Clover', engine='太行', total=300}」bean準備開始初始化了 --->> airPlaneByAirPlaneStaticFactory
「AirPlane{captainName='張三', coPilot='Clover', engine='太行', total=300}」bean初始化方法呼叫完了 --->> airPlaneByAirPlaneStaticFactory

Dao、Controller、Service

通過給bean上新增某些註解,可以快速的將bean加入到ioc容器中。
分別使用Spring提供的這四個註解

  • @Controller
    用來給控制層加上

  • @Service
    業務邏輯層的元件新增這個註解

  • @Repository
    給資料庫層(持久化層/Dao層)新增這個註解

  • @Component

    可以給有功能的類新增上這個註解,表示這個類是一個元件

註解可以隨便加,Spring底層不會去驗證你的這個元件是否如你註解所說就是一個dao層或者就是一個servlet層的元件。

使用註解將元件快速的加入到容器需要這幾步:

  1. 給元件上標註所需註解
  2. 告訴Spring自動掃描加了註解的元件。這個功能依賴了context名稱空間
  3. 匯入aop包,這個包支援註解模式

在配置檔案中加入context名稱空間:xmlns:context="http://www.springframework.org/schema/context"

加入之後這個名稱空間提供了一個context:compnent-scan標籤,用來自動掃描元件。這個標籤中有一個base-package屬性,用來指定掃描的基礎包,把基礎包及子包下的所有加了註解的類自動的掃描進ioc容器中。

預設註冊為單例模式。若想註冊為多例,需要使用@Scope註解

@Autowired自動裝配

@Autowired標註的屬性,spring會根據型別到ioc容器中查詢並賦值給被標註的屬性。前提是你這個屬性所在的類和目標型別已經被註冊進容器中。

@Controller
public class BookController {
  
  @Autowired
  private BookService bookService;

  public void doGet() {
    bookService.saveBook();
  }

}
@Service
public class BookService {
  
  @Autowired
  private BookDao bookDao;

  public void saveBook() {
    System.out.println("呼叫bookDao中的saveBook儲存圖書....");
    bookDao.saveBook();
  }

}
@Repository
public class BookDao {

  public void saveBook() {
    System.out.println("儲存書籍成功....");
  }

}

測試這個controller

/**
 * 測試自動注入
 */
@Test
public void testAutowired() {
  BookController controller = app.getBean(BookController.class);
  controller.doGet();
}
呼叫bookDao中的saveBook儲存圖書....
儲存書籍成功....

在方法上使用

@Autowired可以標註在很多地方

@Target({ElementType.CONSTRUCTOR, ElementType.FIELD, ElementType.METHOD, ElementType.ANNOTATION_TYPE})

如果作用在方法上

  • 這個方法會在bean建立的時候自動執行
  • 這個方法上的每一個引數都會自動注入
@Controller
public class BookController {
  
  @Autowired
  public void testAutowired(BookService bookService) {
    System.out.println("容器建立,autowired執行:---》"+bookService);
  }

}
容器建立,autowired執行:---》top.ctong.service.BookService@5e21e98f

大致原理

  • @Autowired 預設按照型別去容器中找到對應的元件
    • 找到一個就直接賦值
    • 沒有找到就拋異常BeanCreationException
    • 如果找到多個
      1. 會根據變數名最為id再找一次app.getBean(String id)
      2. 如果這時候還是匹配不上,那麼還是丟擲錯誤。

@Qualifier 可以修改@Autowired的哦人行為,@Autowired會根據@Qualifier 的值進行查詢。app.getBean(String id);


自動裝配有三種

  1. @Autowired
    這是spring自己的註解,相比其它兩個,它更強大
  2. @Resource
    這是javax提供的,是一個標準。相比其它,他的擴充套件性更強。如果換成其它框架,@Resource還可以使用,@Autowired可能就不行了。
  3. @Inject

泛型注入

在同類型元件中,@Autowired可以根據泛型去匹配

例如有一個BaseDao

public abstract class BaseDao<T>  {
  abstract void save();
}

有兩個類實現BaseDao

@Repository
public class BookDao extends BaseDao<Book> {
  @Override
  public void save() {
    System.out.println("儲存圖書");
  }
}
@Repository
public class UserDao extends BaseDao<User> {
  @Override
  public void save() {
    System.out.println("使用者儲存");
  }
}

有一個BaseService去使用泛型注入對應Dao

public class BaseService<T> {

  @Autowired
  private BaseDao<T> baseDao;

  public void save() {
    System.out.println("baseDao型別是:" + baseDao);
  }

}

分別有UserServiceBookService繼承BaseService

@Service
public class BookService extends BaseService<Book>{
  public void saveBook() {
    System.out.println("呼叫bookDao中的saveBook儲存圖書....");
  }
}
@Service
public class UserService extends BaseService<User>{
  public UserService() {
    // 防止存在一個或多個有參構造器時反射通過無參構造起例項化發生異常
  }
}

最後測試結果

@Test
public void testBaseService() {
  UserService userBean = app.getBean(UserService.class);
  BookService BookBean = app.getBean(BookService.class);
  userBean.save();
  BookBean.save();
}
baseDao型別是:top.ctong.dao.UserDao@6692b6c6
baseDao型別是:top.ctong.dao.BookDao@1cd629b3

AOP

AOP(Aspect Oriented Programming或面向切面程式設計) 。這是一種新的程式設計思想。他不是用來替代OOP的,他是一種基於OOP基礎之上衍生出的一種新的思想。

面向切面程式設計是指在程式執行期間,將某段程式碼動態的切入到指定方法的指定位置進行執行。

AOP與動態代理

  1. 動態代理寫起來程式碼複雜
  2. jdk預設的動態代理,如果目標物件沒有實現任何介面,是無法為它建立代理物件的。
  3. spring實現的AOP功能,底層就是動態代理
  4. spring可以一句程式碼都不寫的去建立動態代理
  5. spring實現簡單,而且沒有強制要求目標物件必須實現介面
  6. 原生動態代理就是切面程式設計,而Spring簡化了面向切面程式設計

AOP的幾個專業術語

  • 橫切關注點
    每一個方法的開始

  • 通知方法
    每一個橫切關注點觸發的方法

  • 切面類
    通知方法所在的類

  • 連線點
    每一個方法的每一個位置都是一個連線點

  • 切入點

    在眾多連線點中選出我們感興趣的地方。例如:方法結束需要記錄日誌,方法異常需要記錄日誌,方法返回需要記錄日誌的地方。

  • 切入點表示式

導包

若想spring支援面向切面程式設計,需要匯入spring-aspects-4.0.0.RELEASE.jar,這是spring提供的基礎版jar包。

由於spring提供的包比較基礎,可以使用第三方加強包「即使目標物件沒有實現任何介面也能建立動態代理」。

com.springsource.org.aopalliance-1.0.0.jar
com.springsource.net.sf.cglib-2.2.0.jar
com.springsource.org.aspectj.weaver-1.6.8.RELEASE.jar

配置

  • 將目標類和切面類(裝了通知方法的類)加入到ioc容器

    @Service
    public class MyMathCalculator implements Calculator {
    
      @Override
      public int add(int i, int j) {
        return i + j;
      }
    
      @Override
      public int sub(int i, int j) {
        return i - j;
      }
    
      @Override
      public int mul(int i, int j) {
        return i * j;
      }
    
      @Override
      public int div(int i, int j) {
        return i / j;
      }
    
    }
    
  • 使用@Aspect註解告訴spring哪個是切面類

    @Aspect
    @Component
    public class LogUtils {
      /**
         * 方法執行之前的日誌記錄器
         */
      @Before("execution(public int top.ctong.aop.impl.MyMathCalculator.*(int, int))")
      public static void methodStartBefore() {
        System.out.println("方法執行前....");
      }
    
      @After("execution(public int top.ctong.aop.impl.MyMathCalculator.*(int, int)))")
      public static void methodAfter() {
        System.out.println("方法執行完成....");
      }
    
      @AfterReturning("execution(public int top.ctong.aop.impl.MyMathCalculator.*(int, int)))")
      public static void methodReturning() {
        System.out.println("方法執行返回值....");
      }
    
      @AfterThrowing("execution(public int top.ctong.aop.impl.MyMathCalculator.*(int, int)))")
      public static void methodException() {
        System.out.println("方法執行完成....");
      }
    }
    
  • 告訴spring切面類裡面的每一個方法都什麼時候執行。

    1. @Before 在目標方法執行之前執行「前置通知」
    2. @After在目標方法結束之後執行「後置通知」
    3. @AfterReturning 在目標方法正常返回之後執行「返回通知」
    4. @AfterThrowing 在目標方法丟擲異常之後執行「異常通知」
    5. @Around 環繞「環繞通知」
  • 標註好所需註解後,還需要告訴spring,你這個切面在哪裡執行,需要寫切入點表示式

    excution(訪問許可權符 返回值型別 方法簽名)
    
  • 在配置檔案中 開啟基於註解的AOP功能

    <?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:context="http://www.springframework.org/schema/context"
           xmlns:aop="http://www.springframework.org/schema/aop"
           xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
           http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.0.xsd
           http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd">
        <context:component-scan base-package="top.ctong"/>
        <aop:aspectj-autoproxy />
    </beans>
    

測試

@Test
public void testLogUtilsAspect() {
  Calculator bean = this.app.getBean(Calculator.class);
  bean.add(1,1);
}
方法執行前....
方法執行完成....
方法執行返回值....

注意:當前bean還是一個代理物件,因為AOP底層就是原生Proxy,所以不能通過MyMathCalculator.class來獲取。但當被代理類沒有任何父類的時候,又可以通過MyMathCalculator.class進行獲取。

在沒有任何父類的時候,是cglib幫我們建立的代理物件。cglib為我們建立的一個內部類,內部類中實現了所有MyMathCalculator的方法

class top.ctong.aop.impl.MyMathCalculator$$EnhancerByCGLIB$$1a9d9ab2

切入點表示式

切入點表示式的固定寫法

excution(訪問許可權符 返回值型別 方法簽名(引數列表))

萬用字元

  • *
    1. 匹配一個或多個字元
    2. 匹配任意一個引數
    3. 如果放在路徑上,那麼只能匹配一層路徑
    4. 許可權不能表示
  • ..
    1. 匹配任意多個引數,任意型別引數
    2. 匹配任意多層路徑
  • &&切入的位置要同時滿足兩種表示式
  • ||切入的位置最少要滿足一種表示式
  • !不滿足指定表示式觸發

抽取表示式

抽取可重用的切入點表示式

  1. 隨便宣告一個沒有實現的返回void的空方法。
  2. 給方法上標註@Pointcut註解
@Aspect
@Component
public class AspectLogUtils {

  @Pointcut("execution(* top.ctong..add(..))")
  public static void aspectAddMethod() {
    // 抽取切入點表示式
  }

  @After("aspectAddMethod()")
  public static void aspectAllAddMethod() {
    System.out.println("方法執行了...");
  }

}

目標方法資訊

只需要為通知方法的引數列表上寫一個JoinPoint型別的引數。它封裝了目標方法的詳細資訊

@After("execution(* top.ctong.aop.impl.MyMathCalculator.*(..))")
public static void methodAfter(JoinPoint joinPoint) {
  // 獲取方法簽名
  Signature signature = joinPoint.getSignature();
  System.out.println("「" + signature.getName() + "」方法執行完成....使用的引數:「" + Arrays.asList(joinPoint.getArgs()) + "」");
}
「add方法執行完成....使用的引數:「[1, 1]」

獲取方法返回值,需要在通知註解中指定,在@AfterReturning註解中,有一個returning屬性,用來指定引數名

@AfterReturning(value = "execution(public int top.ctong.aop.impl.MyMathCalculator.*(int, int)))", returning = "result")
public static void methodReturning(JoinPoint joinPoint, Object result) {
  // 獲取方法簽名
  Signature signature = joinPoint.getSignature();
  System.out.println("「" + signature.getName() + "」方法執行返回值===>>" + result);
}

獲取異常和獲取返回值操作一樣

@AfterThrowing(value = "execution(* top.ctong.aop.impl.MyMathCalculator.*(..))", throwing = "exception")
public static void methodException(Exception exception) {
  exception.getCause();
}

Around環繞通知

@Around是spring最強大的通知。它相當於我們手寫動態代理。

@Around("aspectAddMethod()")
public Object aspectAround(ProceedingJoinPoint pjp) throws Throwable {
  Object[] args = pjp.getArgs();
  // 類似method.invoke()
  return pjp.proceed(args);
}

基於配置檔案的AOP

<?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"
       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">
  <bean id="myMathCalculator" class="top.ctong.utils.MyMathCalculator"></bean>
  <bean id="aspectLogUtils" class="top.ctong.utils.AspectLogUtils"/>

  <aop:config>
    <!--  指定切面  -->
    <aop:aspect ref="aspectLogUtils">
      <!-- 抽取表示式 -->
      <aop:pointcut id="MyMathCalculatorAllMethod" expression="execution(* top.ctong..MyMathCalculator.*(..))"/>
      <aop:before method="aspectBefore" pointcut="execution(* top.ctong..MyMathCalculator.*(..))"/>
      <aop:after method="aspectAfter" pointcut-ref="MyMathCalculatorAllMethod"/>
      <aop:after-throwing method="aspectThrowing" pointcut-ref="MyMathCalculatorAllMethod"/>
      <aop:after-returning method="aspectReturning" pointcut-ref="MyMathCalculatorAllMethod"/>
    </aop:aspect>
  </aop:config>
</beans>

若想使用基於配置檔案的aop,需要引入aop空間。

  • aop:aspect指定切面類
  • aop:pointcut抽取切入點表示式,這個標籤如果放在aop:aspect中,那麼只能在當前切面可使用,若想全域性使用,可以將其放置在切面外,也就是aop:config直接子級。
  • aop:before指定前置切入
  • aop:after指定後置切入
  • aop:after-throwing 指定異常切入
  • aop:after-returning指定返回時切入

SpringIOC原始碼

  1. IOC是一個容器
  2. 容器啟動的時候建立所有單例項物件
  3. 我們可以直接從容器中獲取到這個物件

SpringIOC啟動過程

  • ClassPathXmlApplicationContext所有構造器呼叫的都是這個構造器

    public ClassPathXmlApplicationContext(String[] configLocations, boolean refresh, ApplicationContext parent)
      throws BeansException {
    
      super(parent);
      setConfigLocations(configLocations);
      if (refresh) {
        refresh();
      }
    }
    
  • 在構造器中,最重要的程式碼是 refresh();。它負責建立所有單例項bean和啟動容器

    1. 在第一行程式碼中,synchronized (this.startupShutdownMonitor) 有個同步鎖,是為了保證在多執行緒環境中IOC容器只被建立一次。

      生產環境預設是多執行緒的。

    2. prepareRefresh();解析配置檔案,以準備重新整理上下文

    3. ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory();準備一個Bean工廠,在此工廠中準備好了待初始化的Bean、系統環境資訊等基礎資訊用於後來建立Bean。也就是說在這一步已經解析好了xml配置檔案。

    4. prepareBeanFactory(beanFactory);設定工廠的所有可能用到的外接工廠,例如註解解析器,類載入器等其它工廠

    5. postProcessBeanFactory(beanFactory)工廠的後置處理設定

    6. invokeBeanFactoryPostProcessors(beanFactory);呼叫bean工廠的後置處理器

    7. registerBeanPostProcessors(beanFactory);註冊所有BeanPostProcessor.class型別的Bean。也就是說在這一步把所有BeanPostProcessor.class例項化。

    8. initMessageSource();支援國際化語言服務

    9. initApplicationEventMulticaster(); 初始化事件傳喚器,spring有建立、銷燬bean時會產生非常多的事件,轉換器是為了能讓其它元件感知到spring觸發了哪個事件。

    10. onRefresh();這是一個空的方法,留給子類的一個初始化方法。例如你重寫IOC容器,可以重寫這個方法進行初始化某些必要的Bean。

    11. registerListeners();註冊監聽器,註冊的是ApplicationListener.class型別的監聽器,這是一個spring事件監聽器。

    12. finishBeanFactoryInitialization(beanFactory);完成所有剩餘的bean例項的初始化

      1. beanFactory.preInstantiateSingletons();初始化所有剩餘的bean

        org.springframework.beans.factory.support.DefaultListableBeanFactory#preInstantiateSingletons

        1. RootBeanDefinition bd = getMergedLocalBeanDefinition(beanName);根據id拿到Bean的定義資訊。裡面包含了bean所有的詳細資訊。

        2. if (!bd.isAbstract() && bd.isSingleton() && !bd.isLazyInit()) { ... }如果當前bean不是一個抽象類,並且是單例項和不是懶載入。那麼就建立它。

          懶載入就是在使用的時候才建立。

        3. if (isFactoryBean(beanName)) {...} else {getBean(beanName);}如果是工廠bean那麼就通過工廠去建立這個bean。否則直接建立。

        4. 所有的getBean呼叫的都是org.springframework.beans.factory.support.AbstractBeanFactory#doGetBean,直接看它就好

          1. final String beanName = transformedBeanName(name);獲取Bean名稱

          2. Object sharedInstance = getSingleton(beanName);從單例快取池中檢查是否存在這個bean。如果存在就直接拿這個bean。

          3. final RootBeanDefinition mbd = getMergedLocalBeanDefinition(beanName);獲取當前bean的定義資訊。

          4. String[] dependsOn = mbd.getDependsOn();獲取當前bean是有依賴了誰。也就是在xml檔案中bean標籤的depends-on屬性。如果有那麼迴圈它,再呼叫getBean方法。也就是說,先建立好所依賴的物件再建立自己。

            String[] dependsOn = mbd.getDependsOn();
            if (dependsOn != null) {
              for (String dependsOnBean : dependsOn) {
                if (isDependent(beanName, dependsOnBean)) {
                  throw new BeanCreationException("Circular depends-on relationship between '" +
                                                  beanName + "' and '" + dependsOnBean + "'");
                }
                registerDependentBean(dependsOnBean, beanName);
                getBean(dependsOnBean);
              }
            }
            
          5. sharedInstance = getSingleton(...) 建立bean。

            1. singletonObject = singletonFactory.getObject();一系列驗證當前bean不存在之後開始建立。

              org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory#doCreateBean

              通過反射建立:

              org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory#createBeanInstance

            2. addSingleton(beanName, singletonObject);將建立好的bean物件放到單例項快取池中

              在專案中使用getBean其實也是在這個快取池中獲取對應的bean

BeanFactory和ApplicationContext的區別

ApplicationContext是BeanFactory的子介面

  • BeanFactory是一個工廠介面,也是Spring最底層的介面。負責建立Bean例項,容器裡面儲存的所有單例bean其實都是在一個map中。
  • ApplicationContext是容器介面,更多的是負責容器功能的實現。(可以基於BeanFactory建立好的物件之上完成強大的容器)容器可以從map中獲取這個bean,並且aop、di在ApplicationContext介面的下面的這些類裡面。

BeanFactory是最底層的介面,而ApplicationContext更多是留給我們使用的ioc容器介面。