本篇将介绍如下内容:
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