Spring實踐(二)AOP的底層實現機制
上一篇通過模擬spring的IOC 機制來理解控制反轉、依賴注入,本篇同樣模擬一下spring的第二大特性AOP(Aspect Oriented Programming)。
本篇將介紹如下內容:
1、AOP的應用場景
2、生成一個簡單的工程案例
3、 AOP 需求分析
4、用JDK的動態反射來描述實現原理
5、用spring的aop 配置來簡化AOP 實現
一、AOP 的場景
我喜歡學習一個技術點的時候,考慮一下這個技術點應用場景,這樣對加深學習記憶以及學習效果比較好。一般來說,我們都習慣於垂直的進行程式設計和應用開發,但是有時候,我們用到一些比如公共的日誌、安全的時候,需要在業務物件裡面,插入這些公共的呼叫。比如,在一個物件的方法,呼叫前後,我們希望列印一下日誌,或者計算一下這個呼叫的開始和結束時間。
二、簡單舉例
先看一下下面這個簡單例子,基本上,呈現的是普通的一個基於介面開發的特徵:
注意:這裡面StudentSimulationDB 是模擬資料持久化,用來對本例做資料支撐的。
具體這些類的原始碼如下:
1、首先是實體類Student.java 描述的是實體物件Student的屬性和基本操作
package com.study.entity;
/*
* this is a simple entity class, descripe Student;
*/
public class Student {
String Name="";
String Sex="";
String Birth="";
public String getName() {
return Name;
}
public void setName(String name) {
Name = name;
}
public String getSex() {
return Sex;
}
public void setSex(String sex) {
Sex = sex;
}
public String getBirth() {
return Birth;
}
public void setBirth(String birth) {
Birth = birth;
}
public String toString(){
return "Name="+this.Name+";Sex="+this.Sex+";Birthday="+this.Birth;
}
}
2、介面StudentDAO.java 是對實體物件DAO操作的介面,為了便於擴展采用面向介面程式設計的方式,設計為介面
package com.study.dao;
import com.study.entity.Student;
/*
* this interface define entity class student's dao(data access operation) interface
*/
public interface StudentDAO {
//學生操作,新增學生
boolean addStudent(Student student);
//學生操作,刪除學生
boolean delStudent(Student student);
//學生操作,修改學生資訊
boolean modifyStudent(Student student);
//學生操作,查詢學生資訊,查詢到返回學生物件,否則返回null
Student queryStudent( String StudentName);
}
3、介面StudentDAOImpl.java 是介面實現類,介面實現類可以有多個,分別實現不同的業務邏輯,這樣才能體現靈活性
package com.study.dao.impl;
import com.study.dao.StudentDAO;
import com.study.entity.Student;
/*
* this is implement of StudentDAO;
*/
public class StudentDAOImpl implements StudentDAO {
@Override
public boolean addStudent(Student student) {
// TODO Auto-generated method stub
//add 操作,加入student物件到list中
return StudentSimulationDB.getInstance().add(student);
}
@Override
public boolean delStudent(Student student) {
// TODO Auto-generated method stub
return false;
}
@Override
public boolean modifyStudent(Student student) {
// TODO Auto-generated method stub
return false;
}
@Override
public Student queryStudent(String StudentName) {
// TODO Auto-generated method stub
//模擬從資料庫中查詢學生名, 資料庫中只有一名 name 為 Tom的學生
return StudentSimulationDB.getInstance().querry(StudentName);
}
}
4、StudentSimulationDB 是模擬資料庫的類,模擬持久化功能
package com.study.dao.impl;
import java.util.ArrayList;
import java.util.List;
import com.study.entity.Student;
//模擬資料庫,目的是對student操作的時候,可以記錄操作的內容
public class StudentSimulationDB {
public List<Student > listStudent=new ArrayList<Student>();
private static StudentSimulationDB instance = null;
private StudentSimulationDB(){}
public static StudentSimulationDB getInstance() {// 例項化引用
if (instance == null) {
instance = new StudentSimulationDB();
}
return instance;
}
//模擬資料庫中增加一條學生記錄
public boolean add(Student student){
//檢查是否存在相同的學生,學生姓名最為唯一關鍵字
for(Student inStudent:listStudent){
if(student.getName().equals(inStudent.getName())){
System.out.println("DB had existed this Student!!!");
return false;
}
}
listStudent.add(student);
return true;
}
//如果在模擬資料庫中存在,那麼範圍student的資訊,否則返回null
public Student querry(String studentName){
for(Student inStudent:listStudent){
if(studentName.equals(inStudent.getName())){
return inStudent;
}
}
return null;
}
}
5、 Student 應用類StudentService,在這個類裡面,用的是StudentDAO, 而在具體執行時,可以指定實現類,這樣就能體現出面向介面程式設計的靈活性、擴充套件性
package com.study.student.service;
import com.study.dao.StudentDAO;
import com.study.entity.Student;
/*
* this class descripe StudentApi
*/
public class StudentService {
//private StudentDAO studentDAO = new StudentDAOImpl();
private StudentDAO studentDAO ;
public StudentDAO getStudentDAO() {
return studentDAO;
}
public void setStudentDAO(StudentDAO studentDAO) {
this.studentDAO = studentDAO;
}
public boolean addStudent(Student student) {
return this.studentDAO.addStudent(student);
}
public String queryStudent(String studentName) {
Student retStudent = this.studentDAO.queryStudent(studentName);
if (null == retStudent)
return "null";
else
return retStudent.toString();
}
}
6、程式碼結構如下圖所示
三、AOP需求分析
上面例子中,如果對於複雜呼叫,經常會要求在呼叫add和querry的開始和結束,需要列印日誌和開始時間、結束時間。 我們往往有如下做法:
1、 修改StudentDAOImpl,在函式開始和結束前新增需求。
這樣操作的問題在於: 如果這樣的類和方法很多,每個都要這麼新增,是非常煩的一件事,而且沒有多大意思。而且如果再變化一下需求,比如時間格式要求變化,還需要一個個修改過去。
2、不允許修改StudentDAOImpl的情況下,我們通過繼承StudentDAOImpl類,重寫相應的方法:
@Override
XXXXXX 方法(){
XXXXXX ; // 新增邏輯
super( xxxx);
XXXXXX ; // 新增邏輯
}
同樣這也無法解決1中,關於需求變化的需求。
3、組合的方式,我們將StudentDAO 與 StudentDAOImpl 組合到我們的類裡面:
package com.study.dao.proxy;
import org.apache.log4j.Logger;
import com.study.dao.StudentDAO;
import com.study.dao.impl.StudentDAOImpl;
import com.study.entity.Student;
public class StudentProxy implements StudentDAO {
private StudentDAO studentdao= new StudentDAOImpl();
static Logger logger = Logger.getLogger(StudentProxy.class);
@Override
public boolean addStudent(Student student) {
// TODO Auto-generated method stub
// add 操作,加入student物件到list中
boolean ret = false;
logger.info("addStudent method start, student info=>"+ student.toString());
ret = studentdao.addStudent(student);
logger.info("addStudent method end.");
return ret;
}
@Override
public boolean delStudent(Student student) {
// TODO Auto-generated method stub
return false;
}
@Override
public boolean modifyStudent(Student student) {
// TODO Auto-generated method stub
return false;
}
@Override
public Student queryStudent(String StudentName) {
// TODO Auto-generated method stub
// 模擬從資料庫中查詢學生名, 資料庫中只有一名 name 為 Tom的學生
logger.info("queryStudent method start, querry prarm=>"+StudentName);
Student student;
student = studentdao.queryStudent(StudentName);
logger.info("queryStudent method end.");
return student;
}
}
關於log4j包的引入和log4j.properties 檔案的配置,這裡就略去了。 我們通過編寫junit 測試類,來測試這個proxy,看看junit 裡面test 結果StudentServiceTest 程式碼如下: (用的是junit4)
package com.study.student.service;
import static org.junit.Assert.*;
import static org.hamcrest.Matchers.*;
import org.junit.After;
import org.junit.BeforeClass;
import org.junit.Test;
import com.study.dao.StudentDAO;
import com.study.dao.proxy.StudentProxy;
import com.study.entity.Student;
public class StudentServiceTest {
public static StudentService studentService;
public static StudentDAO studentDAO;
//注意這裡用beforeClass而不是before,表示全部測試函式呼叫前,呼叫一次
@BeforeClass
public static void init() throws Exception{
studentService= new StudentService();
studentService.setStudentDAO( new StudentProxy());
}
@Test
public void addStudentTest() throws Exception{
Student studentObj = new Student();
studentObj.setName("Tom");
studentObj.setSex("Male");
studentObj.setBirth("19740508");
assertTrue(studentService.addStudent(studentObj));
Student studentObj2 = new Student();
studentObj2.setName("Jerry");
studentObj2.setSex("Female");
studentObj2.setBirth("19780615");
assertTrue(studentService.addStudent(studentObj2));
}
@After
public void queryStudentTest( ) throws Exception {
assertThat(studentService.queryStudent("Tom"), is("Name=Tom;Sex=Male;Birthday=19740508"));
assertThat(studentService.queryStudent("Jack"), is("null"));
}
}
測試結果如下:
日誌列印情況:
[2017-02-13 11:20:00] INFO [main] (StudentProxy.java::19) - addStudent method start, student info=>Name=Tom;Sex=Male;Birthday=19740508
[2017-02-13 11:20:00] INFO [main] (StudentProxy.java::21) - addStudent method end.
[2017-02-13 11:20:00] INFO [main] (StudentProxy.java::19) - addStudent method start, student info=>Name=Jerry;Sex=Female;Birthday=19780615
[2017-02-13 11:20:00] INFO [main] (StudentProxy.java::21) - addStudent method end.
[2017-02-13 11:20:00] INFO [main] (StudentProxy.java::42) - queryStudent method start, querry prarm=>Tom
[2017-02-13 11:20:00] INFO [main] (StudentProxy.java::45) - queryStudent method end.
[2017-02-13 11:20:00] INFO [main] (StudentProxy.java::42) - queryStudent method start, querry prarm=>Jack
[2017-02-13 11:20:00] INFO [main] (StudentProxy.java::45) - queryStudent method end.
同樣這種實現,也無法解決有很多類時,需要進行組裝和修改的問題。
四、AOP需求的jdk reflect 動態實現
在JDK 1.4版本後,提供了呼叫一個方法的時候,動態新增處理邏輯的功能,下面看一下實現:
DynaStudentProxy.java 是動態反射類,裡面的invoke 方法,實現了需要新增在 StudentDAOImpl 物件中方法呼叫時的處理邏輯,本例是列印日誌
package com.study.dao.proxy;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import org.apache.log4j.Logger;
public class DynaStudentProxy implements InvocationHandler {
static Logger logger = Logger.getLogger(DynaStudentProxy.class);
/**
* 目標物件,如StudentDAOImpl物件
*/
private Object target;
/**
* 動態生成方法被處理過後的物件 (寫法固定)
*
*/
public Object bind(Object inTarget) {
this.target = inTarget;
return Proxy.newProxyInstance(
this.target.getClass().getClassLoader(), this.target
.getClass().getInterfaces(), this);
}
/**
* 目標物件中的每個方法會被此方法送去JVM呼叫,也就是說,要目標物件的方法只能通過此方法呼叫,
* 此方法是動態的,不是手動呼叫的
*/
public Object invoke(Object proxy, Method method, Object[] args)
throws Throwable {
Object result = null;
try {
// 執行原來的方法之前記錄日誌
logger.info( method.getName() + " Method Start, args=>"+args[0]);
// JVM通過這條語句執行原來的方法(反射機制)
result = method.invoke(this.target, args);
// 執行原來的方法之後記錄日誌
logger.info(method.getName() + " Method end");
} catch (Exception e) {
e.printStackTrace();
}
// 返回方法返回值給呼叫者
return result;
}
}
然後在應用StudentServiceTest.java 中,動態的繫結StudentDAOImpl,而不是set ,這樣的邏輯就變成,先呼叫proxy中的業務邏輯在執行具體impl方法中的內容。
package com.study.student.service;
import static org.junit.Assert.*;
import static org.hamcrest.Matchers.*;
import org.junit.After;
import org.junit.BeforeClass;
import org.junit.Test;
import com.study.dao.StudentDAO;
import com.study.dao.impl.StudentDAOImpl;
import com.study.dao.proxy.DynaStudentProxy;
import com.study.entity.Student;
public class StudentServiceTest {
public static StudentService studentService;
public static StudentDAO studentDAO;
//注意這裡用beforeClass而不是before,表示全部測試函式呼叫前,呼叫一次
@BeforeClass
public static void init() throws Exception{
studentService= new StudentService();
StudentDAOImpl aImpl=new StudentDAOImpl();
DynaStudentProxy mProxy = new DynaStudentProxy();
//用動態繫結的方法替換set方法,使得呼叫物件的業務邏輯發生變化
//這裡指在呼叫impl的add 和querry 之前,列印日誌
//studentService.setStudentDAO(aImpl);
studentService.studentDAO = (StudentDAO) mProxy.bind(aImpl);
}
@Test
public void addStudentTest() throws Exception{
Student studentObj = new Student();
studentObj.setName("Tom");
studentObj.setSex("Male");
studentObj.setBirth("19740508");
assertTrue(studentService.addStudent(studentObj));
Student studentObj2 = new Student();
studentObj2.setName("Jerry");
studentObj2.setSex("Female");
studentObj2.setBirth("19780615");
assertTrue(studentService.addStudent(studentObj2));
}
@After
public void queryStudentTest( ) throws Exception {
assertThat(studentService.queryStudent("Tom"), is("Name=Tom;Sex=Male;Birthday=19740508"));
assertThat(studentService.queryStudent("Jack"), is("null"));
}
}
五、spring aop 實現
回想一下上一篇的IOC,spring用配置的方法就解決了IOC的功能,AOP同樣,spring也是通過配置檔案就可以解決,上面第四點可以說是spring aop的底層原理。
1、首先引入spring,並且新增配置檔案applicationContext_aop.xml,同時,為了體現spring aop 可以進行配置多個切面邏輯,編寫了2個Handler,分別是 LogHandler和TimeRecHandler即日誌和時間記錄處理。
2、下面先看一下這兩個切面處理邏輯類:
日誌記錄類:
package com.study.dao.handler;
import org.apache.log4j.Logger;
public class LogHandler {
static Logger logger = Logger.getLogger(LogHandler.class);
public void LogBefore() {
logger.info("Method invoke start...");
}
public void LogAfter() {
logger.info("Method invoke end!");
logger.info("");
}
}
時間記錄類:
package com.study.dao.handler;
import org.apache.log4j.Logger;
public class TimeRecHandler {
static Logger logger = Logger.getLogger(LogHandler.class);
public void printTime() {
logger.info("CurrentTime = " + System.currentTimeMillis());
}
}
3、配置aop的xml
在applicationContext_aop.xml裡面,我們進行如下配置
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:context="http://www.springframework.org/schema/context"
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-3.0.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop-3.0.xsd">
<bean id="studentDAOImpl" class="com.study.dao.impl.StudentDAOImpl" />
<bean id="timeRecHandler" class="com.study.dao.handler.TimeRecHandler" />
<bean id="logHandler" class="com.study.dao.handler.LogHandler" />
<bean id="StudentService" class ="com.study.student.service.StudentService" >
<property name="StudentDAO" ref="studentDAOImpl"/>
</bean>
<aop:config>
<aop:aspect id="log" ref="logHandler" order="1">
<aop:pointcut id="printLog" expression="execution(* com.study.dao.impl.StudentDAOImpl.*(..))" />
<aop:before method="LogBefore" pointcut-ref="printLog" />
<aop:after method="LogAfter" pointcut-ref="printLog" />
</aop:aspect>
<aop:aspect id="time" ref="timeRecHandler" order="2">
<aop:pointcut id="addTime" expression="execution(* com.study.dao.impl.StudentDAOImpl.*(..))" />
<aop:before method="printTime" pointcut-ref="addTime" />
<aop:after method="printTime" pointcut-ref="addTime" />
</aop:aspect>
</aop:config>
</beans>
4、拷貝StudentServiceTest =》 StudentServiceTest2
只修改裡面的init 方法
//注意這裡用beforeClass而不是before,表示全部測試函式呼叫前,呼叫一次
@BeforeClass
public static void init() throws Exception{
BeanFactory factory = new ClassPathXmlApplicationContext("applicationContext_aop.xml");
studentService= (StudentService) factory.getBean("StudentService");
}
執行StudentServiceTest2,執行結果如下:
最後附上整個工程的結構以及依賴lib