什么是AOP?
Aspect Oriented Programming(AOP),面向切面编程,是一个比较热门的话题。AOP主要实现的目的是针对业务处理过程中的切面进行提取,它所面对的是处理过程中的某个步骤或阶段,以获得逻辑过程中各部分之间低耦合性的隔离效果。简单来说,就是抽取业务中相同的操作,让业务处理过程值完成自己核心的功能,例如日志和hibernate中的事务。下面将以hibernate中的事务为例子具体阐释。
AOP基本概念
1)aspect(切面):实现了cross-cutting功能,是针对切面的模块。最常见的是logging模块,这样,程序按功能被分为好几层,如果按传统的继承的话,商业模型继承日志模块的话根本没有什么意义,而通过创建一个logging切面就可以使用AOP来实现相同的功能了。
2)jointpoint(连接点):连接点是切面插入应用程序的地方,该点能被方法调用,而且也会被抛出意外。连接点是应用程序提供给切面插入的地方,可以添加新的方法。比如以上我们的切点可以认为是findInfo(String)方法。
3)advice(处理逻辑):advice是我们切面功能的实现,它通知程序新的行为。如在logging里,logging advice包括logging的实现代码,比如像写日志到一个文件中。advice在jointpoint处插入到应用程序中。以上我们在MyHandler.java中实现了advice的功能
4)pointcut(切点):pointcut可以控制你把哪些advice应用于jointpoint上去,通常你使用pointcuts通过正则表达式来把明显的名字和模式进行匹配应用。决定了那个jointpoint会获得通知。
5)introduction:允许添加新的方法和属性到类中。
6)target(目标类):是指那些将使用advice的类,需要进行业务增强操作的类。
7) proxy(代理类):使用了proxy的模式。是指应用了advice的对象。是对目标对象进行业务增强的类。
8)weaving(插入):是指应用aspects到一个target对象创建proxy对象的过程:complie time,classload time,runtime
AOP的应用背景:设计中存在的问题
•代码混乱:越来越多的非业务需求(日志和验证等)加入后, 原有的业务方法急剧膨胀. 每个方法在处理核心逻辑的同时还必须兼顾其他多个关注点.
•代码分散: 以日志需求为例, 只是为了满足这个单一需求, 就不得不在多个模块(方法)里多次重复相同的日志代码. 如果日志需求发生变化, 必须修改所有模块.
使用动态代理解决上述问题
•代理设计模式的原理: 使用一个代理将对象包装起来, 然后用该代理对象取代原始对象. 任何对原始对象的调用都要通过代理. 代理对象决定是否以及何时将方法调用转到原始对象上.
从代码上来理解动态代理 代理对象和目标对象
首先要声明一个代理类,要实现InvocationHandler接口,并且实现接口的invoke()方法。
package com.bj169.dao.impl;
import com.bj169.factory.SessionFactoryUtils;
import org.apache.commons.logging.LogFactory;
import org.hibernate.Session;
import org.hibernate.Transaction;
import org.springframework.stereotype.Repository;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
/**
* @ClassName UserDaoHandler
* @Description TODO
* @Author Administrator
* @Date 2018/12/13 0013 14:37
* @Version 1.0
**/
@Repository
public class UserDaoHandler implements InvocationHandler {
//获取session
Session session = SessionFactoryUtils.getSession();
//目标对象
private Object target;
public UserDaoHandler() {
}
//目标对象初始化
public UserDaoHandler(Object target) {
this.target = target;
}
/**
* 在目标对象(target)上进行增强操作
* 在执行过程中利用异常处理对业务再次进行增强
*
* @param proxy 代理对象
* @param method 目标对象的业务操作
* @param args 目标对象的业务操作的核心参数
* @return 返回增强后的结果
*/
@Override
public Object invoke(Object proxy, Method method, Object[] args) {
//输出日志
LogFactory.getLog(this.getClass()).info("开启事务");
Object invoke = null;
//开启事务
Transaction transaction = session.beginTransaction();
try {
//调用目标对象的方法,args代表参数
invoke = method.invoke(target, args);
LogFactory.getLog(this.getClass()).info("事务提交");
transaction.commit();
} catch (IllegalAccessException e) {
LogFactory.getLog(this.getClass()).info("事务回滚");
transaction.rollback();
e.printStackTrace();
} catch (InvocationTargetException e) {
LogFactory.getLog(this.getClass()).info("事务回滚");
e.printStackTrace();
} finally {
LogFactory.getLog(this.getClass()).info("session关闭");
session.close();
}
return invoke;
}
/**
* 代理对象与目标对象建立联系--当生成代理对象时,就可以调用上面的invoke()方法
* 而invoke()方法里面又可以调用目标对象的方法,进而两者建立联系
* 通过Proxy实现代理 返回代理对象(已经被增强的对象)
* target.getClass().getClassLoader(): 获取目标对象的类加载器
* target.getClass().getInterfaces(): 获取目标对象的所有接口--来获得目标对象的所有方法
* new MathCalcHandler(target) : 对目标对象进行增强操作
*
* @param target 目标对象
* @return 代理对象
*/
public static Object createHandler(Object target) {
return Proxy.newProxyInstance(target.getClass().getClassLoader(), target.getClass().getInterfaces(),
new UserDaoHandler(target));
}
}
被代理的目标类,要求这个被代理的目标类要实现一个接口,即存在一个接口
package com.bj169.dao.impl;
import com.bj169.dao.UserDao;
import com.bj169.entity.myUser;
import com.bj169.factory.SessionFactoryUtils;
import org.hibernate.Session;
import org.springframework.stereotype.Repository;
/**
* @ClassName UserDaoImpl
* @Description TODO
* @Author Administrator
* @Date 2018/12/11 0011 9:52
* @Version 1.0
**/
@Repository
public class UserDaoImpl implements UserDao {
private Session session = SessionFactoryUtils.getSession();
@Override
public void add(myUser myUser) {
session.save(myUser);
}
@Override
public void update(myUser myUser) {
session.update(myUser);
}
@Override
public void delete(myUser myUser) {
session.delete(myUser);
}
@Override
public myUser search(myUser myUser) {
myUser myUser1 = session.get(myUser.getClass(), myUser.getUid());
System.out.println(myUser1);
return myUser1;
}
}
测试类
@Test
public void testUpdate() {
myUser myUser = new myUser();
myUser.setUsername("李小龙");
myUser.setUid(5);
myUser.setPwd("123");
myUser.setAddress("中国");
UserDao userDao = new UserDaoImpl();
//通过UserDaoHandler调用createHandler()方法生成代理对象
//在通过代理对象调用已经被代理对象增强的方法
UserDao handler = (UserDao) UserDaoHandler.createHandler(userDao);
handler.update(myUser);
}
通过上述的操作,我们就对UserDaoImpl这个目标类声明了一个代理对象Proxy,此时调用目标类的所有方法都会调用代理对象中相同的方法,但这些方法已经被业务增强。
从原理上理解动态代理中代理对象与目标对象如何建立联系
首先我们先得到上面单元测试得到的代理对象Proxy0的类的结构,从中可以看出两者的关系。
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//
import com.bj169.dao.UserDao;
import com.bj169.entity.myUser;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.lang.reflect.UndeclaredThrowableException;
public final class Proxy0 extends Proxy implements UserDao {
private static Method m1;
private static Method m4;
private static Method m3;
private static Method m5;
private static Method m6;
private static Method m2;
private static Method m0;
public Proxy0(InvocationHandler var1) throws {
super(var1);
}
public final boolean equals(Object var1) throws {
try {
return (Boolean)super.h.invoke(this, m1, new Object[]{var1});
} catch (RuntimeException | Error var3) {
throw var3;
} catch (Throwable var4) {
throw new UndeclaredThrowableException(var4);
}
}
public final void update(myUser var1) throws {
try {
super.h.invoke(this, m4, new Object[]{var1});
} catch (RuntimeException | Error var3) {
throw var3;
} catch (Throwable var4) {
throw new UndeclaredThrowableException(var4);
}
}
public final void add(myUser var1) throws {
try {
super.h.invoke(this, m3, new Object[]{var1});
} catch (RuntimeException | Error var3) {
throw var3;
} catch (Throwable var4) {
throw new UndeclaredThrowableException(var4);
}
}
public final void delete(myUser var1) throws {
try {
super.h.invoke(this, m5, new Object[]{var1});
} catch (RuntimeException | Error var3) {
throw var3;
} catch (Throwable var4) {
throw new UndeclaredThrowableException(var4);
}
}
public final void search(myUser var1) throws {
try {
super.h.invoke(this, m6, new Object[]{var1});
} catch (RuntimeException | Error var3) {
throw var3;
} catch (Throwable var4) {
throw new UndeclaredThrowableException(var4);
}
}
public final String toString() throws {
try {
return (String)super.h.invoke(this, m2, (Object[])null);
} catch (RuntimeException | Error var2) {
throw var2;
} catch (Throwable var3) {
throw new UndeclaredThrowableException(var3);
}
}
public final int hashCode() throws {
try {
return (Integer)super.h.invoke(this, m0, (Object[])null);
} catch (RuntimeException | Error var2) {
throw var2;
} catch (Throwable var3) {
throw new UndeclaredThrowableException(var3);
}
}
static {
try {
m1 = Class.forName("java.lang.Object").getMethod("equals", Class.forName("java.lang.Object"));
m4 = Class.forName("com.bj169.dao.UserDao").getMethod("update", Class.forName("com.bj169.entity.myUser"));
m3 = Class.forName("com.bj169.dao.UserDao").getMethod("add", Class.forName("com.bj169.entity.myUser"));
m5 = Class.forName("com.bj169.dao.UserDao").getMethod("delete", Class.forName("com.bj169.entity.myUser"));
m6 = Class.forName("com.bj169.dao.UserDao").getMethod("search", Class.forName("com.bj169.entity.myUser"));
m2 = Class.forName("java.lang.Object").getMethod("toString");
m0 = Class.forName("java.lang.Object").getMethod("hashCode");
} catch (NoSuchMethodException var2) {
throw new NoSuchMethodError(var2.getMessage());
} catch (ClassNotFoundException var3) {
throw new NoClassDefFoundError(var3.getMessage());
}
}
}
上面的源代码类Proxy0 中,有三个地方已经用UserDaoHandler类对象来调用invoke方法,他们分别在这些方法的代码中:public final boolean equals(Object obj) ;public final int hashCode() ;public final String toString()。也许你会有疑问,这些方法也没有看到在哪里被Proxy0 对象调用过,怎么能执行invoke方法呢?但是请看Proxy0中的static代码块,这个模块是特殊的,因为当newProxyInstance创建$Proxy0 时,它就被初始化。而这个static模块中的getMethod方法加载了这个三个方法,因而它们里面的代码(h.invoke())被执行。
小结:invoke方法的调用过程,就是先新建目标类对象(实例),然后把它传入newProxyInstance方法中,在里面解析并用它来调用invoke方法。
通过注解的方式实现AOP编程
1.需要在项目配置Aspect的jar包
2.在配置文件里面启动aspect注解,或者通过注解编写配置类替换xml文件
第一种 配置文件里
<!--在配置文件中启动aspect注解-->
<!--<aop:aspectj-autoproxy></aop:aspectj-autoproxy>-->
第二种 注解编写配置类
package com.bj169.aspect;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
/**
* @ClassName Config
* @Description TODO
* @Author Administrator
* @Date 2018/12/14 0014 11:49
* @Version 1.0
**/
@ComponentScan(basePackages = "com.bj169")//扫描com.bj169包下的组件(注解),
// 相当于配置文件里的<context:component-scan base-package="com.bj169"></context:component-scan>
@EnableAspectJAutoProxy//声明Aspect依赖,
// 相当于配置文件里的<aop:aspectj-autoproxy></aop:aspectj-autoproxy>
@Configuration//@Configuration用于定义配置类,可替换xml配置文件,
// 被注解的类内部包含有一个或多个被@Bean注解的方法,
// 这些方法将会被AnnotationConfigApplicationContext
// 或AnnotationConfigWebApplicationContext类进行扫描,并用于构建bean定义,
// 初始化Spring容器
public class Config {
}
当使用第二种注解的方式替换xml时,就需要AnnotationConfigApplicationContext来读取配置文件,进而得到里面的bean
ApplicationContext context = null;
@Before
public void before() {
//读取配置文件类
context = new AnnotationConfigApplicationContext(Config.class);
}
@Test
public void testDelete1() {
myUser myUser = new myUser();
myUser.setUid(18);
UserService userService = (UserService) context.getBean("userServiceImpl");
userService.deleteUser(myUser);
}
3.通过注解的方式编写aspect切面类
•要在 Spring 中声明 AspectJ 切面, 只需要在 IOC 容器中将切面声明为 Bean 实例. 当在 Spring IOC 容器中初始化 AspectJ 切面之后, Spring IOC 容器就会为那些与 AspectJ 切面相匹配的 Bean 创建代理.
•在 AspectJ 注解中, 切面只是一个带有 @Aspect 注解的 Java 类.
•通知是标注有某种注解的简单的 Java 方法.
•AspectJ 支持 5 种类型的通知注解:
–@Before: 前置通知, 在方法执行之前执行
–@After: 后置通知, 在方法执行之后执行
–@AfterRunning: 返回通知, 在方法返回结果之后执行
–@AfterThrowing: 异常通知, 在方法抛出异常之后
–@Around: 环绕通知, 围绕着方法执行
切面类代码如下:
package com.bj169.aspect;
import org.apache.commons.logging.LogFactory;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.List;
/**
* @ClassName MathAspect
* @Description TODO
* @Author Administrator
* @Date 2018/12/14 0014 11:11
* @Version 1.0
**/
//声明时一个Aop切面,同时声明组件类
@Aspect
@Component
public class MathAspect {
//重用切入点
@Pointcut("execution(* com.bj169.invoke.impl.MathCalcImpl.*(..))")
//@Pointcut重用切面表达式,当几种切面方法用的切面表达式都是相同的情况下可以使用这种方法
//后面括号里是切入点表达式,代表对那个包里面那个类的那个方法创建代理
public void myPointCut() {
//这里不需要做任何操作,只为了重用切面。
}
//在方法运行前执行的代码,参数为切入点表达式
@Before("myPointCut()")
public void before(JoinPoint joinPoint) {
//定义一个连接点参数,通过连接点参数获取方法名称
String name = joinPoint.getSignature().getName();
//通过连接点参数来获取方法的参数
//此处通过Array类访问数组的通用方法,asList转换为集合方便观察结果
List<Object> objects = Arrays.asList(joinPoint.getArgs());
//日志输出
LogFactory.getLog(this.getClass()).info(name + "方法执行前" + "begin with" + objects);
}
//在方法执行后执行的代码,注意输出是在注解@After输出后面
//返回通知可以在声明中添加一个returning,用于接受返回值的声明
@AfterReturning(value = "myPointCut()", returning = "result")
public void afterReturning(Object result) {
//此处参数result即为注解中的result,需要一一对应。
LogFactory.getLog(this.getClass()).info("方法执行后" + result);
}
//方法执行遇到异常执行的代码
@AfterThrowing(value = "myPointCut()", throwing = "e")
//将 throwing 属性添加到 @AfterThrowing 注解中, 也可以访问连接点抛出的异常.
// Throwable 是所有错误和异常类的超类. 所以在异常通知方法可以捕获到任何错误和异常.
public void afterThrowing(Exception e) {
LogFactory.getLog(this.getClass()).info("方法出现异常");
e.printStackTrace();
}
//类似于异常里finally中执行的代码,但是会输出在注解@AfterReturning之前
@After("myPointCut()")
public void After() {
LogFactory.getLog(this.getClass()).info("最后必须执行的方法");
}
@Around("myPointCut()")
public Object around(ProceedingJoinPoint p) {
//连接点的参数类型必须是 ProceedingJoinPoint .
// 它是 JoinPoint 的子接口, 允许控制何时执行, 是否执行连接点
Object o = null;
try {
LogFactory.getLog(this.getClass()).info("方法执行前");
//上面对应@Before注解
o = p.proceed();
//在环绕通知中需要明确调用 ProceedingJoinPoint 的 proceed() 方法来执行被代理的方法.
// 如果忘记这样做就会导致通知被执行了, 但目标方法没有被执行.
System.out.println(o);
LogFactory.getLog(this.getClass()).info("方法执行后");
//上面对应@AfterReturning注解
} catch (Throwable throwable) {
LogFactory.getLog(this.getClass()).info("方法出现异常");
//对应@AfterThrowing注解
throwable.printStackTrace();
} finally {
//对应After注解
LogFactory.getLog(this.getClass()).info("最后必须执行的方法");
}
return o;
//注意: 环绕通知的方法需要返回目标方法执行之后的结果,
// 即调用 joinPoint.proceed(); 的返回值, 否则会出现空指针异常
}
}
利用方法签名编写 AspectJ 切入点表达式
–execution(* com.ehome.aop.Calculator.*(..))
匹配 Calculator中声明的所有方法,第一个 * 代表任意修饰符及任意返回值;第二个 * 代表任意方法; .. 匹配任意数量任意类型的参数. 若目标类与接口与该切面在同一个包中, 可以省略包名.
–execution(public * com.ehome.aop.Calculator.*(..))
匹配 Calculator 接口的所有公有方法.
–execution (public int Calculator.*(..))
匹配 Calculator 中返回 int 类型数值的方法
–execution(public int Calculator.*(int , ..))
匹配第一个参数为 int 类型的方法, .. 匹配任意数量任意类型的参数
–execution (public int Calculator.*(int, int):)
匹配参数类型为 int,int类型的方法.
4.目标类,不需要进行任何额外操作
package com.bj169.invoke.impl;
import com.bj169.invoke.MathCalc;
import org.springframework.stereotype.Component;
/**
* @ClassName MathCalcImpl
* @Description TODO
* @Author Administrator
* @Date 2018/12/13 0013 9:36
* @Version 1.0
**/
//需要将执行过程以日志的形式记录
@Component
public class MathCalcImpl implements MathCalc {
@Override
public Integer add(Integer a, Integer b) {
return a + b;
}
@Override
public Integer reduce(Integer a, Integer b) {
return a - b;
}
}