描述:对于AOP术语一些总结以及相关的案例
标签:Spring 面向切面编程
-
目录
{:toc}
几个术语的基本解释
-
通知
直接明白的说就是你想要的功能(为了更清晰的逻辑,让你的业务逻辑去关注本身的业务,而不是去想其他的事情,这些其他的事情包括,比如安全、事务、日志等)
-
连接点
就是允许你使用通知的地方,比如每个方法的前后或者抛出异常时都是连接点。Spring只支持方法连接点
-
切入点
比如说,你的一个类里有15个方法,那不就是几十个连接点了,但是你不可能在所有方法上面都写一个通知吧,你只是想让其中几个方法,就可以使用切点去筛选连接点,选中那几个你想要的方法
-
切面
切面就是通知和切入点的集合,也就是一个类。没连接点什么事情,连接点就是为了让你更好的去理解切点。通知说明了干什么和什么时候干(通过@Before @After等注解就可以明白),而切入点说明了在哪里干(指定到底是哪个方法)
-
引入
允许我们向现有的类(目标类)添加新方法属性。其实说白了就是切面类(这里不就是定义了切入点 通知嘛),就是切面类可以为目标类添加新方法属性。
-
目标
也就是上面提到的目标类,也就是需要被通知的对象。这个目标类的话就是我们的真正业务逻辑,而不是其他事情(其他事情在切面类中去做,上面提到的日志、事务等)
-
代理
怎么实现整套AOP机制的,都是通过代理
-
织入
把切面类上的逻辑应用到目标对象创建新的代理对象的过程。
AOP原理
Spring使用代理类包裹切面,把代理类织入到Spring管理的Bean中。也就是说代理类会先执行切面,然后转发给真正的bean去执行相应的逻辑。
🐫第一种模式:兄弟模式
1、实现和目标类相同的接口,我也实现和你一样的接口,反正上层都是接口级别的调用,这样我就伪装成了和目标类一样的类(实现了同一接口,咱是兄弟了),也就逃过了类型检查,到java运行期的时候,利用多态的后期绑定(所以spring采用运行时),伪装类(代理类)就变成了接口的真正实现,而他里面包裹了真实的那个目标类,最后实现具体功能的还是目标类,只不过伪装类在之前干了点事情(写日志,安全检查,事物等)。
2、这就好比,一个人让你办件事,每次这个时候,你弟弟就会先出来,当然他分不出来了,以为是你,你这个弟弟虽然办不了这事,但是他知道你能办,所以就答应下来了,并且收了点礼物(写日志),收完礼物了,给把事给人家办了啊,所以你弟弟又找你这个哥哥来了,最后把这是办了的还是你自己。但是你自己并不知道你弟弟已经收礼物了,你只是专心把这件事情做好。
🐫第二种模式:父子模式
1、要是本身这个类就没实现一个接口呢,也就是说兄弟没法做了,我就压根没有机会让你搞出这个双胞胎的弟弟,那么就用其他方式,创建一个目标类的子类,生个儿子,让儿子做代理。生成子类调用,这次用子类来做为代理类,子类重写了目标类的所有方法,当然在这些重写的方法中,不仅实现了目标类的功能,还在这些功能之前,实现了一些其他的(写日志,安全检查,事务等)。
2、 这次的对比就是。儿子先从爸爸那把本事都学会了,所有人都找儿子办事情,但是儿子每次办和爸爸同样的事之前,都要收点小礼物(写日志),然后才去办真正的事。当然爸爸是不知道儿子这么干的了。这里就有件事情要说,某些本事是爸爸独有的(final的),儿子学不了,学不了就办不了这件事,办不了这个事情,自然就不能收人家礼了。
总结
前一种兄弟模式,spring会使用JDK的java.lang.reflect.Proxy类,它允许Spring动态生成一个新类来实现必要的接口,织入通知,并且把对这些接口的任何调用都转发到目标类。
后一种父子模式,spring使用CGLIB库生成目标类的一个子类,在创建这个子类的时候,spring织入通知,并且把对这个子类的调用委托到目标类。
相比之下,还是兄弟模式好些,他能更好的实现松耦合,尤其在今天都高喊着面向接口编程的情况下,父子模式只是在没有实现接口的时候,也能织入通知,应当做一种例外。
实际案例
原始写法
现在的需求是在一个接口里面方法调用的前后,打印调用钱的毫秒数以及调用后的毫秒数
如下面的代码,这是接口的定义:
package com.hht.api.aopdemo;
/**
* @author hht
* @create 2019-08-09 11:16
*/
public interface Dao {
public void insert();
public void delete();
}
下面是实现类:
package com.hht.api.aopdemo;
/**
* @author hht
* @create 2019-08-09 11:22
*/
public class DaoImpl implements Dao {
@Override
public void insert() {
System.out.println("现在调用的是insert方法");
}
@Override
public void delete() {
System.out.println("现在调用的是delete方法");
}
}
现在需要在调用前后实现打印功能的话 那么最原始的写法是,新建一个类去包装他们,再书写一层方法
package com.hht.api.aopdemo;
/**
* @author hht
* @create 2019-08-09 11:28
*/
public class ServiceImpl {
private Dao dao = new DaoImpl();
public void insert(){
System.out.println("insert执行开始的时间是:"+System.currentTimeMillis());
dao.insert();
System.out.println("insert执行结束的时间是:"+System.currentTimeMillis());
}
public void delete(){
System.out.println("delete执行开始的时间是:"+System.currentTimeMillis());
dao.delete();
System.out.println("delete执行结束的时间是:"+System.currentTimeMillis());
}
}
这种原始的写法缺点比较明显:
1、输出调用前后时间的业务逻辑无法复用
2、如果这个Dao还有其他的实现类的,还需要书写一个新的ServiceImpl类进行包装实现业务逻辑
使用装饰者模式
装饰者模式的核心就是实现Dao接口同时持有Dao接口的引用。我们新建类LogDao
package com.hht.api.aopdemo;
/**
* @author hht
* @create 2019-08-09 11:46
*/
public class LogDao implements Dao {
private Dao dao;
public LogDao(Dao dao) {
this.dao = dao;
}
@Override
public void insert() {
System.out.println("insert执行开始的时间是:"+System.currentTimeMillis());
dao.insert();
System.out.println("insert执行结束的时间是:"+System.currentTimeMillis());
}
@Override
public void delete() {
System.out.println("delete执行开始的时间是:"+System.currentTimeMillis());
dao.delete();
System.out.println("delete执行结束的时间是:"+System.currentTimeMillis());
}
}
在实际调用的时候 使用 Dao dao = new LogDao(new DaoImpl())的方式调用,里面DaoIml是实际实现接口的类。
可以看出这种方式的优点:
1、透明。对于调用着来说。他只知道他自己实现的dao功能,但是日志功能他不知晓,实现了透明化
2、类不会无限膨胀,如果说Dao的其他实现类需要输出日志,只需要向LogDao的构造函数中传入对应的实现类即可。
缺点:
1、其实还是一样的问题,就是输出日志的逻辑不能实现复用
2、输出日志部分的逻辑代码和实际业务逻辑代码有耦合(其实说的就是第一个缺点)
使用代理模式(Java代理)
使用代理模式的话,那么我们需要定义InvocationHandler(实现该接口)。
下面是代理类的实现代码:
package com.hht.api.aopdemo;
import org.omg.PortableInterceptor.ObjectReferenceFactory;
import sun.reflect.generics.scope.MethodScope;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
/**
* @author hht
* @create 2019-08-09 14:28
*/
public class LogInvocationHandler implements InvocationHandler {
private Object object;
public LogInvocationHandler(Object object) {
this.object = object;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
String methodName = method.getName();
if("insert".equals(methodName) || "delete".equals(methodName)){
System.out.println(methodName +"()方法开始执行的时间是:"+System.currentTimeMillis());
//实际执行目标的业务逻辑
Object result = method.invoke(object, args);
System.out.println(methodName +"()方法结束的时间是:"+System.currentTimeMillis());
return result;
}
return method.invoke(object,args);
}
}
测试类的书写:(这里直接使用main了)
package com.hht.api.aopdemo;
import java.lang.reflect.Proxy;
/**
* @author hht
* @create 2019-08-09 11:34
*/
public class Test {
public static void main(String[] args) {
// ServiceImpl service = new ServiceImpl();
//
// service.insert();
// service.delete();
//测试代理模式
Dao dao = new DaoImpl();
Dao proxyDao = (Dao) Proxy.newProxyInstance(LogInvocationHandler.class.getClassLoader(), new Class<?>[]{Dao.class}, new LogInvocationHandler(dao));
proxyDao.insert();
System.out.println("-----------------");
proxyDao.delete();
System.out.println("-------------------");
}
}
显然优点是比较明显的:
1、输出日志的逻辑已经复用起来了,如果需要针对其他接口用上输出日志的逻辑,只要在newProxyInstance的后面的第二个参数增加Class<?>数组里面的内容即可
缺点:
1、 JDK提供的动态代理只能针对接口做代理,不能针对类做代理
2、代码还是不足,如果需要对接口中新的方法,比如说新加了一个update方法,也需要添加日志打印,那么你在带来类中还是需要增加相应的判断
使用代理模式(CGLIB代理)
使用CGLIB的话,需要实现MethodInterceptor
下面是代理类的实现代码:
package com.hht.api.aopdemo;
import org.springframework.cglib.proxy.MethodInterceptor;
import org.springframework.cglib.proxy.MethodProxy;
import java.lang.reflect.Method;
/**
* @author hht
* @create 2019-08-09 14:57
* CGLIB代理类
*/
public class DaoProxy implements MethodInterceptor {
@Override
public Object intercept(Object object, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
String methodName = method.getName();
if("insert".equals(methodName) || "delete".equals(methodName)){
System.out.println(methodName +"()方法开始执行的时间是:"+System.currentTimeMillis());
//实际执行目标的业务逻辑
methodProxy.invokeSuper(object,args);
System.out.println(methodName +"()方法结束的时间是:"+System.currentTimeMillis());
return object;
}
methodProxy.invokeSuper(object,args);
return object;
}
}
测试类的书写:
package com.hht.api.aopdemo;
import org.springframework.cglib.proxy.Enhancer;
import java.lang.reflect.Proxy;
/**
* @author hht
* @create 2019-08-09 11:34
*/
public class Test {
public static void main(String[] args) {
// ServiceImpl service = new ServiceImpl();
//
// service.insert();
// service.delete();
//测试代理模式 Java代理
Dao dao = new DaoImpl();
Dao proxyDao = (Dao) Proxy.newProxyInstance(LogInvocationHandler.class.getClassLoader(), new Class<?>[]{Dao.class}, new LogInvocationHandler(dao));
proxyDao.insert();
System.out.println("-----------------");
proxyDao.delete();
System.out.println("-------------------");
System.out.println("CGLIB开始");
//测试代理模式 CGLIB代理
DaoProxy daoProxy = new DaoProxy();
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(DaoImpl.class); //这是与Java代理的不同 可以对类进行代理
enhancer.setCallback(daoProxy);
Dao dao1 = (DaoImpl)enhancer.create();
dao1.insert();
System.out.println("-----------------");
dao1.delete();
System.out.println("--------------");
}
}
对比的话,其实好像和JDK代理没有什么区别,只是CGLIB解决了JDK代理只能针对接口做代理的不足。
转下区别: https://blog.csdn.net/yhl_jxy/article/details/80635012
1、JDK动态代理
利用拦截器(拦截器必须实现InvocationHanlder)加上反射机制生成一个实现代理接口的匿名类,
在调用具体方法前调用InvokeHandler来处理。
2、CGLIB动态代理
利用ASM开源包,对代理对象类的class文件加载进来,通过修改其字节码生成子类来处理。
3、何时使用JDK还是CGLIB?
1)如果目标对象实现了接口,默认情况下会采用JDK的动态代理实现AOP。
2)如果目标对象实现了接口,可以强制使用CGLIB实现AOP。
3)如果目标对象没有实现了接口,必须采用CGLIB库,Spring会自动在JDK动态代理和CGLIB之间转换。
4、如何强制使用CGLIB实现AOP?
1)添加CGLIB库(aspectjrt-xxx.jar、aspectjweaver-xxx.jar、cglib-nodep-xxx.jar)
2)在Spring配置文件中加入<aop:aspectj-autoproxy proxy-target-class=“true”/>
5、JDK动态代理和CGLIB字节码生成的区别?
1)JDK动态代理只能对实现了接口的类生成代理,而不能针对类。
2)CGLIB是针对类实现代理,主要是对指定的类生成一个子类,覆盖其中的方法,
并覆盖其中方法实现增强,但是因为采用的是继承,所以该类或方法最好不要声明成final,
对于final类或方法,是无法继承的。
6、CGlib比JDK快?
1)使用CGLib实现动态代理,CGLib底层采用ASM字节码生成框架,使用字节码技术生成代理类,
在jdk6之前比使用Java反射效率要高。唯一需要注意的是,CGLib不能对声明为final的方法进行代理,
因为CGLib原理是动态生成被代理类的子类。
2)在jdk6、jdk7、jdk8逐步对JDK动态代理优化之后,在调用次数较少的情况下,JDK代理效率高于CGLIB代理效率,
只有当进行大量调用的时候,jdk6和jdk7比CGLIB代理效率低一点,但是到jdk8的时候,jdk代理效率高于CGLIB代理,
总之,每一次jdk版本升级,jdk代理效率都得到提升,而CGLIB代理消息确有点跟不上步伐。
7、Spring如何选择用JDK还是CGLIB?
1)当Bean实现接口时,Spring就会用JDK的动态代理。
2)当Bean没有实现接口时,Spring使用CGlib是实现。
3)可以强制使用CGlib(在spring配置中加入<aop:aspectj-autoproxy proxy-target-class=“true”/>)。
千呼万唤始出来的AOP
使用AOP
Aop的概念:
1、AOP(Aspect Oriented Programming),即面向切面编程,可以说是OOP(Object Oriented Programming,面向对象编程)的补充和完善。OOP引入封装、继承、多态等概念来建立一种对象层次结构,用于模拟公共行为的一个集合。不过OOP允许开发者定义纵向的关系,但并不适合定义横向的关系,例如日志功能。日志代码往往横向地散布在所有对象层次中,而与它对应的对象的核心功能毫无关系对于其他类型的代码,如安全性、异常处理和透明的持续性也都是如此,这种散布在各处的无关的代码被称为横切(cross cutting),在OOP设计中,它导致了大量代码的重复,而不利于各个模块的重用
2、使用"横切"技术,AOP把软件系统分为两个部分:核心关注点和横切关注点。业务处理的主要流程是核心关注点,与之关系不大的部分是横切关注点。横切关注点的一个特点是,他们经常发生在核心关注点的多处,而各处基本相似,比如权限认证、日志、事物。AOP的作用在于分离系统中的各种关注点,将核心关注点和横切关注点分离开来。
比如说现在的需求是在访问每个Controller前 记录下请求方法 参数邓 访问后返回结果
首先需要定义一个切面(切入点和通知的集合)代码如下:
package com.hht.api.aopdemo;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
import java.util.Arrays;
/**
*
*切面 需要进行增强的业务实现 在这里进行编码书写
*/
@Aspect
@Component
public class WebLogAcpect {
private Logger logger = LoggerFactory.getLogger(WebLogAcpect.class);
/**
* 定义切入点,切入点为com.example.aop下的所有函数
*/
@Pointcut("execution(public * com.hht.api.controller.*.*(..))")
public void webLog(){}
/**
* 前置通知:在连接点之前执行的通知
* @param joinPoint
* @throws Throwable
*/
@Before("webLog()")
public void doBefore(JoinPoint joinPoint) throws Throwable {
// 接收到请求,记录请求内容
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = attributes.getRequest();
// 记录下请求内容
logger.info("URL : " + request.getRequestURL().toString());
logger.info("HTTP_METHOD : " + request.getMethod());
logger.info("IP : " + request.getRemoteAddr());
logger.info("CLASS_METHOD : " + joinPoint.getSignature().getDeclaringTypeName() + "." + joinPoint.getSignature().getName());
logger.info("ARGS : " + Arrays.toString(joinPoint.getArgs()));
}
@AfterReturning(returning = "ret",pointcut = "webLog()")
public void doAfterReturning(Object ret) throws Throwable {
// 处理完请求,返回内容
logger.info("RESPONSE : " + ret);
}
}
Controller层代码:
@RequestMapping(value = "/sayHello",method = RequestMethod.GET)
public String sayHello(String name){
return "hello " + name;
}
观看控制台信息:
这里我们可以看出:
1、切面上写的代码是可以复用的
2、避免代理类中去关注目标类的方法,我们只需要关注切面内容本身
3、代码和代码之间没有耦合
用一张图来展示AOP的作用:
AOP总结
我们传统的编程方式是垂直化的编程,即A–>B–>C–>D这么下去,一个逻辑完毕之后执行另外一段逻辑。但是AOP提供了另外一种思路,它的作用是在业务逻辑不知情(即业务逻辑不需要做任何的改动)的情况下对业务代码的功能进行增强,这种编程思想的使用场景有很多,例如事务提交、方法执行之前的权限检测、日志打印、方法调用事件等等。
AOP使用场景举例
1、MyBatis的事务默认是不会自动提交的,因此在编程的时候我们必须在增删改完毕之后调用SqlSession的commit()方法进行事务提交,这非常麻烦,下面利用AOP简单写一段代码帮助我们自动提交事务。
代码:
public void commit(JoinPoint jp) {
Object obj = jp.getTarget();
if (obj instanceof MailDao) {
Signature signature = jp.getSignature();
if (signature instanceof MethodSignature) {
SqlSession sqlSession = SqlSessionThrealLocalUtil.getSqlSession();
MethodSignature methodSignature = (MethodSignature)signature;
Method method = methodSignature.getMethod();
String methodName = method.getName();
if (methodName.startsWith("insert") || methodName.startsWith("update") || methodName.startsWith("delete")) {
sqlSession.commit();
}
sqlSession.close();
}
}
}
2、权限控制的例子,不管是从安全角度考虑还是从业务角度考虑,我们在开发一个Web系统的时候不可能所有请求都对所有用户开放,因此这里就需要做一层权限控制了,大家看AOP作用的时候想必也肯定会看到AOP可以做权限控制,这里我就演示一下如何使用AOP做权限控制。我们知道原生的Spring MVC,Java类是实现Controller接口的,基于此,利用AOP做权限控制的大致代码如下
public void hasPermission(JoinPoint jp) throws Exception {
Object obj = jp.getTarget();
if (obj instanceof Controller) {
Signature signature = jp.getSignature();
MethodSignature methodSignature = (MethodSignature)signature;
// 获取方法签名
Method method = methodSignature.getMethod();
// 获取方法参数
Object[] args = jp.getArgs();
// Controller中唯一一个方法的方法签名ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response) throws Exception;
// 这里对这个方法做一层判断
if ("handleRequest".equals(method.getName()) && args.length == 2) {
Object firstArg = args[0];
if (obj instanceof HttpServletRequest) {
HttpServletRequest request = (HttpServletRequest)firstArg;
// 获取用户id
long userId = Long.parseLong(request.getParameter("userId"));
// 获取当前请求路径
String requestUri = request.getRequestURI();
if(!PermissionUtil.hasPermission(userId, requestUri)) {
throw new Exception("没有权限");
}
}
}
}
}
= args[0];
if (obj instanceof HttpServletRequest) {
HttpServletRequest request = (HttpServletRequest)firstArg;
// 获取用户id
long userId = Long.parseLong(request.getParameter(“userId”));
// 获取当前请求路径
String requestUri = request.getRequestURI();
if(!PermissionUtil.hasPermission(userId, requestUri)) {
throw new Exception(“没有权限”);
}
}
}
}
}
来源:https://blog.csdn.net/Sunshine_he_Fly/article/details/98967135