1. 程式人生 > >Spring實踐(二)AOP的底層實現機制

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