什么是AOP
Aspect Oriented Programming 的缩写,翻译为:面向切面编程。
利用AOP可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率。
用一个例子解释下AOP,我们现在有一个完整的登录功能代码,但我们想在加入一个权限控制,在原始方式,我们需要去修改源代码,加入具体逻辑,不需要的时候还需要逐行修改,很麻烦。现在我们利用AOP,先写一个权限管理的模块,通过AOP的方式加入到源代码中,不需要的时候把,对该模块的使用删掉即可,很方便。
使用AOP主要意图就是:
将日志记录,性能统计,安全控制,事务处理,异常处理等代码从业务逻辑代码中划分出来,通过对这些行为的分离,我们希望可以将它们独立到非指导业务逻辑的方法中,进而改变这些行为的时候不影响业务逻辑的代码。
AOP底层原理
AOP底层使用动态代理
1. 第一种情况,有接口,使用 JDK动态代理
简单编写代码
写一个接口和实现类
public interface UserDao {
int add(int a,int b);
String select(String id);
}
public class UserDaoImpl implements UserDao {
@Override
public int add(int a, int b) {
return a+b;
}
@Override
public String select(String id) {
return id;
}
}
public class JdkProxy {
public static void main(String[] args) {
Class[] interfaces={UserDao.class};
UserDao userDao = new UserDaoImpl();
UserDao userDaoProxy =(UserDao) Proxy.newProxyInstance(JdkProxy.class.getClassLoader(), interfaces, new UserDaoHolder(userDao));
int res = userDaoProxy.add(1, 2);
}
}
class UserDaoHolder implements InvocationHandler{
//创建谁的代理对象,就把谁传过来
private Object obj;
//有参构造传递
public UserDaoHolder(Object obj){
this.obj = obj;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
//方法执行前
System.out.println("方法执行前....."+method.getName()+"传递的参数:...."+ Arrays.toString(args));
//被增强的方法执行
Object res = method.invoke(obj, args);
System.out.println(res.toString());
//方法执行后
System.out.println("方法执行后....."+obj);
return res;
}
}
2. 第二种情况,没有接口,使用CGLIB动态代理
CGLIB动态代理
我们了解到,“代理”的目的是构造一个和被代理的对象有同样行为的对象,一个对象的行为是在类中定义的,对象只是类的实例。所以构造代理,不一定非得通过持有、包装对象这一种方式。
通过“继承”可以继承父类所有的公开方法,然后可以重写这些方法,在重写时对这些方法增强,这就是cglib的思想。根据里氏代换原则(LSP),父类需要出现的地方,子类可以出现,所以cglib实现的代理也是可以被正常使用的。
先看下代码
package proxy;
import java.lang.reflect.Method;
import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;
public class CglibProxy implements MethodInterceptor
{
// 根据一个类型产生代理类,此方法不要求一定放在MethodInterceptor中
public Object CreatProxyedObj(Class<?> clazz)
{
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(clazz);
enhancer.setCallback(this);
return enhancer.create();
}
@Override
public Object intercept(Object arg0, Method arg1, Object[] arg2, MethodProxy arg3) throws Throwable
{
// 这里增强
System.out.println("收钱");
return arg3.invokeSuper(arg0, arg2);
}
}
从代码可以看出,它和jdk动态代理有所不同,对外表现上看CreatProxyedObj,它只需要一个类型clazz就可以产生一个代理对象, 所以说是“类的代理”,且创造的对象通过打印类型发现也是一个新的类型。不同于jdk动态代理,jdk动态代理要求对象必须实现接口(三个参数的第二个参数),cglib对此没有要求。
cglib的原理是这样,它生成一个继承B的类型C(代理类),这个代理类持有一个MethodInterceptor,我们setCallback时传入的。 C重写所有B中的方法(方法名一致),然后在C中,构建名叫“CGLIB”+“ 父 类 方 法 名 父类方法名 父类方法名”的方法(下面叫cglib方法,所有非private的方法都会被构建),方法体里只有一句话super.方法名(),可以简单的认为保持了对父类方法的一个引用,方便调用。
这样的话,C中就有了重写方法、cglib方法、父类方法(不可见),还有一个统一的拦截方法(增强方法intercept)。其中重写方法和cglib方法肯定是有映射关系的。
C的重写方法是外界调用的入口(LSP原则),它调用MethodInterceptor的intercept方法,调用时会传递四个参数,第一个参数传递的是this,代表代理类本身,第二个参数标示拦截的方法,第三个参数是入参,第四个参数是cglib方法,intercept方法完成增强后,我们调用cglib方法间接调用父类方法完成整个方法链的调用。
这里有个疑问就是intercept的四个参数,为什么我们使用的是arg3而不是arg1?
@Override
public Object intercept(Object arg0, Method arg1, Object[] arg2, MethodProxy arg3) throws Throwable
{
System.out.println("收钱");
return arg3.invokeSuper(arg0, arg2);
}
因为如果我们通过反射 arg1.invoke(arg0, …)这种方式是无法调用到父类的方法的,子类有方法重写,隐藏了父类的方法,父类的方法已经不可见,如果硬调arg1.invoke(arg0, …)很明显会死循环。
所以调用的是cglib开头的方法,但是,我们使用arg3也不是简单的invoke,而是用的invokeSuper方法,这是因为cglib采用了fastclass机制,不仅巧妙的避开了调不到父类方法的问题,还加速了方法的调用。
fastclass基本原理是,给每个方法编号,通过编号找到方法执行避免了通过反射调用。
对比JDK动态代理,cglib依然需要一个第三者分发请求,只不过jdk动态代理分发给了目标对象,cglib最终分发给了自己,通过给method编号完成调用。cglib是继承的极致发挥,本身还是很简单的,只是fastclass需要另行理解。
JDK动态代理和CGLIB的区别
DK动态代理只能对实现了接口的类生成代理,而不能针对类。
CGLIB是针对类实现代理,主要是对指定的类生成一个子类,覆盖其中的方法,并覆盖其中方法实现增强,但是因为采用的是继承,所以该类或方法最好不要声明成final, 对于final类或方法,是无法继承的
使用CGLib实现动态代理,CGLib底层采用ASM字节码生成框架,使用字节码技术生成代理类,在jdk6之前比使用Java反射效率要高。唯一需要注意的是,CGLib不能对声明为final的方法进行代理,因为CGLib原理是动态生成被代理类的子类
在jdk6、jdk7、jdk8逐步对JDK动态代理优化之后,在调用次数较少的情况下,JDK代理效率高于CGLIB代理效率,只有当进行大量调用的时候,jdk6和jdk7比CGLIB代理效率低一点,但是到jdk8的时候,jdk代理效率高于CGLIB代理,总之,每一次jdk版本升级,jdk代理效率都得到提升,而CGLIB代理消息确有点跟不上步伐Spring如何选择用
JDK还是CGLIB?
- 当Bean实现接口时,Spring就会用JDK的动态代理。
- 当Bean没有实现接口时,Spring使用CGlib是实现。
为什么继承只能使用CGLib,因为JDK代理生成的代理类,默认会继承一个类,由于java是单继承,所以当原始类继承一个类的时候,只能使用CGLib动态代理
总结
- 如果目标对象实现了接口,默认情况下会采用JDK的动态代理实现AOP。
- 如果目标对象实现了接口,可以强制使用CGLIB实现AOP。
- 如果目标对象没有实现了接口,必须采用CGLIB库,Spring会自动在JDK动态代理和CGLIB之间转换
JDK代理是不需要第三方库支持,只需要JDK环境就可以进行代理,使用条件:
-
实现InvocationHandler
-
使用Proxy.newProxyInstance产生代理对象
-
被代理的对象必须要实现接口
CGLib必须依赖于CGLib的类库,但是它需要类来实现任何接口代理的是指定的类生成一个子类,
AOP官方术语
- 连接点
- 切入点
- 通知(增强)
- 切面
AOP操作
引入AOP相关依赖
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.9.4</version>
</dependency>
切入点表达式
execution(【权限修饰符】 【 返回类型 】【类全路径】 【方法名称】(【参数列表】))
对UserDaoImpl中的add方法进行增强
execution(* com.aop.mapper.impl.UserDaoImpl.add(…))
对UserDaoImpl中的所有方法进行增强
execution(* com.aop.mapper.impl.UserDaoImpl.*(…))
对mapper包中的add方法进行增强
execution(* com.aop.mapper. * . *(…))
1. 使用注解方式实现
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
https://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/aop
https://www.springframework.org/schema/aop/spring-aop.xsd">
<!--开启注解扫描-->
<context:component-scan base-package="com.aop"/>
<!--开启Aspectj生成代理对象-->
<aop:aspectj-autoproxy/>
</beans>
原始的类
@Component
public class User {
publicvoid add(){
System.out.println("User中的add方法");
}
public void update(){
System.out.println("User中的update方法");
}
}
增强的类
@Component
@Aspect
public class UserProxy {
//公共切入点提取
@Pointcut(value = "execution(* com.aop.entity.User.add(..))")
public void pointCutAdd(){
}
@Pointcut(value = "execution(* com.aop.entity.User.update(..))")
public void pointCutUpdate() {
}
//前置方法
// @Before注解表示前置通知
@Before(value = "pointCutAdd()")
public void beforeUser(){
System.out.println("方法执行前。。。。。。。");
}
//后置方法
// @After注解表示前置通知
@After(value = "execution(* com.aop.entity.User.add(..))")
public void afterUser(){
System.out.println("方法执行后。。。。。。。");
}
@AfterReturning(value = "execution(* com.aop.entity.User.add(..))")
public void afterUserReturning(){
System.out.println("afterUserReturning。。。。。。。");
}
@AfterThrowing(value = "execution(* com.aop.entity.User.add(..))")
public void afterUserThrowing(){
System.out.println("afterUserThrowing。。。。。。。");
}
//环绕通知
@Around(value = "pointCutUpdate()")
public void around(ProceedingJoinPoint point) throws Throwable {
System.out.println("环绕里方法执行前。。。。。。。");
//被增强的方法执行
point.proceed();
Object[] args = point.getArgs();//拿到被增强方法的参数
System.out.println(Arrays.toString(args));
System.out.println("环绕里方法执行后。。。。。。。");
}
}
测试
@Test
public void testAop() {
ApplicationContext context = new ClassPathXmlApplicationContext("TestBean1.xml");
User user = context.getBean("user", User.class);
user.add();
user.update();
}
注意
有多个类对同一方法进行增强时,在增强类上加上注解 @Order(数字类型值),数字类型值越小优先级越高。
2. 基于xml方式实现
同样准备一个原始类,一个增强类
public class Book {
public void buy() {
System.out.println("Book中的buy方法");
}
}
public class BookProxy {
public void before() {
System.out.println("方法执行前");
}
}
配置xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
https://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/aop
https://www.springframework.org/schema/aop/spring-aop.xsd">
<!--对象注入-->
<bean id="book" class="com.aop.entity.Book"/>
<bean id="bookProxy" class="com.aop.entity.BookProxy"/>
<!--配置aop增强-->
<aop:config>
<!--切入点-->
<aop:pointcut id="myPointCut" expression="execution(* com.aop.entity.Book.buy(..))"/>
<!--配置切面,就是把增强配置到切入点-->
<aop:aspect ref="bookProxy">
<!--增强要应用在具体的方法上-->
<aop:before method="before" pointcut-ref="myPointCut"/>
</aop:aspect>
</aop:config>
</beans>
测试
@Test
public void testAopXml() {
ApplicationContext context = new ClassPathXmlApplicationContext("TestBean1.xml");
Book book = context.getBean("book", Book.class);
book.buy();
}