Spring-面向切面编程(AOP)
1. 切面的概念
横切关注点,可以被模块化的特殊类.
白话:定义了我要在程序执行时,在指定地方执行我编写的代码.
2. AOP术语
- 通知(Advice): 切面的工作. 就是"什么时候做什么"
- 五种类型: 前置通知(Before) , 后置通知(After),返回通知(After-returning),异常通知(After-throwing),环绕通知(Around)
- 连接点(Join Point): 执行程序的时候所有可以插入切面的点
- 切点(Point Cut): 就是 “什么地方”,用来规定通知执行的范围
- 切面(Aspect): 就是通知+切点的结合.共通定义了切面的全部内容
- 引入(Introduction):引入允许我们想现有的类添加新方法和属性,
- 织入(Weaving): 把切面应用到目标对象并创建新的代理对象的过程. 切面在指定的连接点被织入到目标对象.
3. 白话理解
定义一个切面.定义了我要在某个地方执行时,执行我编写的切面的代码.
所以我需要定义一个AOP类:
1. 我需要它声明为切面类(用@Aspect注解放在类上实现)
2. 我需要指定切面做什么类型的事情和做什么事情.(五种通知和业务代码)
3. 我需要定义一个切点,用来指定我在什么地方执行切面类的内容(切点)
4. 我需要在配置我的工程,让切面可以被认识.(@Component
和<aop:aspectj-autoproxy/>
)
4. 简单的代码实现
-
需要的依赖
<!-- spring-aspects中依赖的aspectjweaver是aspectj的织入包 --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-aspects</artifactId> </dependency> <!-- 在我测试的时候,以下依赖不是必要的 --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-aop</artifactId> </dependency> <!-- aspectjrt包是aspectj的runtime包 --> <dependency> <groupId>org.aspectj</groupId> <artifactId>aspectjrt</artifactId> </dependency> <dependency> <groupId>aopalliance</groupId> <artifactId>aopalliance</artifactId> <version>1.0</version> </dependency>
-
切面类
@Aspect //声明切面 @Component //加入容器 public class LogAspect { protected static final Logger logger = LoggerFactory.getLogger(LogAspect.class); //定义一个切点(写法一) @Pointcut("execution(public * com.min.spring.service..*(..))") public void pt1(){} //通知中指定上面定义的切点 @Before("pt1()") public void start(JoinPoint joinPoint) { logger.info("service start====" + joinPoint.getSignature().getName()); } @After("pt1()") public void after(JoinPoint joinPoint) { logger.info("service after====" + joinPoint.getSignature().getName()); } //直接在通知中写明起点,不需要专门定义切点(写法二) @Before("execution(public * com.min.spring.controller..*(..))") public void cstart(JoinPoint joinPoint) { logger.info("controller start====" + joinPoint.getSignature().getName()); } @After("execution(public * com.min.spring.controller..*(..))") public void cafter(JoinPoint joinPoint) { logger.info("controller after====" + joinPoint.getSignature().getName()); } }
-
配置xml (spring-context.xml 和spring-mvc.xml中都要加上启用aspectJ自动代理的标记)
<aop:aspectj-autoproxy/>
-
执行结果
2019-08-14 17:45:06,345 8602 [nio-8080-exec-7] INFO om.min.spring.aspect.LogAspect - controller start====login 2019-08-14 17:45:06,346 8603 [nio-8080-exec-7] INFO om.min.spring.aspect.LogAspect - service start====login 2019-08-14 17:45:06,346 8603 [nio-8080-exec-7] INFO om.min.spring.aspect.LogAspect - service start====findByUsername 2019-08-14 17:45:06,517 8774 [nio-8080-exec-7] INFO om.min.spring.aspect.LogAspect - service after====findByUsername 2019-08-14 17:45:06,518 8775 [nio-8080-exec-7] INFO om.min.spring.aspect.LogAspect - service after====login 2019-08-14 17:45:06,518 8775 [nio-8080-exec-7] INFO om.min.spring.aspect.LogAspect - controller after====login 2019-08-14 17:45:06,540 8797 [nio-8080-exec-8] INFO om.min.spring.aspect.LogAspect - controller start====index 2019-08-14 17:45:06,548 8805 [nio-8080-exec-8] INFO om.min.spring.aspect.LogAspect - controller after====index
5. 切点(PointCut)的常见写法
//任意公共方法的执行:
execution(public * *(..))
//任何一个名字以 set 开始的方法的执行:
execution(* set*(..))
//AccountService 接口定义的任意方法的执行:
execution(* com.xyz.service.AccountService.*(..))
//在 service 包中定义的任意方法的执行:
execution(* com.xyz.service.*.*(..))
//在 service 包或其子包中定义的任意方法的执行:
execution(* com.xyz.service..*.*(..))
//在 service 包中的任意连接点(在 Spring AOP 中只是方法执行):
within(com.xyz.service.*)
//在 service 包或其子包中的任意连接点(在 Spring AOP 中只是方法执行):
within(com.xyz.service..*)
//实现 AccountService 接口的代理对象的任意连接点 (在 Spring AOP 中只是方法执行):
this(com.xyz.service.AccountService)
//实现 AccountService 接口的目标对象的任意连接点 (在 Spring AOP 中只是方法执行):
target(com.xyz.service.AccountService)
//任何一个只接受一个参数,并且运行时所传入的参数是 Serializable 接口的连接点(在 Spring AOP 中只是方法执行):
args(java.io.Serializable)
//请注意在例子中给出的切入点不同于execution(* *(Java.io.Serializable)),args 版本只有在动态运行时候传入参数是 Serializable 时才匹配,而 execution 版本在方法签名中声明只有一个 Serializable 类型的参数时候匹配。
//目标对象中有一个 @Transactional 注解的任意连接点 (在 Spring AOP 中只是方法执行):
@target(org.springframework.transaction.annotation.Transactional)
//任何一个目标对象声明的类型有一个 @Transactional 注解的连接点 (在 Spring AOP 中只是方法执行):
@within(org.springframework.transaction.annotation.Transactional)
任何一个执行的方法有一个 @Transactional 注解的连接点 (在 Spring AOP 中只是方法执行):
@annotation(org.springframework.transaction.annotation.Transactional)
//任何一个只接受一个参数,并且运行时所传入的参数类型具有 @Classified 注解的连接点(在 Spring AOP 中只是方法执行):
@args(com.xyz.security.Classified)
任何一个在名为 tradeService 的 Spring bean 之上的连接点 (在 Spring AOP 中只是方法执行):
bean(tradeService)
//任何一个在名字匹配通配符表达式*Service的 Spring bean 之上的连接点 (在 Spring AOP 中只是方法执行):
bean(*Service)
//其中,this、tagart、args、 @target、 @with、 @annotation和@args在绑定表单中更加常用
常见问题
1. SpringMVC容器和Spring容器父子关系导致Aspect不起作用
描述:
- Aspect中定义的切点包括了Controller层和Service层
- 有父子容器存在SpringMVC容器和Spring容器
- 定义的Aspect类的对象放在了父容器
- 启用aspectJ自动代理只放在了spring-context.xml 或spring-mvc.xml中
- 切面执行的时候只有部分有效果或者没效果
解决方案一:
spring-context.xml 和spring-mvc.xml中都要加上启用aspectJ自动代理的标记
<!--启用aspectJ自动代理-->
<aop:aspectj-autoproxy/>
解决方案二:
spring-context.xml 和spring-mvc.xml中不要分包扫描,
<context:component-scan base-package="com.min.spring">
</context:component-scan>
原因:
1.SpringMVC容器和Spring容器的影响
在一般的项目配置web.xml中,Spring容器是由ContextLoaderListener
生成,而SpringMVC容器是由DispatcherServlet
来加载, Spring容器是父容器, SpringMVC容器是子容器.
子容器(SpringMVC容器
)可以访问父容器(Spring容器
)的内容,反之则不行
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:spring-context*.xml</param-value>
</context-param>
<listener>
<listener-class>
org.springframework.web.context.ContextLoaderListener
</listener-class>
</listener>
<!--配置 Spring 的 Servlet 分发器处理所有 HTTP 的请求和响应-->
<servlet>
<servlet-name>springServlet</servlet-name>
<servlet-class>
org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath*:/spring-mvc*.xml</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
在扫描包的时候父容器(Spring容器
)负责扫描@Controller
以外的注解,
子容器(SpringMVC容器
)负责只扫描@Controller
注解.
spring-context.xml
<context:component-scan base-package="com.min.spring">
<context:exclude-filter type="annotation"
expression="org.springframework.stereotype.Controller"/>
</context:component-scan>
spring-mvc.xml
<!-- 使用 Annotation 自动注册 Bean,只扫描 @Controller use-default-filters必需设置为false-->
<context:component-scan base-package="com.min.spring"
use-default-filters="false">
<context:include-filter type="annotation"
expression="org.springframework.stereotype.Controller"/>
</context:component-scan>
如果你写的切面既有Controller的,又有Service层的时候, 在父容器和子容器中都需要加上启用aspectJ自动代理
如果在只在一个xml中的时候,产生的自动代理只是对应容器的的部分.
所以要在2个容器的xml中都配置启动aspectJ自动代理.
spring-context.xml 和spring-mvc.xml
<!--启用aspectJ自动代理-->
<aop:aspectj-autoproxy/>
如果在只在一个xml中的时候,产生的自动代理只是对应容器的的部分.