草庐IT

约定编程与Sping AOP

Scott 2023-03-28 原文

 

一、约定编程

Spring AOP是一种约定流程的编程,咱们可以先通过动态代理模式的实现来理解Spring AOP的概念。

代理的逻辑很简单,例如,当你需要采访一名儿童时,首先需要经过他父母的同意,在一些问题上父母也许会替他回答,而对于另一些问题,也许父母觉得不太适合这个小孩会拒绝掉,显然这时父母就是这名儿童的代理(proxy)了。通过代理可以增强或者控制对儿童这个真实对象(target)的访问。

 

 

 

 

1. 首先实现拦截器接口Interceptor(自己定义的接口)

下面代码均使用spring boot 2.6.2

package com.springboot.chapter4.intercept;

import java.lang.reflect.InvocationTargetException;
import com.springboot.chapter4.invoke.Invocation;

public class MyInterceptor implements Interceptor {

    @Override
    public boolean before() {
        System.out.println("before ......");
        return true;
    }

    @Override
    public boolean useAround() {
        return true;
    }

    @Override
    public void after() {
        System.out.println("after ......");
    }

    @Override
    public Object around(Invocation invocation) 
           throws InvocationTargetException, IllegalAccessException 
   {
        System.out.println("around before ......");
        Object obj = invocation.proceed();
        System.out.println("around after ......");
        return obj;
    }

    @Override
    public void afterReturning() {
        System.out.println("afterReturning......");

    }

    @Override
    public void afterThrowing() {
        System.out.println("afterThrowing......");
    }

}

 

 

 

 2 创建代理对象

在Java的JDK中,提供了类Proxy的静态方法——newProxyInstance,给予我们来生成一个代理对象(proxy):

public static Object  newProxyInstance(ClassLoader classLoader, Class<?>[] interfaces, 
    InvocationHandler invocationHandler) throws IllegalArgumentException

它有3个参数:

•classLoader——类加载器;(被代理的类)

•interfaces——绑定的接口,也就是把代理对象绑定到哪些接口下,可以是多个;(被代理的类实现的接口列表)

•invocationHandler ——绑定代理对象逻辑实现。

这里的invocationHandler是一个接口InvocationHandler对象,它定义了一个invoke方法,这个方法就是实现代理对象的约定流程

package com.springboot.chapter4.proxy;
package com.springboot.chapter4.proxy;
/**** imports ****/
public class ProxyBean implements InvocationHandler {

     private Object target = null;//代理对象
     private Interceptor interceptor = null;//拦截器

     public ProxyBean (Object target,Interceptor interceptor){
       this.target=target;
       this.interceptor=interceptor;
     }

     /**
     * 处理代理对象方法逻辑
     * @param proxy 代理对象
     * @param method 当前方法
     * @param args  运行参数
     * @return 方法调用结果
     * @throws Throwable 异常
     */
    @Override
    public Object invoke(Object proxy, Method method, Object[] args)  {
        // 异常标识
        boolean exceptionFlag = false;
        Invocation invocation = new Invocation(target, method, args);
        Object retObj = null; 
        try {
            if (this.interceptor.before()) {
                retObj = this.interceptor.around(invocation);
            } else {
                retObj = method.invoke(target, args);
            }
        } catch (Exception ex) {
            // 产生异常
            exceptionFlag = true;
        }
        this.interceptor.after();
        if (exceptionFlag) {
            this.interceptor.afterThrowing();
        } else {
            this.interceptor.afterReturning();
            return retObj;
        }
        return null;
    }

}

从上面例子可以看到:代理对象proxyBean定义了织入流程即invoke方法,将拦截器MyInterceptor包装到目标对象tagert的method上。

 

3. 测试效果

/**
     * 绑定代理对象
     * @param target 被代理对象
     * @param interceptor 拦截器
     * @return 代理对象
     */
    public static Object getProxyObject(Object target, Interceptor interceptor) {
        ProxyBean proxyBean = new ProxyBean(target,interceptor);//proxyBean定义了织入流程即invoke方法
        // 生成代理对象
        Object proxy = Proxy.newProxyInstance(
target.getClass().getClassLoader(), target.getClass().getInterfaces(), proxyBean);//proxyBean实现target对象的接口列表,实现方式为调用代理对象的方法时通过invoke(invoke可调用target对象的方法)调用target对象对应的方法。
// 返回代理对象 return proxy; }
private static void testProxy() {
    IHelloService helloService = new HelloServiceImpl();//被代理对象
    // 按约定获取proxy
    IHelloService proxy = (IHelloService) getProxyObject(
        helloService, new MyInterceptor());
    proxy.sayHello("zhangsan");
    System.out.println("\n###############name is null!!#############\n");

    proxy.sayHello(null);
}

 

 

二、Spring AOP概念和术语

Spring AOP是一种基于方法的AOP,它只能应用于方法上。在Spring中可以使用多种方式配置AOP,因为Spring Boot采用注解方式,所以这里只介绍使用@AspectJ注解的方式。AOP可以减少大量重复的工作,最为典型的应用场景就是数据库事务的管控。比如数据库的打开和关闭以及事务的提交和回滚都有流程默认给你实现。换句话说,你都不需要完成它们,你需要完成的任务是编写SQL这一步而已,然后织入流程中。

 

 

 

@Autowired
private UserDao = null;
......

@Transactional
public int inserUser(User user) {
    return userDao.insertUser(user);
}

这里可以看到仅仅使用了一个注解@Transactional,表明该方法需要事务运行,没有任何数据库打开和关闭的代码,也没有事务回滚和提交的代码,却实现了数据库资源的打开和关闭、事务的回滚和提交。

 

1.AOP术语:

•连接点(join point):对应的是具体被拦截的对象,因为Spring只能支持方法,所以被拦截的对象往往就是指特定的方法,例如,我们前面提到的HelloServiceImpl的sayHello方法就是一个连接点,AOP将通过动态代理技术把它织入对应的流程中。

•切点(point cut):有时候,我们的切面不单单应用于单个方法,也可能是多个类的不同方法,这时,可以通过正则式和指示器的规则去定义,从而适配连接点。切点就是提供这样一个功能的概念。

•通知(advice):就是按照约定的流程下的方法,分为前置通知(beforeadvice)、后置通知(after advice)、环绕通知(around advice)、事后返回通知(afterReturning advice)和异常通知(afterThrowing advice),它会根据约定织入流程中,需要弄明白它们在流程中的顺序和运行的条件。

•目标对象(target):即被代理对象,例如,约定编程中的HelloServiceImpl实例就是一个目标对象,它被代理了。

•引入(introduction):是指引入新的类和其方法,增强现有Bean的功能。

•织入(weaving):它是一个通过动态代理技术,为原有服务对象生成代理对象,然后将与切点定义匹配的连接点拦截,并按约定将各类通知织入约定流程的过程。

•切面(aspect):是一个可以定义切点、各类通知和引入的内容,Spring AOP将通过它的信息来增强Bean的功能或者将对应的方法织入流程。

2.具体实现

2.1.添加maven依赖

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
        </dependency>

 

2.2. 定义切面

首先Spring是以@Aspect作为切面声明的,当以@Aspect作为注解时,Spring就会知道这是一个切面,然后我们就可以通过各类注解(@Before、@After、@AfterReturning和@AfterThrowing)来定义各类的通知了。

package com.springboot.chapter4.aspect;
/**** imports ****/
@Component //注入到Spring IOC @Aspect //定义切面 public class MyAspect { //引入 @DeclareParents(value= "co.springboot.chapter4.aspect.service.impl.UserServiceImpl",defaultImpl=UserValidatorImpl.class) public UserValidator userValidator; //定义切点 @Pointcut("execution(* com.springboot.chapter4.aspect.service.impl.UserServiceImpl.printUser(..))") public void pointCut() { }
//无参模式 @Before(
"pointCut()") public void before() { System.out.println("before ......"); } //获取参数模式 //方式1:切点处加入对应的正则式 //方式2:对于非环绕通知还可以使用一个连接点(JoinPoint)类型的参数,通过它也可以获取参数 //正则式pointCut() && args(user)中,pointCut()表示启用原来定义切点的规则,并且约定将连接点(目标对象方法)名称为user的参数传递进来。这里要注意,JoinPoint类型的参数对于非环绕通知而言,Spring AOP会自动地把它传递到通知中;对于环绕通知而言,可以使用ProceedingJoinPoint类型的参数。 @Before("pointCut() && args(user)") public void beforeParam(JoinPoint point, User user) { Object[] args = point.getArgs(); System.out.println("before ......"); } //环绕通知 //注意:用环绕通知注解测试的时候总是不按顺序执行,估计是Spring版本之间的差异留下的问题,这是在使用时需要注意的。所以在没有必要的时候,应尽量不要使用环绕通知,它很强大,但是也很危险。 @Around("pointCut()") public void around(ProceedingJoinPoint jp) throws Throwable { System.out.println("around before......"); jp.proceed();//回调目标对象的原有方法 System.out.println("around after......"); } @After("pointCut()") public void after() { System.out.println("after ......"); } @AfterReturning("pointCut()") public void afterReturning() { System.out.println("afterReturning ......"); } @AfterThrowing("pointCut()") public void afterThrowing() { System.out.println("afterThrowing ......"); } }

 

上述例子用到的基础类如下

execution(* com.springboot.chapter4.aspect.service.impl.UserServiceImpl.printUser(..))

•execution表示在执行的时候,拦截里面的正则匹配的方法;  
•* 表示任意返回类型的方法;

•com.springboot.chapter4.aspect.service.impl.UserServiceImpl 指定目标对象的全限定名称;

•printUser指定目标对象的方法;

•(..)表示任意参数进行匹配。

 

2.4 使用@DeclareParents 定义引入

@DeclareParents,它的作用是在不侵入原有业务的前提上对原有的服务进行增强,它有两个必须配置的属性value和defaultImpl。

•value:指向你要增强功能的目标对象,这里是要增强UserServiceImpl对象,因此可以看到配置为com.springboot.chapter4.aspect.service.impl.UserServiceImpl+。

•defaultImpl:引入增强功能的类,这里配置为UserValidatorImpl,用来提供校验用户是否为空的功能。

 

 2.5 使用切面的指示器

切面中可以使用的指示器如下,比如上述例子中arg()获取切点的参数值

 

 

 

 

3.测试效果

package com.springboot.chapter4.aspect.controller;
/**** imports ****/
// 定义控制器
@Controller
// 定义类请求路径
@RequestMapping("/user")
public class UserController {

    // 注入用户服务
    @Autowired
    private UserService userService = null;

    // 定义请求
    @RequestMapping("/print")
    // 转换为JSON
    @ResponseBody
    public User printUser(Long id, String userName, String note) {
        User user = new User();
        user.setId(id);
        user.setUsername(userName);
        user.setNote(note);
        userService.printUser(user);// 若user=null,则执行afterthrowing方法

        //测试引入
        UserValidator userValidator = (UserValidator)userService;
        if (userValidator.validate(user)) {
            userService.printUser(user);
        }
 
        return user;// 加入断点
    }
}
        

 

3.1未引入效果

before ......
id =1 username =user_name_1 note =2323
after ......
afterReturning ......

 

3.2引入效果

引入新的接口:UserValidator
around before......
before ......
id =1 username =user_name_1 note =2323
around after......
after ......
afterReturning ......

 

 

3.4 引入原理

这里的newProxyInstance的第二个参数为一个对象数组,也就是说这里生产代理对象时,Spring会把UserService和UserValidator两个接口传递进去,让代理对象下挂到这两个接口下,这样这个代理对象就能够相互转换并且使用它们的方法了。

Object proxy = Proxy.newProxyInstance(
    target.getClass().getClassLoader(), 
    target.getClass().getInterfaces(),
    proxyBean);

 

3.5 织入方式

上面我们都是采用接口(如UserService)+实现类(如UserServiceImpl)的模式,这是Spring推荐的方式,本书也遵循这样的方式。但是对于是否拥有接口则不是Spring AOP的强制要求,对于动态代理的也有多种实现方式,我们之前谈到的JDK只是其中的一种,业界比较流行的还有CGLIB、Javassist、ASM等。Spring采用了JDK和CGLIB,对于JDK而言,它是要求被代理的目标对象必须拥有接口,而对于CGLIB则不做要求。因此在默认的情况下,Spring会按照这样的一条规则处理,即当你需要使用AOP的类拥有接口时,它会以JDK动态代理运行,否则以CGLIB运行。

不使用接口例子

......
// 定义控制器
@Controller
// 定义类请求路径
@RequestMapping("/user")
public class UserController {
    // 使用非接口注入
    @Autowired
    private UserServiceImpl userService = null;

    // 定义请求
    @RequestMapping("/print")
    // 返回JSON
    @ResponseBody
    public User printUser(Long id, String userName, String note) {
        User user = new User();
        user.setId(id);
        user.setUsername(userName);
        user.setNote(note);
        userService.printUser(user);
        return user;// 加入断点测试
     }
     ......
}

 

 此时Spring已经使用了CGLIB为我们生成代理对象,从而将切面的内容织入对应的流程中。当使用接口时,用JDK为我们生成代理对象。

 

 

4. 多个切面

Spring提供了一个注解@Order和一个接口Ordered,我们可以使用它们的任意一个指定切面的顺序。

......
@Aspect
@Order(1)
public class MyAspect1 {
......
}

//或者

......
@Aspect
public class MyAspect1 implements Ordered {
    // 指定顺序
    @Override
    public int getOrder() {
        return 1;
    }
    ....
}

 

效果如下

MyAspect1 before ......
MyAspect2 before ......
MyAspect3 before ......
测试多个切面顺序
MyAspect3 after ......
MyAspect3 afterReturning ......
MyAspect2 after ......
MyAspect2 afterReturning ......
MyAspect1 after ......
MyAspect1 afterReturning ......

 

有关约定编程与Sping AOP的更多相关文章

  1. ruby - 寻找通过阅读代码确定编程语言的ruby gem? - 2

    几个月前,我读了一篇关于ruby​​gem的博客文章,它可以通过阅读代码本身来确定编程语言。对于我的生活,我不记得博客或gem的名称。谷歌搜索“ruby编程语言猜测”及其变体也无济于事。有人碰巧知道相关gem的名称吗? 最佳答案 是这个吗:http://github.com/chrislo/sourceclassifier/tree/master 关于ruby-寻找通过阅读代码确定编程语言的rubygem?,我们在StackOverflow上找到一个类似的问题:

  2. 网络编程套接字 - 2

    网络编程套接字网络编程基础知识理解源`IP`地址和目的`IP`地址理解源MAC地址和目的MAC地址认识端口号理解端口号和进程ID理解源端口号和目的端口号认识`TCP`协议认识`UDP`协议网络字节序socket编程接口`sockaddr``UDP`网络程序服务器端代码逻辑:需要用到的接口服务器端代码`udp`客户端代码逻辑`udp`客户端代码`TCP`网络程序服务器代码逻辑多个版本服务器单进程版本多进程版本多线程版本线程池版本服务器端代码客户端代码逻辑客户端代码TCP协议通讯流程TCP协议的客户端/服务器程序流程三次握手(建立连接)数据传输四次挥手(断开连接)TCP和UDP对比网络编程基础知识

  3. ruby - 我正在学习编程并选择了 Ruby。我应该升级到 Ruby 1.9 吗? - 2

    我完全不是程序员,正在学习使用Ruby和Rails框架进行编程。我目前正在使用Ruby1.8.7和Rails3.0.3,但我想知道我是否应该升级到Ruby1.9,因为我真的没有任何升级的“遗留”成本。缺点是什么?我是否会遇到与普通gem的兼容性问题,或者甚至其他我不太了解甚至无法预料的问题? 最佳答案 你应该升级。不要坚持从1.8.7开始。如果您发现不支持1.9.2的gem,请避免使用它们(因为它们很可能不被维护)。如果您对gem是否兼容1.9.2有任何疑问,您可以在以下位置查看:http://www.railsplugins.or

  4. ruby - 如何以编程方式删除实例上的 "singleton information"以使其编码(marshal)? - 2

    我创建了一个由于“在运行时执行的单例元类定义”而无法编码的对象(这段代码的描述是否正确?)。这是通过以下代码执行的:#defineclassXthatmyusesingletonclassmetaprogrammingfeatures#throughcallofmethod:break_marshalling!classXdefbreak_marshalling!meta_class=class我该怎么做才能使对象编码正确?是否可以从对象instance_of_x的classX中“移除”单例组件?我真的需要一个建议,因为我们的一些对象需要通过Marshal.dump序列化机制进行缓存。

  5. Ruby 元编程问题 - 2

    我正在查看Ruby日志记录库Logging.logger方法并从sourceatgithub提出问题与这段代码有关:logger=::Logging::Logger.new(name)logger.add_appendersappenderlogger.additive=falseclass我知道类 最佳答案 这实际上删除了方法(当它实际被执行时)。这是确保close不会被调用两次的保障措施。看起来好像有嵌套的“class 关于Ruby元编程问题,我们在StackOverflow上找到一

  6. ruby - Paperclip:以编程方式分配图像并设置其名称 - 2

    使用Paperclip,我想从这样的URL抓取图像:require'open-uri'user.photo=open(url)问题是我最后得到一个像“open-uri20110915-4852-1o7k5uw”这样的文件名。有什么方法可以更改user.photo上的文件名?作为一个额外的变化,Paperclip将我的文件存储在S3上,所以如果我可以在初始分配中设置我想要的文件名就更好了,这样图像就会上传到正确的S3key。像这样:user.photo=open(url),:filename=>URI.parse(url).path 最佳答案

  7. ruby - 如何以编程方式检查证书是否已被吊销? - 2

    我正在开发一个xcode自动构建系统。在执行一些预构建验证时,我想检查指定的证书文件是否已被撤销。我了解securityverify-cert验证其他证书属性但不验证吊销。我如何检查撤销?我正在用Ruby编写构建系统,但我对任何语言的想法都持开放态度。我阅读了这个答案(Openssl-Howtocheckifacertificateisrevokedornot),但指向底部的链接(DoesOpenSSLautomaticallyhandleCRLs(CertificateRevocationLists)now?)进入的Material对我的目的来说有点过于复杂(用户上传已撤销的证书是一

  8. ruby - 如何保持我不常用的编程语言技能 - 2

    关闭。这个问题是off-topic.它目前不接受答案。想改进这个问题吗?Updatethequestion所以它是on-topic用于堆栈溢出。关闭11年前。Improvethisquestion我不经常使用ruby​​-通常它加起来相当于每两个月或更长时间编写一次脚本。我的大部分编程都是使用C++进行的,这与ruby​​有很大不同。由于我与ruby​​之间的差距如此之大,我总是忘记语言的基本方面(比如解析文本文件和其他简单的东西)。我想每天练习一些基本的东西,我想知道是否有一些我可以订阅的网站,并且会向我发送当天的Ruby问题或类似的东西。有人知道这样的站点/Internet服务吗?

  9. ruby - 如何以编程方式将 mp3 转换为 itunes 可播放的 aac/m4a 文件? - 2

    我一直在寻找一种以编程方式或通过命令行将mp3转换为aac的方法,但没有成功。理想情况下,我有一段代码可以从我的Rails应用程序中调用,将mp3转换为aac。我安装了ffmpeg和libfaac,并能够使用以下命令创建aac文件:ffmpeg-itest.mp3-acodeclibfaac-ab163840dest.aac当我将输出文件的名称更改为dest.m4a时,它无法在iTunes中播放。谢谢! 最佳答案 FFmpeg提供AAC编码功能(如果您已编译它们)。如果您使用的是Windows,则可以从here获取完整的二进制文件。

  10. ruby - 以编程方式从字符串派生正则表达式 - 2

    我想输入一个字符串并返回一个可用于描述字符串结构的正则表达式。正则表达式将用于查找更多与第一个结构相同的字符串。这是故意模棱两可的,因为我肯定会漏掉SO社区中的某个人会发现的情况。请发布任何和所有可能的方法来做到这一点。 最佳答案 简单的答案(可能不是您想要的)是:返回输入字符串(正则表达式特殊字符转义)。这始终是与字符串匹配的正则表达式。如果您希望识别某些结构,则必须提供有关您希望识别的结构类型的更多信息。如果没有这些信息,问题就会以模棱两可的方式陈述,并且有许多可能的解决方案。例如,输入字符串'aba'可以描述为'阿巴''阿巴*

随机推荐