面向方面编程的思想

I. 面向方面编程

1. 什么是方面 Aspect

大家可能知道,很多出名的框架比如 Spring, 里面用到了面向方面编程的思想,那么什么是面向方面编程呢?和学习面向对象编程 OOP一样,要先学习 对象 Object 是什么,那么在面向方面编程 AOP 中,我们要先知道什么是方面 Aspect

  • 可以这样理解,如果把对象看作一个圆柱体,那么其中的一个切面,就是一个方面
  • 如果系统中存在交叉业务,一个交叉业务,就是要切入到系统的一个方面

我们用个例子来说明,有这么三个类:

  • StudentService 类用来管理学生的增删改查(CRUD)
  • CourseService 类用来管理课程的增删改查
  • MiscService 类用于管理其他东西的功能

我们如果想要在每一个方法调用的时候,加入安全,事务,和日志功能,或者说业务。那么我们可以在每一个方法的开头或者结尾,添加相应的功能。那么就是说,安全,事务,和日志功能贯穿到了好多个模块之中(StudentService, CourseService …), 所以,安全, 事务,和日志就是交叉业务。如下所示

					安全           事务          日志
StudentService ------|--------------|------------|-----------
CourseService  ------|--------------|------------|-----------
MiscService    ------|--------------|------------|-----------

如果用具体的程序代码来描述交叉业务,就如下所示

method1{           method2{             method3{

---------------------------------------------------------- 切面
...                  ...                   ...

---------------------------------------------------------- 切面
}                  }                     }

这些交叉业务,就是要加入到这些切面的位置,比如日志功能。其实这个切面就是 AOP 中所说的方面。

那么问题来了:

  • 如果可以更改源代码,那么我们就要对每一个方法进行修改,加入相同的功能
    • 太过麻烦,而且代码复杂,不易维护
  • 如果不能对源代码进行修改,又想添加这些功能,怎么办?

由此引出了交叉业务的编程问题,即面向方面的编程 AOP

2. 面向方面编程的思想

由上面的分析,我们知道,交叉业务,其实都是相同的,比如日志功能。

所以 AOP 的思想

  • 就是把这些交叉业务模块化,形成功能模块,也叫做切面代码
  • 将这些切面代码移动到原始方法的周围,这与直接在方法内前后编写切面代码运行效果是一样的

也就是说, 这种交叉业务的模块功能,我只写一份,而不是在每个方法内都要写。


---------------------------------------------------------- 切面
method1{           method2{             method3{

...                  ...                   ...

}                  }                    }
---------------------------------------------------------- 切面

现在,我们知道 AOP, 其实就是将交叉业务代码,由原来方法内的切面,移动到方法外的切面,而且,只需要写一次业务代码(切面代码)就行了,那么怎么实现呢? 用代理技术

II. 代理技术

在学习反射的时候,我们接触过动态代理,这里就详细的说一下代理技术(Proxy)

1. 静态代理

静态代理其实就是编写一个与目标类具有相同接口的代理类,代理类的方法调用目标类的相同方法,并添加功能。

可以看下列例子: 定义一个 StudentService 接口,并定义 CRUD 方法:

package org.lovian.aop.proxy;

public interface StudentService {
	void add();
	void read();
	void delete();
	void update();
}

然后增加一个实现类 StudentServiceImpl

package org.lovian.aop.proxy;

public class StudentSerivceImpl implements StudentService{

	@Override
	public void add() {
		System.out.println("add a student");
	}

	@Override
	public void read() {
		System.out.println("read info of a student");
	}

	@Override
	public void delete() {
		System.out.println("remove a student");
	}

	@Override
	public void update() {
		System.out.println("update a student");
	}
}

如果我们想要添加日志功能,那么就写一个代理类,这个代理类中有一个交叉业务模块 log(),由 AOP 的思想,我们这样实现这个代理类:

package org.lovian.aop.proxy;

public class StudentServiceProxy implements StudentService {

	private StudentSerivceImpl ss;

	public StudentServiceProxy() {
		this.ss = new StudentSerivceImpl();
	}

	@Override
	public void add() {
		log();
		ss.add();
	}

	@Override
	public void read() {
		log();
		ss.read();
	}

	@Override
	public void delete() {
		log();
		ss.delete();
	}

	@Override
	public void update() {
		log();
		ss.update();
	}

	private void log() {
		System.out.println("This is log info");
	}
}

测试类中,将要使用反射和配置文件来决定是调用原有类还是代理类

package org.lovian.aop.proxy;

import java.io.FileInputStream;
import java.io.InputStream;
import java.util.Properties;

public class StudentServiceTest {
	public static void main(String[] args) throws Exception{

		InputStream is = new FileInputStream("resource/config.properties");
		Properties props = new Properties();
		props.load(is);
		is.close();

		String className = props.getProperty("className");
		Class<?> c = Class.forName(className);
		StudentService ss = (StudentService) c.newInstance();

		ss.add();
		ss.read();
		ss.update();
		ss.delete();
	}
}

配置文件, 位于项目目录下的 resource/config.properties,我们先定义 classNameStudentSerivceImpl

className = org.lovian.aop.proxy.StudentSerivceImpl

测试结果:

add a student
read info of a student
update a student
remove a student

但如果我们在配置文件中改用代理类

className = org.lovian.aop.proxy.StudentServiceProxy

测试结果:

This is log info
add a student
This is log info
read info of a student
This is log info
update a student
This is log info
remove a student

由这个代理的例子我们可以发现,通过配置文件和反射,我们可以非常容易的通过修改配置文件去调用不同的类,而不用修改原有代码。但是这样做有一个问题,就是如果我有很多个实现类,那么我需要为这很多个实现类添加一个代理类,这样做是非常麻烦的,所以就有了动态代理技术

2. 动态代理

JVM 可以在运行期动态生成出类的字节码,这种动态生成的类往往被用作代理类,即动态代理类。

  • JVM 生成的动态类必须实现一个或者多个接口
    • JVM 生成的动态类只能用具有相同接口的目标类的代理
  • CGLIB 可以动态生成一个类的子类,一个类的子类也可以用作该类的代理类
    • 如果要为一个没有实现接口的类生成动态代理类,可以可以使用CGLIB

代理类的每个方法通常除了要调用目标的相应方法和对外返回目标返回的结果外,还可以在下面四个位置加上系统功能代码

  • 在调用目标方法之前
  • 在调用目标方法之后
  • 在调用目标方法前后
  • 在处理目标方法异常的 catch 块中
void method(){
	.......... // 调用目标方法前
	try{
		target.method();
	}catch{
		........... // 处理目标方法的异常
	}
	......... // 调用目标方法后
}

怎么实现得到动态代理类并调用,查看[JAVA] Java 类加载器和反射

下面是一个封装了生成代理的通用方法:

public static Object getProxy(final Object target, final Advice advice) {
	// 获取 proxy 对象的实例
	Object proxy = Proxy.newProxyInstance(target.getClass().getClassLoader(), target.getClass().getInterfaces(),
			new InvocationHandler() {

				// 使用匿名内部类来实现 Invocation的接口
				@Override
				public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
					// 方法前的切面代码
					advice.beforeMethod(target, method, args);

					// 调用目标方法的返回值
					Object result = null;
					try {
						// 代理对象调用目标方法
						result = method.invoke(target, args);
					} catch (Exception e) {
						// catch 语句块处理异常捕获的切面代码
						advice.catchMethodException(target, method, args);
					}
					// 方法后的切面代码
					advice.afterMethod(target, method, args);
					return result;
				}
			});

	return proxy;
}

getProxy 方法返回一个代理对象,接收两个参数:

  • final Object target : 需要加代理的目标对象
  • final Advice advice :
    • 插入通告接口,定义了交叉业务的接口
    • 定义了三个方法,用于在 invoke() 方法中进行调用
    • 所以只需要实现了这个接口,就可以把它传入到生成代理的参数
package org.lovian.aop.proxy;

import java.lang.reflect.Method;

// 插入通告接口
public interface Advice {
	// 添加到目标方法调用之前的切面代码
	void beforeMethod(Object target, Method method, Object[] args);

	// 添加到目标方法调用之后的切面代码
	void afterMethod(Object target, Method method, Object[] args);

	// 添加到目标方法catch语句块的切面代码
	void catchMethodException(Object target, Method method, Object[] args);
}

III. 实现 AOP 框架

这一节会将如何实现一个 AOP 框架的思路,这个思路也就是 Spring Framework的核心技术

1. 实现 AOP 功能思路

要实现一个 AOP 框架,那么要能够实现 AOP 功能的封装与配置,思路如下:

  • 有一个工厂类 BeanFactory
    • 负责创建目标类或者代理类的实例对象,并通过配置文件进行切换
    • 有一个getBean() 方法,可以根据参数字符串返回一个相应的实例对象
      • Object obj = BeanFactory.getBean(xxx);
      • 如果参数字符串在配置文件中对应的类名不是 ProxyFactoryBean,则直接返回该类的实例对象
      • 否则返回该类实例对象的 getProxy 方法返回的对象
  • ProxyFactoryBean 充当封装生成动态代理的工厂,需要为工厂类提供 targetadvice
  • BeanFactory 的构造方法接收代表配置文件的输入流对象,配置文件格式如下
#xxx = java.util.ArrayList
xxx = org.lovian.aop.framework.ProxyFactoryBean
xxx.target = java.util.ArrayList
xxx.advice = org.lovian.aop.framework.MyAdvice

2. 代码实现

首先我们需要创建一个通告类接口 Advice,用于封装交叉业务方法

package org.lovian.aop.framework;

import java.lang.reflect.Method;

public interface Advice {
	void beforeMethod(Object target, Method method, Object[] args);

	void afterMethod(Object target, Method method, Object[] args);

	void catchMethodException(Object target, Method method, Object[] args);
}

然后我们需要实现这个通告类接口 MyAdive, 模拟交叉业务

package org.lovian.aop.framework;

import java.lang.reflect.Method;

public class MyAdvice implements Advice {

	@Override
	public void beforeMethod(Object target, Method method, Object[] args) {
		System.out.println("Check function");
	}

	@Override
	public void afterMethod(Object target, Method method, Object[] args) {
		System.out.println("Log function");
	}

	@Override
	public void catchMethodException(Object target, Method method, Object[] args) {
		System.out.println("Process exception");
	}

}

由 AOP 框架思路,我们需要一个动态代理类的工厂 ProxyFactoryBean

package org.lovian.aop.framework;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;

import org.lovian.aop.framework.Advice;

public class ProxyFactoryBean {

	private Advice advice;
	private Object target;

	public Advice getAdvice() {
		return advice;
	}

	public void setAdvice(Advice advice) {
		this.advice = advice;
	}

	public Object getTarget() {
		return target;
	}

	public void setTarget(Object target) {
		this.target = target;
	}

	public Object getProxy() {
		// 获取 proxy 对象的实例
		Object proxy = Proxy.newProxyInstance(target.getClass().getClassLoader(), target.getClass().getInterfaces(),
				new InvocationHandler() {

					// 使用匿名内部类来实现 Invocation的接口
					@Override
					public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
						// 方法前的切面代码
						advice.beforeMethod(target, method, args);

						// 调用目标方法的返回值
						Object result = null;
						try {
							// 代理对象调用目标方法
							result = method.invoke(target, args);
						} catch (Exception e) {
							// catch 语句块处理异常捕获的切面代码
							advice.catchMethodException(target, method, args);
						}
						// 方法后的切面代码
						advice.afterMethod(target, method, args);
						return result;
					}
				});

		return proxy;
	}
}

ProxyFactoryBean 工厂类,通过 setter/getter 来传递一个需要被代理的对象和封装了交叉业务的通告类,并提供了一个 getProxy() 方法能够返回动态代理对象。在getProxy() 方法里,调用通告类的切面方法,这个就是 AOP 面向方面编程的核心思想

然后我们需要一个 BeanFactory 工厂类,通过配置文件,来返回 bean 对象,或者是 bean 对象的动态代理对象

package org.lovian.aop.framework;

import java.io.IOException;
import java.io.InputStream;
import java.util.Properties;

public class BeanFactory {

	Properties props = new Properties();

	public BeanFactory(InputStream inputStream) {
		// 通过Properties 来读取输入流,获取配置文件的内容
		try {
			props.load(inputStream);
		} catch (IOException e) {
			e.printStackTrace();
		}
	}


	// getBean 方法,参数为配置文件中定义的 key
	public Object getBean(String name){
		String className = props.getProperty(name);
		Object bean = null;

		try {
			// 获取目标类的 class 文件对象
			Class clazz = Class.forName(className);
			// 调用无参构造创建 Bean 实例
			bean = clazz.newInstance();
		} catch (Exception e) {
			e.printStackTrace();
		}
		// 如果,目标类是动态代理类,则返回代理对象
		if(bean instanceof ProxyFactoryBean){
			Object proxy = null;
			try {
				ProxyFactoryBean proxyFactoryBean = (ProxyFactoryBean) bean;
				// 通过反射生成通告类对象
				Advice advice = (Advice)Class.forName(props.getProperty(name + ".advice")).newInstance();
				// 通过反射生成目标类对象
				Object target = Class.forName(props.getProperty(name + ".target")).newInstance();
				// 为动态代理工厂类设置通告对象和目标对象
				proxyFactoryBean.setAdvice(advice);
				proxyFactoryBean.setTarget(target);
				// 得到动态代理对象
				proxy = proxyFactoryBean.getProxy();
			} catch (Exception e) {
				e.printStackTrace();
			}
			return proxy;
		}

		// 如果不是动态代理类,则返回正常的 bean 对象
		return bean;
	}
}

那么我们创建一个测试类

package org.lovian.aop.framework;

import java.io.IOException;
import java.io.InputStream;
import java.util.Collection;

public class AopFrameWorkTest {
	public static void main(String[] args) throws IOException {
		// 配置文件的路径(相对路径,与测试类位于同一个包下
		String configFilePath = "config.properties";
		InputStream ips = AopFrameWorkTest.class.getResourceAsStream(configFilePath);

		// 通过工厂类来获得 bean 对象
		String className = "xxx";
		Object bean = new BeanFactory(ips).getBean(className);
		String beanName = bean.getClass().getName();
		System.out.println(beanName);

		// 测试 bean 对象
		Collection<String> coll = (Collection<String>) bean;
		coll.add("hello");
		coll.add("world");
		System.out.println("collection: " + coll);

		ips.close();
	}
}

如果我们的配置文件如下:

xxx = java.util.ArrayList
#xxx = org.lovian.aop.framework.ProxyFactoryBean
xxx.target = java.util.ArrayList
xxx.advice = org.lovian.aop.framework.MyAdvice

说明我们此时要生成的是正常的 ArrayList bean 对象,来看运行结果

java.util.ArrayList
collection: [hello, world]

如果我们把配置文件修改如下:

#xxx = java.util.ArrayList
xxx = org.lovian.aop.framework.ProxyFactoryBean
xxx.target = java.util.ArrayList
xxx.advice = org.lovian.aop.framework.MyAdvice

说明我们要生成动态代理的 bean 对象,加入了交叉业务模块,运行结果如下:

com.sun.proxy.$Proxy0
Check function
Log function
Check function
Log function
Check function
Log function
collection: [hello, world]

Share this on