aop 终于提上日程来写一写了。
项目地址:XXXXX;
这种方式看起来很好理解,但是配置起来相当麻烦;小伙伴们可以参考项目来看,这里只贴出比较关键的流程代码。
public interface GoodsService { /** * 查询所有商品信息 * * @param offset 查询起始位置 * @param limit 查询条数 * @return */ List<Goods> queryAll(int offset,int limit); }
@Service @Qualifier("goodsService") public class GoodsServiceImpl implements GoodsService { @Autowired private GoodsDao goodsDao; public List<Goods> queryAll(int offset, int limit) { System.out.println("执行了queryAll方法"); List<Goods> list = new ArrayList<Goods>(); return list; } }
//通知类 LoggerHelper public class LoggerHelper implements MethodBeforeAdvice, AfterReturningAdvice { private static final Logger LOGGER = LoggerFactory.getLogger(LoggerHelper.class); //MethodBeforeAdvice的before方法实现 public void before(Method method, Object[] objects, Object o) throws Throwable { LOGGER.info("before current time:"+System.currentTimeMillis()); } //AfterReturningAdvice的afterReturning方法实现 public void afterReturning(Object o, Method method, Object[] objects, Object o1) throws Throwable { LOGGER.info("afterReturning current time:"+System.currentTimeMillis()); } }
<!-- 定义被代理者 --> <bean id="goodsServiceImpl" class="com.glmapper.framerwork.service.impl.GoodsServiceImpl"></bean> <!-- 定义通知内容,也就是切入点执行前后需要做的事情 --> <bean id="loggerHelper" class="com.glmapper.framerwork.aspect.LoggerHelper"></bean> <!-- 定义切入点位置 --> <bean id="loggerPointcut" class="org.springframework.aop.support.JdkRegexpMethodPointcut"> <property name="pattern" value=".*query.*"></property> </bean> <!-- 使切入点与通知相关联,完成切面配置 --> <!-- 从这里可以帮助我们理解Advisor,advice和pointcut之间的关系--> <!--adivce和pointcut是Advisor的两个属性--> <bean id="loggerHelperAdvisor" class="org.springframework.aop.support.DefaultPointcutAdvisor"> <property name="advice" ref="loggerHelper"></property> <property name="pointcut" ref="loggerPointcut"></property> </bean> <!-- 设置代理 --> <bean id="proxy" class="org.springframework.aop.framework.ProxyFactoryBean"> <!-- 代理的对象 ,也就是目标类--> <property name="target" ref="goodsServiceImpl"></property> <!-- 使用切面 --> <property name="interceptorNames" value="loggerHelperAdvisor"></property> <!-- 代理接口,商品接口 --> <property name="proxyInterfaces" value="com.glmapper.framerwork.service.GoodsService"></property> </bean>
@Controller @RequestMapping("/buy") public class BuyController { @Autowired private OrderService orderService; //因为我们已经在配置文件中配置了proxy, //所以这里可以直接注入拿到我们的代理类 @Autowired private GoodsService proxy; @RequestMapping("/initPage") public ModelAndView initPage(HttpServletRequest request, HttpServletResponse response, ModelAndView view) { //这里使用proxy执行了*query*, List<Goods> goods = proxy.queryAll(10,10); view.addObject("goodsList", goods); view.setViewName("goodslist"); return view; } }
这个方式是通过一个SpringContextUtil工具类来获取代理对象的。
@RequestMapping("/initPage") public ModelAndView initPage(HttpServletRequest request, HttpServletResponse response, ModelAndView view) { //这里通过工具类来拿,效果一样的。 GoodsService proxy= (GoodsService) SpringContextUtil.getBean("proxy"); List<Goods> goods = proxy.queryAll(10,10); view.addObject("goodsList", goods); view.setViewName("goodslist"); return view; }
这个还是有点坑的,首先SpringContextUtil是继承ApplicationContextAware这个接口,我们希望能够SpringContextUtil可以被Spring容器直接管理,所以,需要使用 @Component 标注。标注了之后最关键的是它得能够被我们配置的注入扫描扫到(亲自踩的坑,我把它放在一个扫不到的包下面,一直debug都是null;差点砸电脑...)
@Component public class SpringContextUtil implements ApplicationContextAware { // Spring应用上下文环境 private static ApplicationContext applicationContext; /** * 实现ApplicationContextAware接口的回调方法,设置上下文环境 * * @param applicationContext */ public void setApplicationContext(ApplicationContext applicationContext) { SpringContextUtil.applicationContext = applicationContext; } /** * @return ApplicationContext */ public static ApplicationContext getApplicationContext() { return applicationContext; } /** * 获取对象 * 这里重写了bean方法,起主要作用 * @param name * @return Object 一个以所给名字注册的bean的实例 * @throws BeansException */ public static Object getBean(String name) throws BeansException { return applicationContext.getBean(name); } }
21:04:47.940 [http-nio-8080-exec-7] INFO c.g.framerwork.aspect.LoggerHelper - before current time:1529413487940 执行了queryAll方法 21:04:47.940 [http-nio-8080-exec-7] INFO c.g.framerwork.aspect.LoggerHelper - afterReturning current time:1529413487940
上面就是最最经典的方式,就是通过代理的方式来实现AOP的过程。
注意这里和LoggerHelper的区别,这里的LoggerAspect并没有继承任何接口或者抽象类。
/** * @description: [描述文本] * @email: <a href="guolei.sgl@antfin.com"></a> * @author: guolei.sgl * @date: 18/6/20 */ public class LoggerAspect { private static final Logger LOGGER = LoggerFactory.getLogger(LoggerHelper.class); public void before(){ LOGGER.info("before current time:"+System.currentTimeMillis()); } public void afterReturning() { LOGGER.info("afterReturning current time:"+System.currentTimeMillis()); } }
<!-- 定义通知内容,也就是切入点执行前后需要做的事情 --> <bean id="loggerAspect" class="com.glmapper.framerwork.aspect.LoggerAspect"> </bean> <aop:config> <!--定义切面--> <aop:aspect ref="loggerAspect"> <aop:pointcut id="loggerPointCut" expression= "execution(* com.glmapper.framerwork.service.impl.*.*(..)) " /> <!-- 定义 Advice --> <!-- 前置通知 --> <aop:before pointcut-ref="loggerPointCut" method="before" /> <!-- 后置通知 --> <aop:after-returning pointcut-ref="loggerPointCut" method="afterReturning"/> </aop:aspect> </aop:config>
注意这里LoggerAspect中的before和afterReturning如果有参数,这里需要处理下,否则会报 0 formal unbound in pointcut 异常。
/** * @description: 使用Aspect注解驱动的方式 * @email: <a href="guolei.sgl@antfin.com"></a> * @author: guolei.sgl * @date: 18/6/20 */ @Aspect public class LoggerAspectInject { private static final Logger LOGGER = LoggerFactory.getLogger(LoggerAspectInject.class); @Pointcut("execution(* com.glmapper.framerwork.service.impl.*.*(..))") public void cutIn(){} @Before("cutIn()") public void before(){ LOGGER.info("before current time:"+System.currentTimeMillis()); } @AfterReturning("cutIn()") public void AfterReturning(){ LOGGER.info("afterReturning current time:"+System.currentTimeMillis()); } }
<aop:aspectj-autoproxy /> <!-- 定义通知内容,也就是切入点执行前后需要做的事情 --> <bean id="sleepHelper" class="com.glmapper.framerwork.aspect.LoggerAspectInject"> </bean> <!-- 定义被代理者 --> <bean id="goodsServiceImpl" class="com.glmapper.framerwork.service.impl.GoodsServiceImpl"> </bean>
客户端使用:
@Controller @RequestMapping("/buy") public class BuyController { @Autowired private OrderService orderService; @RequestMapping("/initPage") public ModelAndView initPage(HttpServletRequest request, HttpServletResponse response, ModelAndView view) { //通过SpringContextUtil手动获取 代理bean GoodsService goodsService=(GoodsService) SpringContextUtil.getBean("goodsServiceImpl"); List<Goods> goods = goodsService.queryAll(10,10); view.addObject("goodsList", goods); view.setViewName("goodslist"); return view; } }
@Aspect @Component //这里加上了Component注解,就不需要在xml中配置了 public class LoggerAspectInject { private static final Logger LOGGER = LoggerFactory.getLogger(LoggerAspectInject.class); @Pointcut("execution(* com.glmapper.framerwork.service.impl.*.*(..))") public void cutIn(){} @Before("cutIn()") public void before(){ LOGGER.info("before current time:"+System.currentTimeMillis()); } @AfterReturning("cutIn()") public void AfterReturning(){ LOGGER.info("afterReturning current time:"+System.currentTimeMillis()); } }
客户端代码:
@Controller @RequestMapping("/buy") public class BuyController { @Autowired private OrderService orderService; //直接注入 @Autowired private GoodsService goodsService; @RequestMapping("/initPage") public ModelAndView initPage(HttpServletRequest request, HttpServletResponse response, ModelAndView view) { List<Goods> goods = goodsService.queryAll(10,10); view.addObject("goodsList", goods); view.setViewName("goodslist"); return view; } }
/** * @description: aop * @email: <a href="henugl@1992.163.com"></a> * @author: glmapper@磊叔 * @date: 18/6/4 */ @Aspect @Component public class LoggerAspectInject { private static final Logger LOGGER= LoggerFactory.getLogger(LoggerAspectInject.class); @Pointcut("execution(* com.glmapper.book.web.controller.*.*(..))") public void cutIn(){ } @Around("cutIn()") // 定义Pointcut,名称即下面的标识"aroundAdvice public Object aroundAdvice(ProceedingJoinPoint poin){ System.out.println("环绕通知"); Object object = null; try{ object = poin.proceed(); }catch (Throwable e){ e.printStackTrace(); } return object; } // 定义 advise //这个方法只是一个标识,相当于在配置文件中定义了pointcut的id,此方法没有返回值和参数 @Before("cutIn()") public void beforeAdvice(){ System.out.println("前置通知"); } @After("cutIn()") public void afterAdvice(){ System.out.println("后置通知"); } @AfterReturning("cutIn()") public void afterReturning(){ System.out.println("后置返回 "); } @AfterThrowing("cutIn()") public void afterThrowing(){ System.out.println("后置异常"); } }
关于命名切入点:上面的例子中cutIn方法可以被称之为命名切入点,命名切入点可以被其他切入点引用,而匿名切入点是不可以的。只有@AspectJ支持命名切入点,而Schema风格不支持命名切入点。 如下所示,@AspectJ使用如下方式引用命名切入点:
@Pointcut("execution(* com.glmapper.book.web.controller.*.*(..))") public void cutIn(){ } //引入命名切入点 @Before("cutIn()") public void beforeAdvice(){ System.out.println("前置通知"); }
这种方式我感觉是第二种和第三种的结合的一种方式。
/** * @description: 注入式 也是一种通过XML方式配置的方式 * @email: <a href="guolei.sgl@antfin.com"></a> * @author: guolei.sgl * @date: 18/6/20 */ public class LoggerAspectHelper { private static final Logger LOGGER = LoggerFactory.getLogger(LoggerAspectHelper.class); /** * 调动方法前执行 * @param point * @throws Throwable */ public void doBefore(JoinPoint point) throws Throwable { LOGGER.info("before current time:"+System.currentTimeMillis()); } /** * 在调用方法前后执行 * @param point * @return * @throws Throwable */ public Object doAround(ProceedingJoinPoint point) throws Throwable { LOGGER.info("around current time:"+System.currentTimeMillis()); if(point.getArgs().length>0) { return point.proceed(point.getArgs()); }else{ return point.proceed(); } } /** * 在调用方法之后执行 * @param point * @throws Throwable */ public void doAfter(JoinPoint point) throws Throwable { LOGGER.info("after current time:"+System.currentTimeMillis()); } /** * 异常通知 * @param point * @param ex */ public void doThrowing(JoinPoint point, Throwable ex) { LOGGER.info("throwing current time:"+System.currentTimeMillis()); } }
<bean id="loggerAspectHelper" class="com.glmapper.framerwork.aspect.LoggerAspectHelper"> </bean> <aop:config> <aop:aspect id="configAspect" ref="loggerAspectHelper"> <!--配置com.glmapper.framerwork.service.imp 包下所有类或接口的所有方法 --> <aop:pointcut id="cutIn" expression= "execution(* com.glmapper.framerwork.service.impl.*.*(..))" /> <aop:before pointcut-ref="cutIn" method="doBefore" /> <aop:after pointcut-ref="cutIn" method="doAfter" /> <aop:around pointcut-ref="cutIn" method="doAround" /> <aop:after-throwing pointcut-ref="cutIn" method="doThrowing" throwing="ex" /> </aop:aspect> </aop:config>
23:39:48.756 [http-nio-8080-exec-4] INFO c.g.f.aspect.LoggerAspectHelper - before current time:1529509188756 23:39:48.757 [http-nio-8080-exec-4] INFO c.g.f.aspect.LoggerAspectHelper - around current time:1529509188757 excute queryAll method... 23:39:48.757 [http-nio-8080-exec-4] INFO c.g.f.aspect.LoggerAspectHelper - after current time:1529509188757
用于匹配方法执行的连接点;
execution(* com.glmapper.book.web.controller.*.*(..))
用于匹配指定类型内的方法执行;
//如果在com.glmapper.book.web.controller包或其下的任何子包中 //定义了该类型,则在Web层中有一个连接点。 within(com.glmapper.book.web.controller..*) @Pointcut("within(com.glmapper.book.web.controller..*)") public void cutIn(){}
@within:用于匹配所以持有指定注解类型内的方法;
/** * @description: 注解定义 * @email: <a href="henugl@1992.163.com"></a> * @author: glmapper@磊叔 * @date: 18/6/4 */ @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.METHOD,ElementType.FIELD}) public @interface AuthAnnotation { }
任何目标对象对应的类型持有AuthAnnotation注解的类方法;必须是在目标对象上声明这个注解,在接口上声明的对它不起作用。
@within(com.glmapper.book.common.annotaion.AuthAnnotation) //所有被@AdviceAnnotation标注的类都将匹配 @Pointcut("@within(com.glmapper.book.common.annotaion.AuthAnnotation)") public void cutIn(){}
用于匹配当前AOP代理对象类型的执行方法;注意是AOP代理对象的类型匹配,这样就可能包括引入接口也类型匹配;this中使用的表达式必须是类型全限定名,不支持通配符;
//当前目标对象(非AOP对象)实现了 UserService 接口的任何方法 this(com.glmapper.book.web.service.UserService) //用于向通知方法中传入代理对象的引用。 @Before("cutIn() && this(proxy)") public void beforeAdvice(ProceedingJoinPoint poin,Object proxy){ System.out.println("前置通知"); }
用于匹配当前目标对象类型的执行方法;注意是目标对象的类型匹配,这样就不包括引入接口也类型匹配;target中使用的表达式必须是类型全限定名,不支持通配符;
//当前目标对象(非AOP对象)实现了 UserService 接口的任何方法 target(com.glmapper.book.web.service.UserService) //用于向通知方法中传入代理对象的引用。 @Before("cutIn() && target(proxy)") public void beforeAdvice(ProceedingJoinPoint poin,Object proxy){ System.out.println("前置通知"); }
@target:用于匹配当前目标对象类型的执行方法,其中目标对象持有指定的注解;任何目标对象持有Secure注解的类方法;这个和@within一样必须是在目标对象上声明这个注解,在接口上声明的对它同样不起作用。
@target(com.glmapper.book.common.annotaion.AuthAnnotation) @Pointcut("@target(com.glmapper.book.common.annotaion.AuthAnnotation)") public void cutIn(){}
用于匹配当前执行的方法传入的参数为指定类型的执行方法;参数类型列表中的参数必须是类型全限定名,通配符不支持;args属于动态切入点,这种切入点开销非常大,非特殊情况最好不要使用;
//任何一个以接受“传入参数类型为java.io.Serializable”开头, //且其后可跟任意个任意类型的参数的方法执行, //args指定的参数类型是在运行时动态匹配的 args (java.io.Serializable,..) //用于将参数传入到通知方法中。 @Before("cutIn() && args(age,username)") public void beforeAdvide(JoinPoint point, int age, String username){ //... }
@args:用于匹配当前执行的方法传入的参数持有指定注解的执行;任何一个只接受一个参数的方法,且方法运行时传入的参数持有注解AuthAnnotation;动态切入点,类似于arg指示符;
@args (com.glmapper.book.common.annotaion.AuthAnnotation) @Before("@args(com.glmapper.book.common.annotaion.AuthAnnotation)") public void beforeAdvide(JoinPoint point){ //... }
使用“@annotation(注解类型)”匹配当前执行方法持有指定注解的方法;注解类型也必须是全限定类型名;
//当前执行方法上持有注解 AuthAnnotation将被匹配 @annotation(com.glmapper.book.common.annotaion.AuthAnnotation) //匹配连接点被它参数指定的AuthAnnotation注解的方法。 //也就是说,所有被指定注解标注的方法都将匹配。 @Pointcut("@annotation(com.glmapper.book.common.annotaion.AuthAnnotation)") public void cutIn(){}
还有一种是bean的方式,没用过。有兴趣可以看看。
例子在下面说到的基础概念部分对应给出。
基础概念部分主要将 AOP 中的一些概念点捋一捋,这部分主要参考了官网上的一些解释。
AOP(Aspect-Oriented Programming)
, 即 面向切面编程 , 它与 OOP
( Object-Oriented Programming
, 面向对象编程) 相辅相成, 提供了与 OOP
不同的抽象软件结构的视角。在 OOP
中,我们以类(class)作为我们的基本单元, 而 AOP
中的基本单元是 Aspect(切面) 。
横切关注点( Cross Cutting Concern
):独立服务,如系统日志。如果不是独立服务(就是与业务耦合比较强的服务)就不能横切了。通常这种独立服务需要遍布系统各个角落,遍布在业务流程之中。
目标对象。织入 advice 的目标对象。 目标对象也被称为 advised object
。 因为 Spring AOP 使用运行时代理的方式来实现 aspect, 因此 adviced object 总是一个代理对象(proxied object);注意, adviced object 指的不是原来的类, 而是织入 advice 后所产生的代理类。
即 Advice
应用在 JoinPoint
的过程,这个过程叫织入。从另外一个角度老说就是将 aspect
和其他对象连接起来, 并创建 adviced object
的过程。
根据不同的实现技术, AOP
织入有三种方式:
Java Advice
Spring 采用动态代理织入, 而AspectJ采用编译器织入和类装载期
Spring AOP默认使用代理的是标准的JDK动态代理。这使得任何接口(或一组接口)都可以代理。
Spring AOP也可以使用CGLIB代理。如果业务对象不实现接口,则默认使用CGLIB。对接口编程而不是对类编程是一种很好的做法;业务类通常会实现一个或多个业务接口。在一些特殊的情况下,即需要通知的接口上没有声明的方法,或者需要将代理对象传递给具体类型的方法,有可能强制使用CGLIB。
我们知道Java语言本身并非是动态的,就是我们的类一旦编译完成,就很难再为他添加新的功能。但是在一开始给出的例子中,虽然我们没有向对象中添加新的方法,但是已经向其中添加了新的功能。这种属于向现有的方法添加新的功能,那能不能为一个对象添加新的方法呢?答案肯定是可以的,使用introduction就能够实现。
introduction:动态为某个类增加或减少方法。为一个类型添加额外的方法或字段。Spring AOP 允许我们为 目标对象
引入新的接口(和对应的实现)。
切面:通知和切入点的结合。
切面实现了cross-cutting(横切)功能。最常见的是logging模块、方法执行耗时模块,这样,程序按功能被分为好几层,如果按传统的继承的话,商业模型继承日志模块的话需要插入修改的地方太多,而通过创建一个切面就可以使用AOP来实现相同的功能了,我们可以针对不同的需求做出不同的切面。
而将散落于各个业务对象之中的Cross-cutting concerns 收集起来,设计各个独立可重用的对象,这些对象称之为Aspect;在上面的例子中我们根据不同的配置方式,定义了四种不同形式的切面。
Aspect 在应用程序执行时加入业务流程的点或时机称之为 Joinpoint ,具体来说,就是 Advice 在应用程序中被呼叫执行的时机,这个时机可能是某个方法被呼叫之前或之后(或两者都有),或是某个异常发生的时候。
环绕通知 = 前置+目标方法执行+后置通知,proceed方法就是用于启动目标方法执行的。
环绕通知 ProceedingJoinPoint 执行 proceed 方法 的作用是让目标方法执行 ,这 也是环绕通知和前置、后置通知方法的一个最大区别。
Proceedingjoinpoint 继承了 JoinPoint 。是在JoinPoint的基础上暴露出 proceed 这个方法。proceed很重要,这个是aop代理链执行的方法;暴露出这个方法,就能支持 aop:around 这种切面(其他的几种切面只需要用到JoinPoint,这跟切面类型有关), 能决定是否走代理链还是走自己拦截的其他逻辑。
在环绕通知的方法中是需要返回一个Object类型对象的,如果把环绕通知的方法返回类型是void,将会导致一些无法预估的情况,比如:404。
匹配 join points
的谓词。 Advice
与切入点表达式相关联, 并在切入点匹配的任何连接点上运行。(例如,具有特定名称的方法的执行)。由切入点表达式匹配的连接点的概念是 AOP
的核心, Spring
默认使用 AspectJ
切入点表达式语言。
在 Spring
中, 所有的方法都可以认为是 Joinpoint
, 但是我们并不希望在所有的方法上都添加 Advice
, 而 Pointcut
的作用就是提供一组规则(使用 AspectJ pointcut expression language
来描述) 来匹配 Joinpoint
, 给满足规则的 Joinpoint
添加 Advice
。
在 Spring AOP
中, 所有的方法执行都是 join point
。 而 point cut
是一个描述信息,它修饰的是 join point
, 通过 point cut
,我们就可以确定哪些 join point
可以被织入 Advice
。 因此 join point
和 point cut
本质上就是两个不同维度上的东西。
advice
是在 join point
上执行的, 而 point cut
规定了哪些 join point
可以执行哪些 advice
。
Advice 是我们切面功能的实现,它是切点的真正执行的地方。比如像前面例子中打印时间的几个方法(被@Before等注解标注的方法都是一个通知);Advice 在 Jointpoint 处插入代码到应用程序中。
BeforeAdvice,AfterAdvice,区别在于Advice在目标方法之前调用还是之后调用,Throw Advice 表示当目标发生异常时调用Advice。
下面这张图是在网上一位大佬的博客里发现的,可以帮助我们更好的理解这些概念之间的关系。
在调试程序过程中出现的一些问题记录
@Around("cutIn()") public void aroundAdvice(ProceedingJoinPoint poin) { System.out.println("环绕通知"); }
这里需要注意的是再使用环绕通知时,需要给方法一个返回值。
@Around("cutIn()") public Object aroundAdvice(ProceedingJoinPoint poin) throws Throwable { System.out.println("环绕通知"); return poin.proceed(); }
在spring 4.x中 提供了aop注解方式 带参数的方式。看下面例子:
@Pointcut(value = "execution(* com.glmapper.framerwork.service.impl.*(int,int)) && args(i,j)") public void cutIn(int i, int j) {} @Before(value="cutIn(i, j)",argNames = "i,j") public void beforeMethod( int i, int j) { System.out.println("---------begins with " + i + "-" +j); }
比如说这里,Before中有两个int类型的参数,如果此时我们在使用时没有给其指定参数,那么就会抛出: Caused by: java.lang.IllegalArgumentException: error at ::0 formal unbound in pointcut 异常信息。
本来是想放在一篇里面的,但是实在太长了,就分开吧;周末更新下