spring第三天
一、AOP的相關概念
1.1AOP概述
1.1.1什麼是AOP
AOP:全稱是Aspext Oriented Programming即:面向切面程式設計。 簡單的說它就是我們程式重複的代碼抽取出來,在需要執行的時候,使用動态代理的技術,在不修改源碼的基礎上,對我們的已有方法進行增強。
1.1.2AOP的作用及優勢
作用:
在程式運作期間,不修改源碼對已有方法進行增強。
優勢:
減少重複代碼 提高開發效率 維護友善 1.1.3AOP的實作方式
使用動态代理技術 1.2AOP的具體應用
1.2.1案例中問題
這是我們之前在struts2課程中做的一個完整的增删改查例子。下面是客戶的業務層接口和實作類。
客戶的業務層接口
/**
* 客戶的業務層接口
*/
public interface ICustomerService {
/**
* 儲存客戶
* @param customer
*/
void saveCustomer(Customer customer);
/**
* 查詢所有客戶
* @return
*/
List<Customer> findAllCustomer();
/**
* 删除客戶
* @param customer
*/
void removeCustomer(Customer customer);
/**
* 根據id查詢客戶
* @param custId
* @return
*/
Customer findCustomerById(Long custId);
/**
* 修改客戶
* @param customer
*/
void updateCustomer(Customer customer);
} 客戶的業務層實作類
/**
* 客戶的業務層實作類
* 事務必須在此控制
* 業務層都是調用持久層的方法
*/
public class CustomerServiceImpl implements ICustomerService { private ICustomerDao customerDao = new CustomerDaoImpl();
@Override
public void saveCustomer(Customer customer) {
Session s = null;
Transaction tx = null;
try{
s = HibernateUtil.getCurrentSession();
tx = s.beginTransaction();
customerDao.saveCustomer(customer);
tx.commit();
}catch(Exception e){
tx.rollback();
throw new RuntimeException(e);
}
} @Override
public List<Customer> findAllCustomer() {
Session s = null;
Transaction tx = null;
try{
s = HibernateUtil.getCurrentSession();
tx = s.beginTransaction();
List<Customer> customers = customerDao.findAllCustomer();
tx.commit();
return customers;
}catch(Exception e){
tx.rollback();
throw new RuntimeException(e);
}
} @Override
public void removeCustomer(Customer customer) {
Session s = null;
Transaction tx = null;
try{
s = HibernateUtil.getCurrentSession();
tx = s.beginTransaction();
customerDao.removeCustomer(customer);
tx.commit();
}catch(Exception e){
tx.rollback();
throw new RuntimeException(e);
}
} @Override
public Customer findCustomerById(Long custId) {
Session s = null;
Transaction tx = null;
try{
s = HibernateUtil.getCurrentSession();
tx = s.beginTransaction();
Customer c = customerDao.findCustomerById(custId);
tx.commit();
return c;
}catch(Exception e){
tx.rollback();
throw new RuntimeException(e);
}
} @Override
public void updateCustomer(Customer customer) {
Session s = null;
Transaction tx = null;
try{
s = HibernateUtil.getCurrentSession();
tx = s.beginTransaction();
customerDao.updateCustomer(customer);
tx.commit();
}catch(Exception e){
tx.rollback();
throw new RuntimeException(e);
}
}
} 1.2.2動态代理回顧
1.2.2.1動态代理的特點
位元組碼随用随建立,随用随加載。
它與靜态代理的差別也在于此。因為靜态代理是位元組碼一上來就建立好,并完成加載。
裝飾者模式就是靜态代理的一種展現。 1.2.2.2動态代理常用的有兩種方式
基于接口的動态代理
提供者:JDK官方的Porxy類
要求:被代理者最少實作一個接口 基于子類的動态代理
提供者:第三方的CGLib,如果報asmxxxx異常,需要導入asm.jar。
要求:被代理類不能用final修飾的類(最終類) 1.2.2.3使用JDK官方的Proxy類建立代理對象
此時就是一個演員的例子:
很久以前,演員和劇組是直接聯系見面的,沒有中間人環節。
而随着時間的推移,産生了一個新興行業:經紀人(中間人),這個時候劇組再想找演員就需要通過經紀人來找了。下面我們就用代碼示範出來。
/**
* 一個經紀公司的要求:
* 能做基本的表演和危險的表演
*/
public interface IActor {
/**
* 基本演出
* @param money
*/
public void basicAct(float money);
/**
* 危險演出
* @param money
*/
public void dangerAct(float money);
} /**
* 一個演員
*/
//實作了接口,就表示具有接口中的方法實作。即:符合經紀公司的要求
public class Actor implements IActor{
public void basicAct(float money){
System.out.println("拿到錢,開始基本的表演:"+money);
}
public void dangerAct(float money){
System.out.println("拿到錢,開始危險的表演:"+money);
}
} public class Client {
public static void main(String[] args) {
//一個劇組找演員:
final Actor actor = new Actor();//直接
/**
* 代理:
* 間接。
* 擷取代理對象:
* 要求:
* 被代理類最少實作一個接口
* 建立的方式
* Proxy.newProxyInstance(三個參數)
* 參數含義:
* ClassLoader:和被代理對象使用相同的類加載器。
* Interfaces:和被代理對象具有相同的行為。實作相同的接口。
* InvocationHandler:如何代理。
* 政策模式:使用場景是:
* 資料有了,目的明确。
* 如何達成目标,就是政策。
*
*/
IActor proxyActor = (IActor) Proxy.newProxyInstance(
actor.getClass().getClassLoader(),
actor.getClass().getInterfaces(),
new InvocationHandler() {
/**
* 執行被代理對象的任何方法,都會經過該方法。
* 此方法有攔截的功能。
*
* 參數:
* proxy:代理對象的引用。不一定每次都用得到
* method:目前執行的方法對象
* args:執行方法所需的參數
* 傳回值:
* 目前執行方法的傳回值
*/
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
String name = method.getName();
Float money = (Float) args[0];
Object rtValue = null;
//每個經紀公司對不同演出收費不一樣,此處開始判斷
if("basicAct".equals(name)){
//基本演出,沒有2000不演
if(money > 2000){
//看上去劇組是給了8000,實際到演員手裡隻有4000
//這就是我們沒有修改原來basicAct方法源碼,對方法進行了增強
rtValue = method.invoke(actor, money/2);
}
}
if("dangerAct".equals(name)){
//危險演出,沒有5000不演
if(money > 5000){
//看上去劇組是給了50000,實際到演員手裡隻有25000
//這就是我們沒有修改原來dangerAct方法源碼,對方法進行了增強
rtValue = method.invoke(actor, money/2);
}
}
return rtValue;
}
});
//沒有經紀公司的時候,直接找演員。
// actor.basicAct(1000f);
// actor.dangerAct(5000f);
//劇組無法直接聯系演員,而是由經紀公司找的演員
proxyActor.basicAct(8000f);
proxyActor.dangerAct(50000f);
}
} 1.2.2.4使用CGLib的Enhancer類建立代理對象
還是那個演員的例子,隻不過不讓他實作接口
/**
* 一個演員
*/
public class Actor{//沒有實作任何接口
public void basicAct(float money){
System.out.println("拿到錢,開始基本的表演:"+money);
}
public void dangerAct(float money){
System.out.println("拿到錢,開始危險的表演:"+money);
}
} public class Client {
/**
* 基于子類的動态代理
* 要求:
* 被代理對象不能是最終類
* 用到的類:
* Enhancer
* 用到的方法:
* create(Class, Callback)
* 方法的參數:
* Class:被代理對象的位元組碼
* Callback:如何代理
* @param args
*/
public static void main(String[] args) {
final Actor actor = new Actor();
Actor cglibActor = (Actor) Enhancer.create(actor.getClass(),
new MethodInterceptor() {
/**
* 執行被代理對象的任何方法,都會經過該方法。在此方法内部就可以對被代理對象的任何方法進行增強。
*
* 參數:
* 前三個和基于接口的動态代理是一樣的。
* MethodProxy:目前執行方法的代理對象。
* 傳回值:
* 目前執行方法的傳回值
*/
@Override
public Object intercept(Object proxy, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
String name = method.getName();
Float money = (Float) args[0];
Object rtValue = null;
if("basicAct".equals(name)){
//基本演出
if(money > 2000){
rtValue = method.invoke(actor, money/2);
}
}
if("dangerAct".equals(name)){
//危險演出
if(money > 5000){
rtValue = method.invoke(actor, money/2);
}
}
return rtValue;
}
});
cglibActor.basicAct(10000);
cglibActor.dangerAct(100000);
}
} 1.2.3解決案例中的問題
思路隻有一個:使用動态代理技術建立客戶業務層的代理對象,在執行CustomerServiceImpl時,對裡面的方法進行增強,加入事務的支援。
/**
* 用于建立客戶業務層對象工廠(當然也可以建立其他業務層對象,隻不過我們此處不做那麼繁瑣)
*/
public class BeanFactory {
/**
* 擷取客戶業務層對象的代理對象
* @return
*/
public static ICustomerService getCustomerService() {
//定義客戶業務層對象
final ICustomerService customerService = new CustomerServiceImpl();
//生成它的代理對象
ICustomerService proxyCustomerService = (ICustomerService)
Proxy.newProxyInstance(customerService.getClass().getClassLoader()
,customerService.getClass().getInterfaces(),
new InvocationHandler() {
//執行客戶業務層任何方法,都會在此處被攔截,我們對那些方法增強,加入事務。
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
String name = method.getName();
Object rtValue = null;
try{
//開啟事務
HibernateUtil.beginTransaction();
//執行操作
rtValue = method.invoke(customerService, args);
//送出事務
HibernateUtil.commit();
}catch(Exception e){
//復原事務
HibernateUtil.rollback();
e.printStackTrace();
}finally{
//釋放資源.hibernate在我們事務操作(送出/復原)之後,已經幫我們關了。
//如果他沒關,我們在此處關
}
return rtValue;
}
});
return proxyCustomerService;
}
}
1.3Spring中AOP
1.3.1關于代理的選擇
在spring中,架構會根據目标類是否實作了接口來決定采用哪種動态代理的方式。 1.3.2AOP相關術語
Joinpoint(連接配接點):
所謂連接配接點是指的那些被攔截到的點。在spring中,這些點指的是方法,因為spring隻支援方法類型的連接配接點。 Pointcut(切入點):
所謂切入點是指我們要對哪些Joinpoint進行攔截的定義。 Advice(通知/增強)
所謂通知是指攔截到Joinpoint之後所要做的事情就是通知。
通知的類型:前置通知,後置通知,異常通知,最終通知,環繞通知。 Introduction(引介):
引介是一種特殊的通知在不修改類代碼的前提下,Introduction可以在運作期為類動态地添加一些方法或Field。 Target(目标對象):
代理的目标對象。 Weaving(織入):
是指把增強應用到目标對象來建立新的代理對象的過程。
spring采用動态代理織入,而AspectJ采用編譯器織入和類裝載器織入。 Proxy(代理):
一個類被AOP織入增強後,就産生了一個結果代理類。 Aspect(切面):
是切入點和通知(引介)的結合。 1.3.3學習spring中的AOP要明确的事。
a、開發階段(我們做的)
編寫核心業務代碼(開發主線):大部分程式員來做,要求熟悉業務需求。
把公用代碼抽取出來,制作成通知。(開發階段最後來做):AOP程式設計人員來做。
在配置檔案中,聲明切入點與通知間的關系,即切面。:AOP程式設計人員來做。 b、運作階段(Spring架構完成的)
Spring架構監控切入點方法的執行。一旦監控到切入點方法被運作,使用代理機制,動态建立目标對象的代理對象,根據通知類别,在代理對象的對應位置,将通知對應的功能織入,完成完整的代碼邏輯運作。 二、基于XML的AOP配置
2.1環境搭建
2.1.1第一步:準備客戶的業務層和接口(需要增強的類)
/**
* 客戶的業務層接口
*/
public interface ICustomerService {
/**
* 儲存客戶
*/
void saveCustomer();
/**
* 修改客戶
* @param i
*/
void updateCustomer(int i);
} /**
* 客戶的業務層實作類
*/
public class CustomerServiceImpl implements ICustomerService { @Override
public void saveCustomer() {
System.out.println("調用持久層,執行儲存客戶");
} @Override
public void updateCustomer(int i) {
System.out.println("調用持久層,執行修改客戶");
}
} 2.1.2第二步:拷貝必備的jar包到工廠的lib目錄
2.1.3第三步:建立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"
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"> </beans>
2.1.4第四步:把客戶的業務層配置到spring容器中
<!-- 把資源交給spring來管理 -->
<bean id="customerService" class="com.itheima.service.impl.CustomerServiceImpl"/> 2.1.5第五步:制作通知(增強的類)
/**
* 一個記錄日志的工具類
*/
public class Logger {
/**
* 期望:此方法在業務核心方法執行之前,就記錄日志
*/
public void beforePrintLog(){
System.out.println("Logger類中的printLog方法開始記錄日志了。。。。");
}
} 2.2配置步驟
2.2.1第一步:把通知類用bean标簽配置起來
<!-- 把有公共代碼的類也讓spring來管理(把通知類也交給spring來管理) -->
<bean id="logger" class="com.itheima.util.Logger"></bean> 2.2.2第二步:使用aop:config聲明aop配置
<!-- aop的配置 -->
<aop:config>
<!-- 配置的代碼都寫在此處 -->
</aop:config> 2.2.3第三步:使用aop:aspect配置切面
<!-- 配置切面 :此标簽要出現在aop:config内部
id:給切面提供一個唯一辨別
ref:引用的是通知類的bean的id
-->
<aop:aspect id="logAdvice" ref="logger">
<!--配置通知的類型要寫在此處-->
</aop:aspect> 2.2.4第四步:使用aop:begore配置前置通知
<!-- 用于配置前置通知:指定增強的方法在切入點方法之前執行
method:用于指定通知類中的增強方法名稱
ponitcut-ref:用于指定切入點的表達式的引用
-->
<aop:before method="beforePrintLog" pointcut-ref="pt1"/> 2.2.5第五步:使用aop:pointcut配置切入點表達式
<aop:pointcut expression="execution(public void com.changemax.service.impl.CustomerServiceImpl.saveCustomer())"
id="pt1"/> 2.3切入點表達式說明:
execution:
比對方法的執行(常用)
execution(表達式)
表達式文法:execution([修飾符] 傳回值類型 包名.類名.方法名(參數))
寫法說明:
全比對方式:
public void com.itheima.service.impl.CustomerServiceImpl.saveCustomer() 通路修飾符可以省略 void
com.itheima.service.impl.CustomerServiceImpl.saveCustomer() 傳回值可以使用*号,表示任意傳回值
* com.itheima.service.impl.CustomerServiceImpl.saveCustomer() 包名可以使用*号,表示任意包,但是有幾級包,需要寫幾個*
* *.*.*.*.CustomerServiceImpl.saveCustomer() 使用..來表示目前包,及其子包
* com..CustomerServiceImpl.saveCustomer() 類名可以使用*号,表示任意類
* com..*.saveCustomer() 方法名可以使用*号,表示任意方法
* com..*.*() 參數清單可以使用*,表示參數可以是任意資料類型,但是必須有參數
* com..*.*(*) 參數清單可以使用..表示有無參數均可,有參數可以是任意類型
* com..*.*(..) 全通配方式:
* *..*.*(..) 2.4常用标簽
2.4.1<aop:config>
作用:
用于聲明開始aop的配置 2.4.2<aop:aspect>
作用:
用于配置切面
屬性:
id:給切面提供一個唯一辨別
ref:引用配置好的通知類bean的id。 2.4.3<aop:pointcut>
作用:
用于配置切入點表達式
屬性:
expression:用于定義切入點表達式
id:用于給切入點表達式提供一個唯一辨別。 2.4.4<aop:before>
作用:
用于配置前置通知
屬性:
method:指定通知中方法的名稱。
pointct:定義切入點表達式。
pointcut-ref:指定切入點表達式的引用 2.4.5<aop:after-returning>
作用:
用于配置後置通知
屬性:
method:指定通知中方法的名稱。
pointct:定義切入點表達式。
pointcut-ref:指定切入點表達式的引用 2.4.6<aop:after-throwing>
作用:
用于配置異常通知
屬性:
method:指定通知中方法的名稱。
pointct:定義切入點表達式。
pointcut-ref:指定切入點表達式的引用 2.4.7<aop:after>
作用:
用于配置最終通知
屬性:
method:指定通知中方法的名稱。
pointct:定義切入點表達式。
pointcut-ref:指定切入點表達式的引用 2.4.8<aop:around>
作用:
用于配置環繞通知
屬性:
method:指定通知中方法的名稱。
pointct:定義切入點表達式。
pointcut-ref:指定切入點表達式的引用 2.5通知的類型
2.5.1類型說明
<!-- 配置通知的類型
aop:before:
用于配置前置通知。前置通知的執行時間點:切入點方法執行之前執行 aop:after-returning:
用于配置後置通知。後置通知的執行時間點:切入點正常執行之後。它和異常通知隻能有一個執行。 aop:after-throwing
用于配置異常通知。異常通知的執行時間點:切入點方法執行産生異常後執行。它和後置通知隻能執行一個。 aop:after
用于配置最終配置。最終通知的執行時間點:無論切入帶你方法執行時是否異常,它都會在其後面執行。 aop:around
用于配置環繞通知。他和前面四個不一樣,他不是用于指定通知方法何時執行的。
-->
<aop:before method="beforePrintLog" pointcut-ref="pt1"/>
<aop:after-returning method="afterReturningPrintLog" pointcut-ref="pt1"/>
<aop:after-throwing method="afterThrowingPrintLog" pointcut-ref="pt1"/>
<aop:after method="afterPrintLog" pointcut-ref="pt1"/>
<aop:around method="aroundPringLog" pointcut-ref="pt1"/> 2.5.2環繞通知的特殊說明:
/**
* 環繞通知
* 它是spring架構為我們提供的一種可以在代碼中手動控制增強部分什麼時候執行的方式。
* 問題:
* 當我們配置了環繞通知之後,增強的代碼執行了,業務核心方法沒有執行。
* 分析:
* 通過動态代理我們知道在invoke方法中,有明确調用業務核心方法:method.invoke()。
* 我們配置的環繞通知中,沒有明确調用業務核心方法。
* 解決:
* spring架構為我們提供了一個接口:ProceedingJoinPoint,它可以作為環繞通知的方法參數
* 在環繞通知執行時,spring架構會為我們提供該接口的實作類對象,我們直接使用就行。
* 該接口中有一個方法proceed(),此方法就相當于method.invoke()
*/
public void aroundPringLog(ProceedingJoinPoint pjp){
try {
System.out.println("前置通知:Logger類的aroundPringLog方法記錄日志");
pjp.proceed();
System.out.println("後置通知:Logger類的aroundPringLog方法記錄日志");
} catch (Throwable e) {
System.out.println("異常通知:Logger類的aroundPringLog方法記錄日志");
e.printStackTrace();
}finally{
System.out.println("最終通知:Logger類的aroundPringLog方法記錄日志");
} }
三、基于注解的AOP配置
3.1環境搭建
3.1.1第一步:準備客戶的業務層和接口并用注解配置(需要增強的類)
/**
* 客戶的業務層接口
*/
public interface ICustomerService {
/**
* 儲存客戶
*/
void saveCustomer();
/**
* 修改客戶
* @param i
*/
void updateCustomer(int i);
} /**
* 客戶的業務層實作類
*/
public class CustomerServiceImpl implements ICustomerService { @Override
public void saveCustomer() {
System.out.println("調用持久層,執行儲存客戶");
} @Override
public void updateCustomer(int i) {
System.out.println("調用持久層,執行修改客戶");
}
} 3.1.2第二步;拷貝必備的jar包到工程的lib目錄
3.1.3第三步:建立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
http://www.springframework.org/schema/context/spring-context.xsd">
</beans> 3.1.4第四步:把資源使用注解讓spring來管理
/**
* 客戶的業務層實作類
*/
@Service("customerService")
public class CustomerServiceImpl implements ICustomerService { @Override
public void saveCustomer() {
System.out.println("調用持久層,執行儲存客戶");
}
@Override
public void updateCustomer(int i) {
System.out.println("調用持久層,執行修改客戶");
}
} 3.1.5第五步:在配置檔案中指定spring要掃描的包
<!-- 告知spring,在建立容器時要掃描的包 -->
<context:component-scan base-package="com.itheima"></context:component-scan> 3.2配置步驟
3.2.1第一步:把通知類也使用注解配置
/**
* 一個記錄日志的工具類
*/
@Component("logger")
public class Logger {
/**
* 期望:此方法在業務核心方法執行之前,就記錄日志
* 前置通知
*/
public void beforePrintLog(){
System.out.println("前置通知:Logger類中的printLog方法開始記錄日志了");
}
} 3.2.2第二步:在通知類上使用@Aspect注解聲明為切面
/**
* 一個記錄日志的工具類
*/
@Component("logger")
@Aspect//表明目前類是一個切面類
public class Logger {
/**
* 期望:此方法在業務核心方法執行之前,就記錄日志
* 前置通知
*/
public void beforePrintLog(){
System.out.println("前置通知:Logger類中的printLog方法開始記錄日志了");
}
} 3.2.3第三步:在增強的方法上使用@Before注解配置前置通知
/**
* 期望:此方法在業務核心方法執行之前,就記錄日志
* 前置通知
*/
@Before("execution(* com.itheima.service.impl.*.*(..))")//表示前置通知
public void beforePrintLog(){
System.out.println("前置通知:Logger類中的printLog方法開始記錄日志了");
} 3.2.4第四步:在spring配置檔案中開啟spring對注解aop的支援
<!-- 開啟spring對注解AOP的支援 -->
<aop:aspectj-autoproxy/> 3.3常用注解
3.3.1@Aspect:
作用:
把目前類聲明為切面類。 3.3.2@Before:
作用:
把目前方法看成是前置通知。
屬性:
value:用于指定切入點表達式,還可以指定切入點表達式的引用。 3.3.3@AfteReturning
作用:
把目前方法看成是異常通知。
屬性:
value:用于指定切入點表達式,還可以指定切入點表達式的引用。 3.3.4@AfterThrowing
作用:
把目前方法看成異常通知。
屬性:
value:用于指定切入點表達式,還可以指定切入點表達式的引用。 3.3.5@After
作用:
把目前方法看成是最終通知
屬性:
value:用于指定切入點表達式,還可以指定切入點表達式的引用。 3.3.6@Around
作用:
把目前方法看成是環繞通知
屬性:
value:用于指定切入點表達式,還可以指定切入點表達式的引用。 3.3.7@Pointcut
作用:
指定切入點表達式
屬性:
value:指定表達式的内容 3.4不适用xml的配置方式
@Configuration
@ComponentScan(basePackages="com.changemax")
@EnableAspectJAutoProxy
public class SpringConfiguration{
}