AOP (面向切面编程)Aspect Oriented Programming的缩写
优点:解耦
利用AOP可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率。
主要功能
日志记录,性能统计,安全控制,事务处理,异常处理等等。
主要意图
将日志记录,性能统计,安全控制,事务处理,异常处理等代码从业务逻辑代码中划分出来,通过对这些行为的分离,我们希望可以将它们独立到非业务逻辑的方法中,进而改变这些行为的时候不影响业务逻辑的代码。
AOP相关概念
1.方面(Aspect)
2.连接点(Joinpoint)
3.通知(Advice): 各种类型的通知包括“around”、“before”和“throws”通知。
许多AOP框架包括Spring都是以拦截器做通知模型,维护一个“围绕”连接点的拦截器链。
Spring中定义了四个advice:
BeforeAdvice, AfterAdvice, ThrowAdvice和DynamicIntroductionAdvice
4.切入点(Pointcut)
5.引入(Introduction): 添加方法或字段到被通知的类。
6.目标对象(Target Object): 被通知或被代理对象。
7.AOP代理(AOP Proxy): AOP框架创建的对象,包含通知。
8.织入(Weaving): 组装方面来创建一个被通知对象。这可以在编译时完成(例如使用AspectJ编译器),也可以在运行时完成。Spring和其他纯Java AOP框架一样,在运行时完成织入。
实现AOP的技术,主要分为两大类:一是采用动态代理技术,利用截取消息的方式,对该消息进行装饰,以取代原有对象行为的执行;二是采用静态织入的方式,引入特定的语法创建“方面”,从而使得编译器可以在编译期间织入有关“方面”的代码。
在Spring中,AOP代理可以是JDK动态代理或者CGLIB代理。
Spring AOP代理对象的生成
Spring提供了两种方式来生成代理对象: JDKProxy和Cglib,具体使用哪种方式生成由AopProxyFactory根据AdvisedSupport对象的配置来决定。默认的策略是如果目标类是接口,则使用JDK动态代理技术,否则使用Cglib来生成代理。
- 静态代理
- JDK动态代理
- CGlib动态代理
静态代理
静态代理很简单,咱们自己在写代码的时候都会写到这种类似静态代理的代码。简单来说,就是把被代理类作为参数传给代理类的构造方法,让代理类替被代理类实现更强大的功能。
public class StaticProxyTest {
public static void main(String[] args) {
UserService userService = new UserService();
LogProxy logProxy = new LogProxy(userService);
logProxy.addUser();
logProxy.deleteUser();
}
}
interface IUserService{
void addUser();
void deleteUser();
}
class UserService implements IUserService{
@Override
public void addUser() {
System.out.println("添加用户");
}
@Override
public void deleteUser() {
System.out.println("删除用户");
}
}
//日志代理
class LogProxy implements IUserService{
//目标类
private UserService target;
public LogProxy(UserService target){
this.target = target;
}
@Override
public void addUser() {
System.out.println("记录日志开始");
target.addUser();
System.out.println("记录日志结束");
}
@Override
public void deleteUser() {
System.out.println("记录日志开始");
target.deleteUser();
System.out.println("记录日志结束");
}
}
动态代理在Spring源码中,用到的动态代理主要有两种,JDK动态代理以及CGLib动态代理。
两者主要区别是:
JDK动态代理一般针对实现了接口的类生成代理。
目标对象没有实现接口,则默认会采用CGLIB代理。如果目标对象实现了接口,可以强制使用CGLIB实现代理。
相同点:两种动态代理本质上都是字节码组装
如果被代理的目标对象实现了至少一个接口,则会使用JDK动态代理。所有该目标对象实现的接口都将被代理。 若该目标对象没有实现任何接口,则创建一个CGLIB代理。
CGLIB代理模式下每一个目标对象创建一个子类。每一个代理实例会生成两个对象:实际代理对象和它的一个实现了通知的子类实例。
且CGLib的效率没有使用JDK代理机制高,速度平均要慢8倍左右。
JDK动态代理的代理类一般需要实现接口
public class JdkProxyTest {
public static void main(String[] args) {
IPersonService personService = JdkDynamicProxy.getProxy();
personService.addPerson();
personService.deletePerson();
}
}
interface IPersonService{
void addPerson();
void deletePerson();
}
class PersonService implements IPersonService{
@Override
public void addPerson() {
System.out.println("添加人物");
}
@Override
public void deletePerson() {
System.out.println("删除人物");
}
}
/**
* newProxyInstance方法参数说明:
* ClassLoader loader:指定当前目标对象使用的类加载器,获取加载器的方法是固定的
* Class<?>[] interfaces:指定目标对象实现的接口的类型,使用泛型方式确认类型
* InvocationHandler:指定动态处理器,执行目标对象的方法时,会触发事件处理器的方法
*/
class JdkDynamicProxy{
public static IPersonService getProxy(){
IPersonService personService = new PersonService();
IPersonService proxy = (IPersonService) Proxy.newProxyInstance(IPersonService.class.getClassLoader(), new Class<?>[]{IPersonService.class}, new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("记录日志开始");
Object obj = method.invoke(personService, args);
System.out.println("记录日志结束");
return obj;
}
});
return proxy;
}
}
CGLib动态代理
public class CglibProxyTest {
public static void main(String[] args) {
CglibProxy proxy = new CglibProxy();
Train t = (Train)proxy.getProxy(Train.class);
t.move();
}
}
class Train {
public void move(){
System.out.println("火车行驶中...");
}
}
class CglibProxy implements MethodInterceptor {
private Enhancer enhancer = new Enhancer();
public Object getProxy(Class clazz){
enhancer.setSuperclass(clazz);
enhancer.setCallback(this);
return enhancer.create();
}
/**
* 拦截所有目标类方法的调用
* obj 目标类的实例
* m 目标方法的反射对象
* args 方法的参数
* proxy代理类的实例
*/
@Override
public Object intercept(Object obj, Method m, Object[] args, MethodProxy proxy) throws Throwable {
System.out.println("日志开始...");
//代理类调用父类的方法
proxy.invokeSuper(obj, args);
System.out.println("日志结束...");
return null;
}
}
原理分析
下面我们来研究一下Spring如何使用JDK来生成代理对象,具体的生成代码放在JdkDynamicAopProxy这个类中,假设我们要对下面这个用户管理进行代理:
public interface UserMgr {
void addUser();
void delUser();
}
public class UserMgrImpl implements UserMgr {
@Override
public void addUser() {
System.out.println("添加用户.....");
}
@Override
public void delUser() {
System.out.println("删除用户.....");
}
}
按照代理模式的实现方式,肯定是用一个代理类,让它也实现UserMgr接口,然后在其内部声明一个UserMgrImpl,然后分别调用addUser和delUser方法,并在调用前后加上我们需要的其他操作。但是这样很显然都是写死的,我们怎么做到动态呢?别急,接着看。 我们知道,要实现代理,那么我们的代理类跟被代理类都要实现同一接口,但是动态代理的话我们根本不知道我们将要代理谁,也就不知道我们要实现哪个接口,那么要怎么办呢?我们只有知道要代理谁以后,才能给出相应的代理类,那么我们何不等知道要代理谁以后再去生成一个代理类呢?想到这里,我们好像找到了解决的办法,就是动态生成代理类!
这时候我们亲爱的反射又有了用武之地,我们可以写一个方法来接收被代理类,这样我们就可以通过反射知道它的一切信息——包括它的类型、它的方法等等。
JDK动态代理的两个核心分别是InvocationHandler和Proxy,下面我们就用简单的代码来模拟一下它们是怎么实现的:
InvocationHandler接口:
package com.tgb.proxy;
import java.lang.reflect.Method;
public interface InvocationHandler {
public void invoke(Object o, Method m);
}
实现动态代理的关键部分,通过Proxy动态生成我们具体的代理类:
动态生成Java源文件,写入本地,然后将Java文件编译成class文件,加载到内存,并实例化,删除本地源文件。
package com.tgb.proxy;
public class Proxy {
/**
*
* @param infce 被代理类的接口
* @param h 代理类
* @return
* @throws Exception
*/
public static Object newProxyInstance(Class infce, InvocationHandler h) throws Exception {
String methodStr = "";
String rt = "\r\n";
//利用反射得到infce的所有方法,并重新组装
Method[] methods = infce.getMethods();
for(Method m : methods) {
methodStr += " @Override" + rt +
" public "+m.getReturnType()+" " + m.getName() + "() {" + rt +
" try {" + rt +
" Method md = " + infce.getName() + ".class.getMethod(\"" + m.getName() + "\");" + rt +
" h.invoke(this, md);" + rt +
" }catch(Exception e) {e.printStackTrace();}" + rt +
" }" + rt ;
}
//生成Java源文件
String srcCode =
"package com.tgb.proxy;" + rt +
"import java.lang.reflect.Method;" + rt +
"public class $Proxy1 implements " + infce.getName() + "{" + rt +
" public $Proxy1(InvocationHandler h) {" + rt +
" this.h = h;" + rt +
" }" + rt +
" com.tgb.proxy.InvocationHandler h;" + rt +
methodStr + rt +
"}";
String fileName = "d:/src/com/tgb/proxy/$Proxy1.java";
File f = new File(fileName);
FileWriter fw = new FileWriter(f);
fw.write(srcCode);
fw.flush();
fw.close();
//将Java文件编译成class文件
JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
StandardJavaFileManager fileMgr = compiler.getStandardFileManager(null, null, null);
Iterable units = fileMgr.getJavaFileObjects(fileName);
CompilationTask t = compiler.getTask(null, fileMgr, null, null, null, units);
t.call();
fileMgr.close();
//加载到内存,并实例化
URL[] urls = new URL[] {new URL("file:/" + "d:/src/")};
URLClassLoader ul = new URLClassLoader(urls);
Class c = ul.loadClass("com.tgb.proxy.$Proxy1");
Constructor ctr = c.getConstructor(InvocationHandler.class);
Object m = ctr.newInstance(h);
return m;
}
}
这个类的主要功能就是,根据被代理对象的信息,动态组装一个代理类,生成Proxy1.java文件,然后将其编译成Proxy1.class。这样我们就可以在运行的时候,根据我们具体的被代理对象生成我们想要的代理类了。这样一来,我们就不需要提前知道我们要代理谁。也就是说,你想代理谁,想要什么样的代理,我们就给你生成一个什么样的代理类。
然后,在客户端我们就可以随意的进行代理了。
package com.tgb.proxy;
public class Client {
public static void main(String[] args) throws Exception {
UserMgr mgr = new UserMgrImpl();
//为用户管理添加事务处理
InvocationHandler h = new TransactionHandler(mgr);
UserMgr u = (UserMgr)Proxy.newProxyInstance(UserMgr.class,h);
//为用户管理添加显示方法执行时间的功能
TimeHandler h2 = new TimeHandler(u);
u = (UserMgr)Proxy.newProxyInstance(UserMgr.class,h2);
u.addUser();
System.out.println("\r\n==========华丽的分割线==========\r\n");
u.delUser();
}
}
事务处理:
package com.tgb.proxy;
import java.lang.reflect.Method;
public class TransactionHandler implements InvocationHandler {
private Object target;
public TransactionHandler(Object target) {
super();
this.target = target;
}
@Override
public void invoke(Object o, Method m) {
System.out.println("开启事务.....");
try {
m.invoke(target);
} catch (Exception e) {
e.printStackTrace();
}
System.out.println("提交事务.....");
}
}
运行结果:
开始时间:2014年-07月-15日 15时:48分:54秒
开启事务.....
添加用户.....
提交事务.....
结束时间:2014年-07月-15日 15时:48分:57秒
耗时:3秒
==========华丽的分割线==========
开始时间:2014年-07月-15日 15时:48分:57秒
开启事务.....
删除用户.....
提交事务.....
结束时间:2014年-07月-15日 15时:49分:00秒
耗时:3秒
项目中使用AOP遇到的坑
1.在Spring配置文件中配置了事务管理器,如下:
<!--事务管理器配置,单数据源事务-->
<tx:annotation-driven transaction-manager="transactionManager" />
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource" />
</bean>
2.配置了事务管理器后,加入了@Transactional注解
@Service
@Transactional
public class AccountService{
//to do something
}
上面配置默认启用JDK动态代理,JDK只能代理接口不能代理类。而我的项目中用的是这个配置,却因为没有定义Service接口导致项目启动报错。
如果需要使用CGLIB动态代理:配置如下:
<tx:annotation-driven transaction-manager="txManager" proxy-target-class="true"/>
关于spring的aop拦截的问题 protected方法代理问题
jdk是代理接口,私有方法必然不会存在在接口里,所以就不会被拦截到;
cglib是子类,private的方法照样不会出现在子类里,也不能被拦截。
Example
public class ServiceA {
private ServiceA self;
public void setSelf(ServiceA self) {
this.self = self;
}
public String methodA(String str) {
System.out.println("methodA: args=" + str);
self.methodB("b");
return "12345" + str;
}
private String methodB(String str) {
System.out.println("methodB: args=" + str);
self.methodC("c");
return "12345" + str;
}
public String methodC(String str) {
System.out.println("methodC: args=" + str);
return "12345" + str;
}
}
execution(* test.aop.ServiceA.*(..))
如果外部调用methodA,那么methodA和methodC会被拦截到,methodB不行,但是protected方法能被拦截到
execution(public * test.aop.ServiceA.*(..))
还有个奇怪的现象,execution里如果不写权限,那么public protected package的方法都能被拦截到
如果写了public,那就只拦截public方法这个没问题,
如果写了protected,他就什么事情都不做,连protected的方法也不拦截。
分析
在Spring使用纯Spring AOP(只能拦截public/protected/包),private方法都是无法被拦截的,因为子类无法覆盖;包级别能被拦截的原因是,如果子类和父类在同一个包中是能覆盖的。
在cglib代理情况下, execution(* *(..)) 可以拦截 public/protected/包级别方法(即这些方法都是能代理的)。
private static boolean isOverridable(Method method, Class targetClass) {
if (Modifier.isPrivate(method.getModifiers())) {
return false;
}
if (Modifier.isPublic(method.getModifiers()) || Modifier.isProtected(method.getModifiers())) {
return true;
}
return getPackageName(method.getDeclaringClass()).equals(getPackageName(targetClass));
}
如果想要实现拦截private方法的 可以使用 原生 AspectJ 编译期/运行期织入。
场景1:execution(* *(..))
public class Impl2 {
protected/public String testAop2() {
System.out.println("234");
return "1233";
}
}
因为切入点没有访问修饰符,即可以是任意,因此canApply方法能拿到这种protected/public方法,即可以实施代理。
场景2:execution(public * *(..))
public class Impl2 {
public String testAop2() {
System.out.println("234");
return "1233";
}
}
因为拦截public的,因此canApply方法能拿到public方法(拿不到protected),即可以实施代理。
场景3:execution(protected * *(..))
public class Impl2 {
protected String testAop2() {
System.out.println("234");
return "1233";
}
}
还记得之前说过,在canApply方法中 的 Method[] methods = clazz.getMethods();只能拿到public方法的,因此跟protected访问修饰符是无法匹配的,所以如果“execution(protected * *(..))” 是 无法代理的。
在使用代理的时候,@Transactional 注解应该只被应用到 public 可见度的方法上。 如果你在 protected、private 或者 package-visible 的方法上使用 @Transactional 注解,系统也不会报错, 但是这个被注解的方法将不会执行已配置的事务设置。如果你非要注解非公共方法的话,请参考使用AspectJ