Spring AOP基础
AOP
1.什么是AOP
AOP(Aspect Oriented Programming)面向切面的编程,AOP是基于OOP,并建立在OOP之上的编程思想,OOP主要面对的是对象,而AOP是面向对象的切面,在处理日志,安全管理,事务管理等方面有重要作用.
AOP的基础概念就是在不修改原有代码的情况下,增强和主要业务无关的公共功能到原本已经完成的指定代码位置.
AOP的底层设计模式是使用的代理模式,通过代理去处理原本的业务,并在原本业务的前后增加一些额外的功能;
代理模式有两种实现:
1. 静态代理
需要为每一个被代理类创建一个代理类,在代理类中开发对被代理类的额外功能处理.
2. 动态代理
不去创建确定的代理类,在java通过反射等方式动态创建代理类,进行方法的代理
通过jdk提供proxy实现一个简单动态代理
使用环境类
package com.learn.bean;
import org.springframework.stereotype.Component;
/**
* @author wangxing
* @version 2020/6/28 15:05 Administrator
*/
@Component
public class User {
private Long id;
private String userName;
private String password;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getUserName() {
return userName;
}
public void setUserName(String userName) {
this.userName = userName;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
}
package com.learn.dao;
import com.learn.bean.User;
/**
* @author wangxing
* @version 2020/6/28 15:06 Administrator
*/
public interface IUserDao {
User select(Long id);
void add(User user);
void delete(Long id);
void update(Long id,User user);
}
package com.learn.dao.impl;
import com.learn.bean.User;
import com.learn.dao.IUserDao;
import org.springframework.stereotype.Repository;
/**
* @author wangxing
* @version 2020/6/28 15:06 Administrator
*/
@Repository
public class UserDaoImpl implements IUserDao {
public User select(Long id) {
if (id == 0) {
throw new RuntimeException("select exception: id is 0");
}
System.out.println("查询user");
User user = new User();
user.setId(id);
user.setPassword(String.valueOf(System.currentTimeMillis()).substring(8));
user.setUserName(String.valueOf(System.currentTimeMillis()).substring(5));
return user;
}
public void add(User user) {
if (user == null) {
throw new RuntimeException("add exception: user is null");
}
System.out.println("创建 user");
}
public void delete(Long id) {
if (id == 0) {
throw new RuntimeException(" delete exception: id is 0");
}
System.out.println("删除user");
}
public void update(Long id, User user) {
if (id == 0) {
throw new RuntimeException("update exception: id is 0");
}
System.out.println("更新use");
}
}
自定义动态代理类
package com.learn.proxy;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
/**
* 根据jdk提供的proxy自定义动态代理,仅供实现接口的类使用
*
* @author wangxing
* @version 2020/6/29 9:17 Administrator
*/
public class MyProxy {
public static Object getProxy(final Object t) {
//类加载器:被代理类的接口的类加载
ClassLoader loader = t.getClass().getClassLoader();
//类型:被代理类的接口的类型
Class<?>[] interfaces = t.getClass().getInterfaces();
//委托执行的处理类:在类内部的invoke方法中进行对方法的加强
InvocationHandler h = new InvocationHandler() {
@Override
/**
* 执行目标对象的方法
* @param proxy 代理对象,用于给jdk使用,无需我们自己操作
* @param method 当前将要执行的目标对象的方法
* @param args 当前方法外部调用使用的参数
* @return
* @throws Throwable
*/
public Object invoke(Object proxy,Method method, Object[] args) throws Throwable {
Object result = null;
try {
System.out.println("执行前执行");
result = method.invoke(t, args);
System.out.println("执行后执行");
} catch (Exception e) {
System.out.println("执行异常退出时执行");
} finally {
System.out.println("执行退出时执行");
}
return result;
}
};
return Proxy.newProxyInstance(loader, interfaces, h);
}
}
测试类
package com.learn.proxy;
import com.learn.dao.IUserDao;
import com.learn.dao.impl.UserDaoImpl;
import org.junit.Test;
/**
* @author wangxing
* @version 2020/6/29 9:33 Administrator
*/
public class MyProxyTest {
@Test
public void getProxy() {
IUserDao userDao = new UserDaoImpl();
System.out.println("直接调用方法");
userDao.select(1L);
System.out.println(userDao.getClass());
System.out.println("--------分割线--------");
userDao = (IUserDao) MyProxy.getProxy(userDao);
System.out.println("使用代理调用方法");
System.out.println(userDao.getClass());
userDao.select(1L);
}
}
通过输出结果可以看到userDao的类型从UserDaoImpl变为了 class com.sun.proxy.$Proxy24
证明新的对象是通过动态代理生成的实例而不是原本的实现类
SpringAOP有两种动态实现方式:
- 使用jdk提供的动态代理Proxy:使用JDK提供的动态代理需要类本身要进行代理的类实现了某个接口,在动态代理过程中对这个接口的暴露方法进行代理,在方法的前后提供额外的操作.
- 使用CGLIB动态代理:不需要实现接口,直接使用类就可以进行代理,但是效率方面低于jdk提供的动态代理,实现功能上是一致的
这两种实现在我们使用时是体验不到区别的,spring会根据情况自行选择
进行SpringAOP实现
基础配置引入spring aop所需依赖
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.2.6.RELEASE</version>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.9.5</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
<version>5.2.6.RELEASE</version>
</dependency>
1.通过配置文件方式进行AOP操作
1.配置spring.xml文件进行aop设置
<bean class="com.learn.dao.impl.UserDaoImpl" id="userDao" ></bean>
<bean class="com.learn.aspect.LogXmlAspect" id="logXmlAspect"></bean>
<aop:config>
<!-- 配置aop匹配方式
execution模式细粒度配置匹配方式 配置切入点,将AOP切入到什么类,什么方法中
第一段 切点的标识符(public,private等) 可以不写,不写为全部标识符号
<aop:pointcut id="globalPoint" expression="execution(* com.learn.dao.*.*(..))"/>
<aop:pointcut id="globalPointPublic" expression="execution(public * com.learn.dao.*.*(..))"/>
第二段 * 表示返回类型,*表示全部返回类型,可以指定特定的返回类型,系统类和void直接写入类名即可,自定义类需要写入全路径名
<aop:pointcut id="myPoint" expression="execution(* com.learn.service..*.*(..))"/>
<aop:pointcut id="myPointUser" expression="execution(com.learn.bean.User com.learn.service..*.*(..))"/>
<aop:pointcut id="myPointVoid" expression="execution(void com.learn.service..*.*(..))"/>
第三段 要进行匹配的的类名包名,写到类名就是匹配到当前类,写到包名后一个.表示匹配当前包之下的类(com.learn.dao.)写..表示当前包和当前包的子孙包都进行匹配当前规则(com.learn.service..)
<aop:pointcut id="myPoint" expression="execution(* com.learn.service.*.*(..))"/>
<aop:pointcut id="myPointAffiliatePacket" expression="execution(* com.learn.service..*.*(..))"/>
<aop:pointcut id="myPointUserService" expression="execution(* com.learn.service.IUserService.*(..))"/>
第四段 匹配的方法名,写*表示匹配全部方法名称,可以指定具体方法名
<aop:pointcut id="globalPointUserDao" expression="execution(* com.learn.dao.IUserDao.*(..))"/>
<aop:pointcut id="globalPointUserDaoSelect" expression="execution(public * com.learn.dao.IUserDao.select(..))"/>
第五段 匹配的方法参数 可写入确认内容根据参数去进行匹配,也可以使用..表示匹配所有的参数情况
<aop:pointcut id="globalPointUserDaoSelect" expression="execution(public * com.learn.dao.IUserDao.select(Long))"/>
<aop:pointcut id="globalPointUserDaoSelect" expression="execution(public * com.learn.dao.IUserDao.select(..))"/>
-->
<aop:pointcut id="globalPoint" expression="execution(* com.learn.dao.*.*(..))"/>
<aop:pointcut id="globalPointPublic" expression="execution(public * com.learn.dao.*.*(..))"/>
<aop:pointcut id="globalPointUserDao" expression="execution(* com.learn.dao.IUserDao.*(..))"/>
<aop:pointcut id="globalPointUserDaoSelectLong" expression="execution(public * com.learn.dao.IUserDao.select(Long))"/>
<aop:pointcut id="globalPointUserDaoSelect" expression="execution(public * com.learn.dao.IUserDao.select(..))"/>
<aop:pointcut id="myPoint" expression="execution(* com.learn.service.*.*(..))"/>
<aop:pointcut id="myPointUserService" expression="execution(* com.learn.service.IUserService.*(..))"/>
<aop:pointcut id="myPointAffiliatePacket" expression="execution(* com.learn.service..*.*(..))"/>
<aop:pointcut id="myPointUser" expression="execution(com.learn.bean.User com.learn.service..*.*(..))"/>
<aop:pointcut id="myPointVoid" expression="execution(void com.learn.service..*.*(..))"/>
<!-- 配置一个切面类-->
<aop:aspect ref="logXmlAspect" id="log" >
<!-- 配置执行方法前切点切入-->
<aop:before method="before" pointcut-ref="globalPoint" ></aop:before>
<!-- 配置执行方法后切点切入-->
<aop:after method="after" pointcut-ref="globalPoint"></aop:after>
<!-- 配置返回后方法切入-->
<aop:after-returning method="afterReturning" pointcut-ref="globalPointPublic"></aop:after-returning>
<!-- 配置异常后方法切点切入 需要设置异常参数throwing值为异常参数名-->
<aop:after-throwing method="afterThrowing" pointcut-ref="globalPointUserDao" throwing="ex"></aop:after-throwing>
<!-- 配置环绕方法切入-->
<aop:around method="around" pointcut-ref="globalPointUserDao"></aop:around>
</aop:aspect>
</aop:config>
2.设定切面类
package com.learn.aspect;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import java.util.Arrays;
/**
* @author wangxing
* @version 2020/6/29 10:16 Administrator
*/
public class LogXmlAspect {
public void before(JoinPoint joinPoint) {
String name=joinPoint.getSignature().getName();
System.out.println(name+"前置通知");
}
public void after(JoinPoint joinPoint) {
String name=joinPoint.getSignature().getName();
System.out.println(name+"后置通知");
}
public void afterReturning(JoinPoint joinPoint) {
String name=joinPoint.getSignature().getName();
System.out.println(name+"方法后置返回通知");
}
public void afterThrowing(JoinPoint joinPoint,Exception ex) {
String name=joinPoint.getSignature().getName();
System.out.println(name+"方法后置异常通知");
}
/**
* 环绕通知
*
* @return
*/
public Object around(ProceedingJoinPoint joinPoint) {
Object result = null;
//获取方法参数
Object[] args = joinPoint.getArgs();
String methodName = joinPoint.getSignature().getName();
try {
if (args != null || args.length >= 1) {
System.out.println("方法参数为:" + Arrays.asList(args));
}
System.out.println("环绕:前置通知:" + methodName + "方法开始执行");
result = joinPoint.proceed(args);
System.out.println("环绕:返回通知:" + methodName + "方法返回值为:"+result);
} catch (Throwable throwable) {
System.out.println("环绕:后置异常通知:" + methodName + "方法出现异常,异常信息为"+throwable);
} finally {
System.out.println("环绕:后置通知:" + methodName + "方法执行结束");
}
return result;
}
}
3.进行测试
public class SpringAOPTest {
@Test
public void testXmlAop(){
ApplicationContext context = new ClassPathXmlApplicationContext("classpath:/spring-aop.xml");
IUserDao userDao = context.getBean("userDao",IUserDao.class);
//在没有使用AOP的情况下输出的类是当前接口的实现类
System.out.println(userDao.getClass());
System.out.println(userDao.select(1L));
}
}
2.通过注释方式进行AOP操作
1.配置spring.xml文件进行aop设置
<!--扫描包-->
<context:component-scan base-package="com.learn"></context:component-scan>
<!-- 启动AOP注解方式-->
<aop:aspectj-autoproxy></aop:aspectj-autoproxy>
2.建立AOP切面类
package com.learn.aspect;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;
/**
* @author wangxing
* @version 2020/6/28 15:47 Administrator
*/
@Component//标记为组件
@Aspect//标记为切面
public class LogAspect {
//使用一个通用的规则去进行匹配要进行切面的节点
@Pointcut("execution(* com.learn..*.*(..))")
public void pointcut(){
}
// 前置通知
@Before("pointcut()")
public void before(JoinPoint joinPoint){
System.out.println("annotation:"+joinPoint.getSignature().getName()+"方法前置通知");
}
// 后置通知
@After(pointcut())
public void after(JoinPoint joinPoint){
System.out.println("annotation:"+joinPoint.getSignature().getName()+"后置通知");
}
// 后置返回通知
@AfterReturning("execution(* com.learn.service..*.*(..))")
public void afterReturning(JoinPoint joinPoint){
System.out.println("annotation:"+joinPoint.getSignature().getName()+"后置返回通知");
}
// 后置异常通知
@AfterThrowing(value = "execution(* com.learn.service..*.*(..))",throwing = "ex")
public void afterThrowing(JoinPoint joinPoint,Exception ex){
System.out.println("annotation:"+joinPoint.getSignature().getName()+"后置异常通知"+ex);
}
}
SpringAOP的注解方式细节说明
1.使用注解模式要确保在xml文件中配置开启aop注解
<!-- 启动AOP注解方式-->
<aop:aspectj-autoproxy></aop:aspectj-autoproxy>
2.要进行切面编程的类都需要在ioc容器中
<!--扫描包-->
<context:component-scan base-package="com.learn"></context:component-scan>
3.构建AOP切面类,切面类需要用@Aspect标记为切面,并且切面类也要在容器中存在
@Component//标记为组件
@Aspect//标记为切面
public class LogAspect
4.在切面类中可以建立通用的匹配规则方法,其余的匹配可以调用这个方法来实现设立通用匹配方法的注解为:@Pointcut,在其参数中设置匹配规则,使用时直接用方法名替代原本的匹配规则
@Pointcut("execution(* com.learn..*.*(..))")
public void pointcut(){}
@Before("pointcut()")
public void before(JoinPoint joinPoint){
System.out.println("annotation:"+joinPoint.getSignature().getName()+"方法前置通知");
}
5.匹配方法的设定规则,和xml的配置规则方式相同
具体如下:
通过@Pointcut注解进行设置,在value属性中设置匹配规则,支持使用AspectJ切点标识符(PCD)用来进行切点表达式
标识符有:
1.execution:用于匹配方法执行连接点,使用Spring AOP时使用的主要切点标识符
2.within:只能匹配类这级,只能指定类, 类下面的某个具体的方法无法指定
3.this: 匹配实现了某个接口
4.target:限制匹配到连接点(使用Spring AOP时方法的执行),其中目标对象(正在代理的应用程序对象)是给定类型的实例
5.args:限制与连接点的匹配(使用Spring AOP时方法的执行),其中变量是给定类型的实例
@Pointcut 源码:
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
public @interface Pointcut {
String value() default "";
String argNames() default "";
}
6.匹配语法
1- 类匹配
within表达式
通过类名进行匹配 粗粒度的切入点表达式
语法:within(包名.类名)
2- 类.方法匹配
execution()表达式
通过类名,方法名进行匹配 细粒度的切入点表达式
语法:execution(返回值类型 包名.类名.方法名(参数类型,参数类型…))
1.访问修饰符:可不写 可以匹配任何一个访问修饰符
2.返回值:如果是jdk自带类型可以不用写完整限定名,如果是自定义类型需要写上完整限定名,如果被切入的方法返回值不一样可以使用代表所有的方法值都能匹配
3.包名:cn. cn.任意名字 但是只能匹配一级,cn..匹配所有子包和子孙包
4.类名: 可以写,代表任何名字的类名。 也可以模糊匹配 ServiceImpl==> UserServiceImpl ==>RoleServiceImpl
5.方法名:可以写,代表任何方法。 也可以模糊匹配 *add==> useradd ==>roleadd
6.参数:如果是jdk自带类型可以不用写完整限定名,如果是自定义类型需要写上完整限定名。 如果需要匹配任意参数 可以写: ..
-3 合并切点表达式
可以使用 &&, || 和 !等符号进行合并操作,也可以通过名字来指向切点表达式
//&&:两个表达式同时
execution( public int cn.tulingxueyuan.inter.MyCalculator.*(..)) && execution(* *.*(int,int) )
//||:任意满足一个表达式即可
execution( public int cn.tulingxueyuan.inter.MyCalculator.*(..)) && execution(* *.*(int,int) )
//!:只要不是这个位置都可以进行切入
execution( public int cn.tulingxueyuan.inter.MyCalculator.*(..)) && ! execution(* *.*(int,int) )
7.通知方法的执行顺序
正常执行情况下,AOP通知的执行顺序
@Before-->方法-->@After-->@AfterReturning
异常执行情况下,AOP通知的执行顺序
@Before-->方法-->@After-->@AfterThrowing
8.获取方法的详细信息
需要进行获取执行方法的参数可以在AOP切面类的方法参数中增加一个参数即可,参数类型为:
JoinPoint---(import org.aspectj.lang.JoinPoint;)
示例
// 后置返回通知
@AfterReturning("execution(* com.learn.service..*.*(..))")
public void afterReturning(JoinPoint joinPoint){
System.out.println("annotation:"+joinPoint.getSignature().getName()+"后置返回通知");
}
如果需要获取结果,还需要添加另外一个方法参数,并且告诉spring使用哪
个参数来进行结果接收
示例:
// 后置返回通知
@AfterReturning(value = "execution(* com.learn.service.impl.UserServiceImpl.select(..))",returning = "r1")
public void afterReturning(JoinPoint joinPoint,Object r1){
System.out.println("annotation:"+joinPoint.getSignature().getName()+"后置返回通知方法执行完成,结果是:"+r1);
}
可以通过相同的方式来获取异常的信息
// 后置异常通知
@AfterThrowing(value = "execution(* com.learn.service..*.*(..))",throwing = "ex")
public void afterThrowing(JoinPoint joinPoint,Exception ex){
System.out.println("annotation:"+joinPoint.getSignature().getName()+"后置异常通知"+ex);
}
9.spring对通过方法的要求
spring对于通知方法的要求并不是很高,你可以任意改变方法的返回值和方法的访问修饰符,但是唯一不能修改的就是方法的参数,会出现参数绑定的错误,原因在于通知方法是spring利用反射调用的,每次方法调用得确定这个方法的参数的值
10.表达式的抽取
在实际使用过程中,多个方法的表达式是一致的话,那么可以考虑将切入点表达式抽取出来
1.随便生命一个没有实现的返回void的空方法
2.给方法上标注@Potintcut注解
示例
//使用一个通用的规则去进行匹配要进行切面的节点
@Pointcut("execution(* com.learn..*.*(..))")
public void pointcut(){}
// 前置通知
@Before("pointcut()")
public void before(JoinPoint joinPoint){
System.out.println("annotation:"+joinPoint.getSignature().getName()+"方法前置通知");
}
11.环绕通知的使用
环绕通知是使用一个方法将需要切面的方法进行包围,在执行对应方法前后进行切面方法操作
使用标签为:@Around
示例
/**
* 环绕通知
* @param proceedingJoinPoint
* @return
*/
@Around("pointcut()")
public Object myAround(ProceedingJoinPoint proceedingJoinPoint){
Object[] args = proceedingJoinPoint.getArgs();
String name = proceedingJoinPoint.getSignature().getName();
Object proceed = null;
try {
System.out.println("环绕前置通知:"+name+"方法开始,参数是"+ Arrays.asList(args));
//利用反射调用目标方法,就是method.invoke()
proceed = proceedingJoinPoint.proceed(args);
System.out.println("环绕返回通知:"+name+"方法返回,返回值是"+proceed);
} catch (Throwable e) {
System.out.println("环绕异常通知"+name+"方法出现异常,异常信息是:"+e);
}finally {
System.out.println("环绕后置通知"+name+"方法结束");
}
return proceed;
}
环绕通知的执行顺序是优先于普通通知的,在有环绕通知和普通AOP通知的情况下执行顺序为:
环绕前置>普通前置>目标方法执行>环绕正常结束/出现异常>环绕后置>普通后置>普通返回或者异常
但需要注意的是:
如果出现了异常,那么环绕通知会处理或者捕获异常,普通异常通知是接收不到的,因此最好的方式是在环绕异常通知中向外抛出异常
总结
1.什么是AOP
2.代理模式:静态代理,动态代理,和相关实现
3.SpringAOP的实现原理
4.使用xml配置文件进行AOP操作
5.使用注解进行AOP操作