Guice框架-AOP(@面向切面编程)

简介:

2.AOP 面向切面编程

2.1 AOP入门

在前面的章节主要讲Guice的依赖注入,有了依赖注入的基础后我们再来看Guice的AOP。我们先从一个例子入手,深入浅出的去理解Guice的AOP的原理和实现。

首先我们定义服务Service,这个服务有一个简单的方法sayHello,当然了我们有一个服务的默认实现ServiceImpl,然后使用@ImplementedBy将服务和默认实现关联起来,同时将服务的实现标注为单例模式。

1 @ImplementedBy(ServiceImpl.class)

2 public interface Service {

3     void sayHello();

4 }

 

在服务的实现ServiceImpl中,我们sayHello方法就是输出一行信息,这行信息包含服务的类名,hashCode以及方法名称和执行的时间。

 

 1 @Singleton

 2 public class ServiceImpl implements Service {

 3 

 4     @Override

 5     @Named("log")

 6     public void sayHello() {

 7         System.out.println(String.format("[%s#%d] execute %s at %d", this.getClass().getSimpleName(),hashCode(),"sayHello",System.nanoTime()));

 8     }

 9 

10 }

11 

接下来定义一个AOP的实现。在Aopalliance中(大家都认可的AOP联盟)实现我们的方法拦截器。这个拦截器 LoggerMethodInterceptor 也没有做什么特别的事情,只是记录些执行的时间,当然了由于执行时间比较短我们用纳秒来描述(尽管不是那么精确)。

在MethodInvocation中我们一定要调用proceed()方法,这样我们的服务才能被执行。当然了如果为了做某些控制我们就能决定是否调用服务代码了。

 1 import static java.lang.System.out;

 3 import org.aopalliance.intercept.MethodInterceptor;

 4 import org.aopalliance.intercept.MethodInvocation;

 5 

 6 public class LoggerMethodInterceptor implements MethodInterceptor {

 7 

 8     @Override

 9     public Object invoke(MethodInvocation invocation) throws Throwable {

10         String methodName = invocation.getMethod().getName();

11         long startTime=System.nanoTime();

12         out.println(String.format("before method[%s] at %s", methodName, startTime));

13         Object ret = null;

14         try {

15             ret = invocation.proceed();

16         } finally {

17             long endTime=System.nanoTime();

18             out.println(String.format(" after method[%s] at %s, cost(ns):%d", methodName, endTime,(endTime-startTime)));

19         }

20         return ret;

21     }

22 }

23 

最后才是我们的客户端程序,注意在这里我们需要绑定一个拦截器,这个拦截器匹配任何类的带有log注解的方法。所以这就是为什么我们服务的实现方法需要用log标注的原因了。

 

 

 1 public class AopDemo {

 2     @Inject

 3     private Service service;

 4 

 5     public static void main(String[] args) {

 6         Injector inj = Guice.createInjector(new Module() {

 7             @Override

 8             public void configure(Binder binder) {

 9                 binder.bindInterceptor(Matchers.any(),//

10                         Matchers.annotatedWith(Names.named("log")),//

11                         new LoggerMethodInterceptor());

12             }

13         });

14         inj.getInstance(AopDemo.class).service.sayHello();

15         inj.getInstance(AopDemo.class).service.sayHello();

16         inj.getInstance(AopDemo.class).service.sayHello();

17     }

18 }

19 

我们的程序输出了我们期望的结果。

 

before method[sayHello] at 7811306067456

[ServiceImpl$$EnhancerByGuice$$96717882#33353934] execute sayHello at 7811321912287

after method[sayHello] at 7811322140825, cost(ns):16073369

before method[sayHello] at 7811322315064

[ServiceImpl$$EnhancerByGuice$$96717882#33353934] execute sayHello at 7811322425280

after method[sayHello] at 7811322561835, cost(ns):246771

before method[sayHello] at 7811322710141

[ServiceImpl$$EnhancerByGuice$$96717882#33353934] execute sayHello at 7811322817521

after method[sayHello] at 7811322952455, cost(ns):242314

 

 

关于此结果有几点说明。

 

(1)由于使用了AOP我们的服务得到的不再是我们写的服务实现类了,而是一个继承的子类,这个子类应该是在内存中完成的。

 

(2)除了第一次调用比较耗时外(可能guice内部做了比较多的处理),其它调用事件为0毫秒(我们的服务本身也没做什么事)。

 

(3)确实完成了我们期待的AOP功能。

 

我们的例子暂且说到这里,来看看AOP的相关概念。

 

2.2 AOP相关概念

 

老实说AOP有一套完整的体系,光是概念就有一大堆,而且都不容易理解。这里我们结合例子和一些场景来大致了解下这些概念。

 

通知(Advice)

 

所谓通知就是我们切面需要完成的功能。比如2.1例子中通知就是记录方式执行的耗时,这个功能我们就称之为一个通知。

 

比 如说在很多系统中我们都会将操作者的操作过程记录下来,但是这个记录过程又不想对服务侵入太多,这样就可以使用AOP来完成,而我们记录日志的这个功能就 是一个通知。通知除了描述切面要完成的工作外还需要描述何时执行这个工作,比如是在方法的之前、之后、之前和之后还是只在有异常抛出时。

 

连接点(Joinpoint)

 

连 接点描述的是我们的通知在程序执行中的时机,这个时机可以用一个“点”来描述,也就是瞬态。通常我们这个瞬态有以下几种:方法运行前,方法运行后,抛出异 常时或者读取修改一个属性等等。总是我们的通知(功能)就是插入这些点来完成我们额外的功能或者控制我们的执行流程。比如说2.1中的例子,我们的通知 (时间消耗)不仅在方法执行前记录执行时间,在方法的执行后也输出了时间的消耗,那么我们的连接点就有两个,一个是在方法运行前,还有一个是在方法运行 后。

 

切入点(Pointcut)

 

切 入点描述的是通知的执行范围。如果通知描述的是“什么时候”做“什么事”,连接点描述有哪些“时候”,那么切入点可以理解为“什么地方”。比如在2.1例 子中我们切入点是所有Guice容器管理的服务的带有@Named(“log”)注解的方法。这样我们的通知就限制在这些地方,这些地方就是所谓的切入 点。

 

切面(Aspect)

 

切面就是通知和切入点的结合。就是说切面包括通知和切入点两部分,由此可见我们所说的切面就是通知和切入点。通俗的讲就是在什么时候在什么地方做什么事。

 

引入(Introduction)

 

引入是指允许我们向现有的类添加新的方法和属性。个人觉得这个特性尽管很强大,但是大部分情况下没有多大作用,因为如果一个类需要切面来增加新的方法或者属性的话那么我们可以有很多更优美的方式绕过此问题,而是在绕不过的时候可能就不是很在乎这个功能了。

 

目标(Target)

 

目标是被通知的对象,比如我们2.1例子中的ServiceImpl 对象。

 

代理(Proxy)

 

代理是目标对象被通知引用后创建出来新的对象。比如在2.1例子中我们拿到的Service对象都不是ServiceImpl本身,而是其包装的子类ServiceImpl$$EnhancerByGuice$$96717882。

 

织入(Weaving)

 

所谓织入就是把切面应用到目标对象来创建新的代理对象的过程。通常情况下我们有几种实际来完成织入过程:

 

编译时:就是在Java源文件编程成class时完成织入过程。AspectJ就存在一个编译器,运行在编译时将切面的字节码编译到目标字节码中。

 

类加载时:切面在目标类加载到JVM虚拟机中时织入。由于是在类装载过程发生的,因此就需要一个特殊的类装载器(ClassLoader),AspectJ就支持这种特性。

 

运行时:切面在目标类的某个运行时刻被织入。一般情况下AOP的容器会建立一个新的代理对象来完成目标对象的功能。事实上在2.1例子中Guice就是使用的此方式。

 

Guice支持AOP的条件是:

 

类必须是public或者package (default) 

类不能是final类型的 

方法必须是public,package或者protected 

方法不能使final类型的 

实例必须通过Guice的@Inject注入或者有一个无参数的构造函数 

2.3 切面注入依赖

 

如果一个切面(拦截器)也需要注入一些依赖怎么办?没关系,Guice允许在关联切面之前将切面的依赖都注入。比如看下面的例子。

 

我们有一个前置服务,就是将所有调用的方法名称输出。

 

 

1 @ImplementedBy(BeforeServiceImpl.class)

2 public interface BeforeService {

4     void before(MethodInvocation invocation);

5 }

1 public class BeforeServiceImpl implements BeforeService {

3     @Override

4     public void before(MethodInvocation invocation) {

5         System.out.println("before method "+invocation.getMethod().getName());

6     }

7 }

然后有一个切面,这个切面依赖前置服务,然后输出一条方法调用结束语句。

 

 1 public class AfterMethodInterceptor implements MethodInterceptor {

 2    @Inject

 3     private BeforeService beforeService;

 4     @Override

 5     public Object invoke(MethodInvocation invocation) throws Throwable {

 6         beforeService.before(invocation);

 7         Object ret = null;

 8         try {

 9             ret = invocation.proceed();

10         } finally {

11             System.out.println("after "+invocation.getMethod().getName());

12         }

13         return ret;

14     }

15 }

 

 

在AopDemo2中演示了如何注入切面的依赖。在第9行,AfterMethodInterceptor 请求Guice注入其依赖。

 

 1 public class AopDemo2 {

 2     @Inject

 3     private Service service;

 4     public static void main(String[] args) {

 5         Injector inj = Guice.createInjector(new Module() {

 6             @Override

 7             public void configure(Binder binder) {

 8                 AfterMethodInterceptor after= new AfterMethodInterceptor();

 9                 binder.requestInjection(after);

10                 binder.bindInterceptor(Matchers.any(),//

11                         Matchers.annotatedWith(Names.named("log")),//

12                         after);

13             }

14         });

15         AopDemo2 demo=inj.getInstance(AopDemo2.class);

16         demo.service.sayHello();

17     }

18 }

 

 

尽管切面允许注入其依赖,但是这里需要注意的是,如果切面依赖仍然走切面的话那么程序就陷入了死循环,很久就会堆溢出。

 

2.4 Matcher

 

Binder绑定一个切面的API是

 

com.google.inject.Binder.bindInterceptor(Matcher<? super Class<?>>, Matcher<? super Method>, MethodInterceptor...)

 

第一个参数是匹配类,第二个参数是匹配方法,第三个数组参数是方法拦截器。也就是说目前为止Guice只能拦截到方法,然后才做一些切面工作。

 

对于Matcher有如下API:

 

com.google.inject.matcher.Matcher.matches(T) 

com.google.inject.matcher.Matcher.and(Matcher<? super T>) 

com.google.inject.matcher.Matcher.or(Matcher<? super T>) 

其中第2、3个方法我没有发现有什么用,好像Guice不适用它们,目前没有整明白。

 

对于第一个方法,如果是匹配Class那么这里T就是一个Class<?>的类型,如果是匹配Method就是一个Method对象。不好理解吧。看一个例子。

 

 1 public class ServiceClassMatcher implements Matcher<Class<?>>{

 2     @Override

 3     public Matcher<Class<?>> and(Matcher<? super Class<?>> other) {

 4         return null;

 5     }

 6     @Override

 7     public boolean matches(Class<?> t) {

 8         return t==ServiceImpl.class;

 9     }

10     @Override

11     public Matcher<Class<?>> or(Matcher<? super Class<?>> other) {

12         return null;

13     }

14 }

 

 

在前面的例子中我们是使用的Matchers.any()对象匹配所有类而通过标注来识别方法,这里可以只匹配ServiceImpl类来控制服务运行流程。

 

事实上Guice里面有一个Matcher的抽象类com.google.inject.matcher.AbstractMatcher<T>,我们只需要覆盖其中的matches方法即可。

 

大多数情况下我们只需要使用Matchers提供的默认类即可。Matchers中有如下API:

 

com.google.inject.matcher.Matchers.any():任意类或者方法 

com.google.inject.matcher.Matchers.not(Matcher<? super T>):不满足此条件的类或者方法 

com.google.inject.matcher.Matchers.annotatedWith(Class<? extends Annotation>):带有此注解的类或者方法 

com.google.inject.matcher.Matchers.annotatedWith(Annotation):带有此注解的类或者方法 

com.google.inject.matcher.Matchers.subclassesOf(Class<?>):匹配此类的子类型(包括本身类型) 

com.google.inject.matcher.Matchers.only(Object):与指定类型相等的类或者方法(这里是指equals方法返回true) 

com.google.inject.matcher.Matchers.identicalTo(Object):与指定类型相同的类或者方法(这里是指同一个对象) 

com.google.inject.matcher.Matchers.inPackage(Package):包相同的类 

com.google.inject.matcher.Matchers.inSubpackage(String):子包中的类(包括此包) 

com.google.inject.matcher.Matchers.returns(Matcher<? super Class<?>>):返回值为指定类型的方法 

通常只需要使用上面的方法或者组合方法就能满足我们的需求。

相关文章
|
3月前
Micronaut AOP与代理机制:实现应用功能增强,无需侵入式编程的秘诀
AOP(面向切面编程)能够帮助我们在不修改现有代码的前提下,为应用程序添加新的功能或行为。Micronaut框架中的AOP模块通过动态代理机制实现了这一目标。AOP将横切关注点(如日志记录、事务管理等)从业务逻辑中分离出来,提高模块化程度。在Micronaut中,带有特定注解的类会在启动时生成代理对象,在运行时拦截方法调用并执行额外逻辑。例如,可以通过创建切面类并在目标类上添加注解来记录方法调用信息,从而在不侵入原有代码的情况下增强应用功能,提高代码的可维护性和可扩展性。
64 1
|
25天前
|
安全 Java 编译器
什么是AOP面向切面编程?怎么简单理解?
本文介绍了面向切面编程(AOP)的基本概念和原理,解释了如何通过分离横切关注点(如日志、事务管理等)来增强代码的模块化和可维护性。AOP的核心概念包括切面、连接点、切入点、通知和织入。文章还提供了一个使用Spring AOP的简单示例,展示了如何定义和应用切面。
67 1
什么是AOP面向切面编程?怎么简单理解?
|
1月前
|
XML Java 开发者
论面向方面的编程技术及其应用(AOP)
【11月更文挑战第2天】随着软件系统的规模和复杂度不断增加,传统的面向过程编程和面向对象编程(OOP)在应对横切关注点(如日志记录、事务管理、安全性检查等)时显得力不从心。面向方面的编程(Aspect-Oriented Programming,简称AOP)作为一种新的编程范式,通过将横切关注点与业务逻辑分离,提高了代码的可维护性、可重用性和可读性。本文首先概述了AOP的基本概念和技术原理,然后结合一个实际项目,详细阐述了在项目实践中使用AOP技术开发的具体步骤,最后分析了使用AOP的原因、开发过程中存在的问题及所使用的技术带来的实际应用效果。
57 5
|
2月前
|
Java 容器
AOP面向切面编程
AOP面向切面编程
43 0
|
3月前
Micronaut AOP与代理机制:实现应用功能增强,无需侵入式编程的秘诀
【9月更文挑战第9天】AOP(面向切面编程)通过分离横切关注点提高模块化程度,如日志记录、事务管理等。Micronaut AOP基于动态代理机制,在应用启动时为带有特定注解的类生成代理对象,实现在运行时拦截方法调用并执行额外逻辑。通过简单示例展示了如何在不修改 `CalculatorService` 类的情况下记录 `add` 方法的参数和结果,仅需添加 `@Loggable` 注解即可。这不仅提高了代码的可维护性和可扩展性,还降低了引入新错误的风险。
48 13
|
4月前
|
XML Java 数据格式
Spring5入门到实战------11、使用XML方式实现AOP切面编程。具体代码+讲解
这篇文章是Spring5框架的AOP切面编程教程,通过XML配置方式,详细讲解了如何创建被增强类和增强类,如何在Spring配置文件中定义切入点和切面,以及如何将增强逻辑应用到具体方法上。文章通过具体的代码示例和测试结果,展示了使用XML配置实现AOP的过程,并强调了虽然注解开发更为便捷,但掌握XML配置也是非常重要的。
Spring5入门到实战------11、使用XML方式实现AOP切面编程。具体代码+讲解
|
4月前
|
Java Spring XML
掌握面向切面编程的秘密武器:Spring AOP 让你的代码优雅转身,横切关注点再也不是难题!
【8月更文挑战第31天】面向切面编程(AOP)通过切面封装横切关注点,如日志记录、事务管理等,使业务逻辑更清晰。Spring AOP提供强大工具,无需在业务代码中硬编码这些功能。本文将深入探讨Spring AOP的概念、工作原理及实际应用,展示如何通过基于注解的配置创建切面,优化代码结构并提高可维护性。通过示例说明如何定义切面类、通知方法及其应用时机,实现方法调用前后的日志记录,展示AOP在分离关注点和添加新功能方面的优势。
59 0
|
5月前
|
Java Spring 容器
Spring问题之Spring AOP是如何实现面向切面编程的
Spring问题之Spring AOP是如何实现面向切面编程的
|
4月前
|
监控 安全 数据库
面向方面编程(AOP)的概念
【8月更文挑战第22天】
80 0
|
4月前
|
XML Java 数据库
Spring5入门到实战------10、操作术语解释--Aspectj注解开发实例。AOP切面编程的实际应用
这篇文章是Spring5框架的实战教程,详细解释了AOP的关键术语,包括连接点、切入点、通知、切面,并展示了如何使用AspectJ注解来开发AOP实例,包括切入点表达式的编写、增强方法的配置、代理对象的创建和优先级设置,以及如何通过注解方式实现完全的AOP配置。