Spring整合單元測試 在前面的案例中我麽需要自己創建ApplicationContext對象,然後在調用getBean來獲取需要測試的Bean Spring提供了一種更加方便的方式來創建測試所需的ApplicationContext,並且可以幫助我們把需要測試的Bean直接註入到測試類中 添加依 ...
Spring整合單元測試
在前面的案例中我麽需要自己創建ApplicationContext對象,然後在調用getBean來獲取需要測試的Bean
Spring提供了一種更加方便的方式來創建測試所需的ApplicationContext,並且可以幫助我們把需要測試的Bean直接註入到測試類中
添加依賴:
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>5.2.2.RELEASE</version>
</dependency>
測試代碼:
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import javax.annotation.Resource;
@RunWith(SpringJUnit4ClassRunner.class)//固定寫法
@ContextConfiguration("classpath:applicationContext.xml") //指定載入的配置文件
public class MyTest2 {
@Resource(name = "userService") //直接使用DI獲取Bean
private UserService userService;
@Test
public void test(){
userService.getUserDao().save();//測試
}
}
AOP概念
在軟體業,AOP為Aspect Oriented Programming的縮寫,翻譯為:面向切麵編程,通過預編譯方式和運行期動態代理實現程式功能的統一維護的一種技術。AOP是OOP的延續,是軟體開發中的一個熱點,也 是Spring框架中的一個重要內容,是函數式編程的一種衍生範型。利用AOP可以對業務邏輯的各個部分 進行隔離,從而使得業務邏輯各部分之間的耦合度降低,提高程式的可重用性,同時提高了開發的效 率。
以上內容來自百度百科,看完你會發現這玩兒說個啥呢?看完和沒看差不多,太過於抽象,咱們還是帶著問題來看AOP吧;
為什麼需要AOP
案例分析
在項目開發中我們經常遇到一系列通用需求比如:許可權控制,日誌輸出,事務管理,數據統計等,這寫看似簡單的需求,在實際開發中卻會帶來麻煩,舉個例子:
在某個的Dao層如UserDao,存在以下幾個方法
public class UserDao{
public void save(){
System.out.println("save sql");
}
public void delete(){
System.out.println("delete sql");
}
public void update(){
System.out.println("update sql");
}
}
在第一個版本中,已經實現了程式的實際功能,但是後來發現資料庫操作出現瓶頸,這時領導說要對這些方法進行執行時間統計,並輸出日誌分析問題;
解決方案
這點小需求對於你來說太easy了,於是你打開了代碼,熟練的添加代碼
public class UserDao{
public void save(){
//獲取類名和方法名
String className = Thread.currentThread().getStackTrace()[1].getClassName();
String methodName = Thread.currentThread().getStackTrace()[1].getMethodName();
//開始時間
long startTime = new Date().getTime();
//原邏輯
System.out.println("save sql");
//耗時
long runTime = new Date().getTime() - startTime;
System.out.printf("info [class:%s method:%s runtime:%s]",className,methodName,runTime);
}
public void delete(){
String className = Thread.currentThread().getStackTrace()[1].getClassName();
String methodName = Thread.currentThread().getStackTrace()[1].getMethodName();
long startTime = new Date().getTime();
System.out.println("delete sql");
long runTime = new Date().getTime() - startTime;
System.out.printf("info [class:%s method:%s runtime:%s]",className,methodName,runTime);
}
public void update(){
System.out.println("update sql");
}
public static void main(String[] args) {
new UserDao().save();
}
}
問題
需求實現了,但是作為優秀的開發工程師你當然不會就這麼完事了,因為上述代碼存在以下問題:
1.修改了源代碼(違反了OCP,違反了對修改封閉,但滿足調用方式不變)
2.大量重覆代碼
再看AOP
我們先不考慮如何解決這些問題,其實AOP之所以出現就是因為,我們需要對一些已經存在的方法進行功能擴展,但是又不能通過修改源代碼或改變調用方式的手段來解決
反過來說就是要在保證不修改源代碼以及調用方式不變的情況下為原本的方法增加功能
而由於需要擴展的方法有很多,於是把這些方法稱作一個切麵,即切麵就是一系列需要擴展功能的方法的集合
AOP的目的
將日誌記錄,性能統計,安全控制,事務處理,異常處理等重覆代碼從業務邏輯代碼中劃分 出來,通過對這些行為的分離,我們希望可以將它們獨立到非業務邏輯的方法中,進而改變這些行為的時候不會影響業務邏輯的代碼。
吐槽:
直接看名字的確是比較抽象的,沒辦法,當你創造了一個全新的東西時,你往往也會想給它取一個nb的名字,而這個解決方案是針對一些固定場景的,我們很難找到一個非常準確的名字去描述這個方案
假設你想出了一個解決方案,那麼你會給他取個什麼名字呢?
AOP相關術語
AOP這一概念是AOP聯盟aopalliance提出的,相關的概念也出自aopalliance定義
- 連接點(joinpoint)
是擴展內容與原有內容的交互的點,可以理解為可以被擴展的地方,通常是一個方法,而AspectJ中也支持屬性作為連接點
示例:案例中的三個方法
- 切點(pointcut)
切點指的是要被擴展(增加了功能)的內容,包括方法或屬性(joinpoint)
示例:案例中的兩個增加了功能的方法
- 通知(adivce)
通知指的是要在切點上增加的功能
按照執行時機不同分為:
前置,後置,異常,最終,環繞,引介
引介通知指的是在不修改類代碼的前提下,為類增加方法或屬性(瞭解即可非重點)
示例:上述案例中的輸出執行時間功能
- 目標(target)
目標就是要應用通知的對象,即要被增強的對象
示例:上述案例中的userDao
- 織入(weaving)
織入是一個動詞,描述的是將擴展功能應用到target的這個過程
示例:案例中修改源代碼的過程
- 代理(proxy)
Spring是使用代理來完成AOP,對某個對象增強後就得到一個代理對象;
Spring AOP的整個過程就是對target應用advice最後產生proxy,我們最後使用的都是proxy對象; 狸貓換太子,偷梁換柱;
- 切麵(aspect)
是切入點和通知的結合切麵,是一個抽象概念; 一個切麵指的是所有應用了同一個通知的切入點的集合
示例:案例中的save 和 delete方法共同組成一個切麵
AOP的傳統實現
就在官方努力退出動態代理時,民間開發者也安耐不住自己躁動的新,開發了自己的一套實現AOP的方案,兩者都是利用代理對象,都屬於代理模式,但是實現原理略有不同;
動態代理(官方)
JDK1.4出現
介面:
public interface UserDao {
public void save();
public void delete();
}
實現類:
package com.yh.demo4;
public class UserDaoImpl implements UserDao {
public void save(){
System.out.println("save run");
}
public void delete(){
System.out.println("delete run");
}
}
代理類及測試代碼:
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.Date;
public class MyProxy implements InvocationHandler {
private Object target;
public MyProxy(Object target) {
this.target = target;
}
//創建代理對象 本質是動態的產生一個target對象的介面實現類
public Object createProxy(){
Object o = Proxy.newProxyInstance(target.getClass().getClassLoader(), target.getClass().getInterfaces(), this);
return o;
}
//方法處理 動態代理核心方法,在調用代理對象方法時都會自動調用該方法
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
//獲取類信息和方法名
String className = target.getClass().getName();
String methodName = method.getName();
//記錄開始時間
long startTime = new Date().getTime();
//調用原始方法
Object result = method.invoke(target,args);
//計算耗時
long runtime = new Date().getTime() - startTime;
System.out.printf("info [class:%s method:%s runtime:%s]\n",className,methodName,runtime);
//返回原始方法執行結果
return result;
}
public static void main(String[] args) {
//目標對象
UserDao userDao = new UserDaoImpl();
//代理對象
UserDao proxyDao = (UserDao) new MyProxy(userDao).createProxy();
proxyDao.save();
proxyDao.delete();
}
}
當我們要對某些方法進行許可權控制時也非常簡單,只需要判斷方法名稱,然後增加許可權控制邏輯即可;
註意:
1.動態代理,要求被代理的target對象必須實現了某個介面,且僅能代理介面中聲明的方法,這給開髮帶來了一些限制,當target不是某介面實現類時,則無法使用動態代理,CGLib則可以解決這個問題
2.被攔截的方法包括介面中聲明的方法以及代理對象和目標對象都有的方法如:toString
3.對代理對象執行這些方法將造成死迴圈
CGLib(民間)
CGLib是第三方庫,需要添加依賴:
<dependency>
<groupId>cglib</groupId>
<artifactId>cglib</artifactId>
<version>3.2.5</version>
</dependency>
被代理類:
public class UserDaoImpl {
public void save(){
System.out.println("save run");
}
public void delete(){
System.out.println("delete run");
}
}
代理類及測試代碼:
public class MyProxy implements MethodInterceptor {
private Object target;
public MyProxy(Object target) {
this.target = target;
}
//創建代理對象 本質是動態的產生一個target對象的介面實現類
public Object createProxy(){
//CGLib核心類
Enhancer enhancer = new Enhancer();
//指定要代理的對象類型
enhancer.setSuperclass(target.getClass());
//設置方法回調 即代理調用代理對象的方法時會執行的方法
enhancer.setCallback(this);
//創建代理對象
Object o = enhancer.create();
return o;
}
/***
* @param o 代理對象
* @param method 客戶要執行的方法
* @param objects 方法參數
* @param methodProxy 方法代理對象 用於執行父類(目標)方法
* @return 原始方法的返回值
* @throws Throwable*/
public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
//註意不要對第一個參數(代理對象) 執行任何方法,會造成死迴圈
//獲取類信息和方法名
String className = target.getClass().getName();
String methodName = method.getName();
//記錄開始時間
long startTime = new Date().getTime();
//調用原始方法1 傳入的是目標對象
Object result = method.invoke(target,objects);
//調用原始方法2 傳入的是代理對象
Object result = methodProxy.invokeSuper(o,objects);
//計算耗時
long runtime = new Date().getTime() - startTime;
System.out.printf("info [class:%s method:%s runtime:%s]\n",className,methodName,runtime);
//返回原始方法執行結果
return result;
}
public static void main(String[] args) {
//目標對象
UserDaoImpl userDao = new UserDaoImpl();
//代理對象
UserDaoImpl proxyDao = (UserDaoImpl) new MyProxy(userDao).createProxy();
proxyDao.save();
proxyDao.delete();
}
}
註意:
1.CGLib可以攔截代理目標對象的所有方法
2.CGLib採用的是產生一個繼承目標類的代理類方式產生代理對象,所以如果類被final修飾將無法使用CGLib
利用上述兩種方法我們就可以實現OAP了
Spring中的AOP
Spring在運行期,可以自動生成動態代理對象,不需要特殊的編譯器,Spring AOP的底層就是通過JDK動態代理和CGLib動態代理技術 為目標Bean執行橫向織入。並且Spring會自動選擇代理方式
1.若目標對象實現了若幹介面,spring使用JDK的java.lang.reflect.Proxy類代理。
2.若目標對象沒有實現任何介面,spring使用CGLIB庫生成目標對象的子類。
Spring通知類型
前置
org.springframework.aop.MethodBeforeAdvice
用於在原始方法執行前的預處理後置
org.springframework.aop.AfterReturningAdvice
用於在原始方法執行後的後處理環繞
org.aopalliance.intercept.MethodInterceptor
這個名字不知道誰給起的,其實不算是通知,而是叫攔截器,在這裡我們可以阻止原始方法的執行,而其他通知做不到異常
org.springframework.aop.ThrowsAdvice
用於在原始方法拋出異常時處理引介
org.springframework.aop.IntroductionInterceptor
在目標類中添加一些新的方法和屬性(非重點)
Spring切麵類型
普通的切麵(Advisor)
普通切麵指的是未指定具體切入點的切麵,那麼將把目標對象中所有方法作為切入點(全部增強)
介面:
public interface StudentDao {
public void save();
public void update();
public void delete();
public void select();
}
實現類:
public class StudentDaoImpl implements StudentDao {
public void save() { System.out.println("save run"); }
public void update() { System.out.println("update run"); }
public void delete() { System.out.println("delete run"); }
public void select() { System.out.println("select run"); }
}
配置文件:
<!--目標Bean-->
<bean id="studentDao" class="com.yh.demo7.StudentDaoImpl"/>
<!--通知-->
<bean id="before" class="com.yh.demo7.MyAdvice"/>
<!---代理Bean-->
<bean id="studentDaoProxy" class="org.springframework.aop.framework.ProxyFactoryBean">
<!--指定目標-->
<property name="target" ref="studentDao"/>
<!--指定目標實現的介面-->
<property name="proxyInterfaces" value="com.yh.demo7.StudentDao"/>
<!--通知(攔截)Bean的名稱 多個用逗號隔開-->
<property name="interceptorNames" value="before"/>
<!--其他設置:-->
<!--告知spring目標對象是否是一個普通類 true時使用CGlib-->
<property name="proxyTargetClass" value="true"/>
<!--代理類是否採用單例,預設true-->
<property name="singleton" value="false"/>
<!--是否強制使用CGlib-->
<property name="optimize" value="true"/>
</bean>
通知類:
public class MyAdvice implements MethodBeforeAdvice,AfterReturningAdvice, MethodInterceptor {
public void before(Method method, Object[] objects, Object o) throws Throwable {
System.out.println("前置通知....");
}
public void afterReturning(Object o, Method method, Object[] objects, Object o1) throws Throwable {
System.out.println("後置通知....");
}
public Object invoke(MethodInvocation methodInvocation) throws Throwable {
System.out.println("環繞前....");
Object result = methodInvocation.proceed(); //執行原始方法
System.out.println("環繞後....");
return result;
}
}
測試代碼:
@RunWith(SpringJUnit4ClassRunner.class)//固定寫法
@ContextConfiguration("classpath:applicationContext4.xml") //指定載入的配置文件
public class MyTest4 {
@Resource(name = "studentDaoProxy")
private StudentDao studentDao;
@Test
public void test(){
studentDao.delete();
studentDao.save();
studentDao.update();
studentDao.select();
}
}
切入點切麵使用(PointcutAdvisor)
顧名思義,也就是指定為目標對象中僅某進行增強
使用正則匹配方法的切麵:
<?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
https://www.springframework.org/schema/beans/spring-beans.xsd">
<!--目標Bean-->
<bean id="studentDao" class="com.yh.demo7.StudentDaoImpl"/>
<!--通知-->
<bean id="advice" class="com.yh.demo7.MyAdvice"/>
<!--組織切麵信息-->
<bean id="myAdvisor" class="org.springframework.aop.support.RegexpMethodPointcutAdvisor">
<!--指定一個正則表達式 方法名稱匹配則將方法作為切入點-->
<property name="pattern" value=".*save"/>
<!--指定多個正則表達式 多個表達式用逗號隔開即可-->
<property name="patterns" value=".*save,.*update"/>
<!--指定要應用的通知-->
<property name="advice" ref="advice"/>
</bean>
<!---代理Bean-->
<bean id="studentDaoProxy" class="org.springframework.aop.framework.ProxyFactoryBean">
<!--指定目標-->
<property name="target" ref="studentDao"/>
<!--指定目標實現的介面-->
<property name="proxyInterfaces" value="com.yh.demo7.StudentDao"/>
<!--指向Pointcut切入點-->
<property name="interceptorNames" value="myAdvisor"/>
</bean>
</beans>
使用預設的切點切麵:
<bean id="pointcutBean" class="org.springframework.aop.support.NameMatchMethodPointcut">
<property name="mappedNames">
<list>
<value>*save</value>
</list>
</property>
</bean>
<bean id="myAdvisor2" class="org.springframework.aop.support.DefaultPointcutAdvisor">
<property name="advice" ref="advice"/>
<property name="pointcut" ref="pointcutBean"/>
</bean>
自動生成代理 待更新....................
基於BeanName生成代理
基於切點信息生成代理