Spring-AOP基础
1.静态代理:
1.1角色分析:
抽象角色:一般使用接口或抽象类来实现
真实角色:被代理的角色
代理角色:代理真实角色,一般会增加其他操作
客户:使用代理角色来进行一些操作
1.2代码逻辑:
1.写一个公共方法的接口,这个方法就是真实角色和代理角色都要做的事
public interface Rent {
//租房
public void rent();
}
2.写真实角色,来实现这个接口中的方法
public class Host implements Rent {
@Override
public void rent() {
System.out.println("房东要出租房");
}
}
3.写代理角色,来实现这个接口中的方法,代理角色可以拓展真实角色以外要做的事
public class Proxy implements Rent {
private Host host;
public Proxy(){
}
public Proxy(Host host){
this.host=host;
}
@Override
public void rent() {
seeHouse();
host.rent();
}
//看房,这个是拓展的业务
public void seeHouse(){
System.out.println("中介带着看房");
}
}
4.客户访问代理角色。在访问代理角色时需要有真实角色,因为真正要做的业务是真实角色的
public class Client {
public static void main(String[] args) {
Host host = new Host();
//代理
Proxy proxy = new Proxy(host);
proxy.rent();
}
}
不改变原有代码的情况下,要扩展业务,就用代理模式
1.3代理模式的好处:
1.真实角色只需要做自己的业务,不用关心公共的业务
2.代理角色要处理公共业务
3.公共业务发送拓展时,方便集中管理
1.4静态代理的优缺点:
优点:结构清晰,易于理解
缺点:如果委托者有多个方法,则代理者也需要开发多个方法,其中往往存在大量重复代码,任然存在重复代码
2.动态代理:
需要了解的两个类:Proxy 代理,InvocationHandler 调用处理程序
Proxy:是用来动态生成代理对象实例的
InvocationHandler:调用处理程序用来返回一个结果
2.1代码逻辑:
1.写一个类去实现InvocationHandler接口
2.动态生成代理对象,Proxy的newProxyInstance方法,newProxyInstance方法有三个参数,依次是类加载器,被代理的类接口,InvocationHandler对象
3.编写invoke方法,这个方法是用来处理代理对象,并返回结果的
2.2动态代理的好处:
1.真实角色不用关注业务是怎么处理的
2.公共的业务交给代理角色,实现业务分工
3.一个动态代理类代理的是一个接口,一般就是对应的一类业务
4.一个动态代理类可以代理多个类,只要是实现了同一个接口即可
例子:
真实角色要做的业务:
这里直接实现UserService了,就这4个方法
public class UserServiceImpl implements UserService {
@Override
public void add() {
System.out.println("现在在使用add方法");
}
@Override
public void delete() {
System.out.println("现在在使用delete方法");
}
@Override
public void update() {
System.out.println("现在在使用update方法");
}
@Override
public void query() {
System.out.println("现在在使用query方法");
}
}
处理程序:只做两个事情
1.重写invoke,执行真正要执行的方法。
2.生成代理类
/**
* 自动生成代理类的步骤
* 1.Proxy.newProxyInstance(),需要传三个参数
* 2.第一个是类加载器,第二个是被代理的接口,第三个是InvocationHandler
*/
public class ProxyInvocationHandler implements InvocationHandler {
//被代理的接口
private Object target;
public void setTarget(Object target) {
this.target = target;
}
//生成代理类
public Object getProxy(){
return Proxy.newProxyInstance(this.getClass().getClassLoader(),
target.getClass().getInterfaces(),this);
}
//处理代理实例,并返回结果
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
//动态代理的本质,就是使用反射机制
log(method.getName());
Object result = method.invoke(target, args);
return result;
}
//增加的功能
public void log(String msg){
System.out.println("现在在执行"+msg+"方法");
}
}
客户访问代理类:
public class Client {
public static void main(String[] args) {
//真实角色
UserServiceImpl userService = new UserServiceImpl();
/**
*
* 生成代理类:通过调用程序处理角色来处理要调用的接口对象
* 怎么得到一个代理类
* 1.代理一个接口,也就是真实对象
* 2.用Proxy.newProxyInstance()方法动态生成代理类
*/
ProxyInvocationHandler pih = new ProxyInvocationHandler();
pih.setTarget(userService);//设置要代理的真实对象
//动态生成代理类
UserService proxy = (UserService) pih.getProxy();
proxy.add();
}
}
Spring事务是如何管理的:
1.编程式事务管理
TransactionTemplate
2.声明式事务管理:
建立在aop上,本质是通过aop功能,对方法前后进行拦截,将事务处理的功能编织到拦截的方法中,也就是在目标方法开始之前启动一个事务,在执行完目标方法之后根据执行情况提交或回滚事务。
spring-aop详细
1.核心概念
连接点(Join Point):
在程序执行过程中的某个特定的点,比如某方法调用的时候或者处理异常的时候。在SpringAOP中,一个连接点总是表示一个方法的执行 .
通俗的来讲,层与层之间方法的调用过程称之为连接点
切入点(Pointcut):
匹配连接点的断言。通知和一个切入点表达式关联,并在满足这个切入点的连接点上运行(例如:当执行某个特定名称的方法时)。切入点表达式如何与连接点匹配是AOP的核心,Spring缺省使用AspectJ切入点语法。
通俗讲的来讲,在连接点的基础上,增加切入规则,选择需要进行增强的连接点,这些基于规则选出来的连接点,就称为切入点
切面(Aspect)
一个关注点的模块化,这个关注点可能会横切多个对象,· 事务管理是J2EE应用中一个关于横切关注点的很好的例子。在Spring AOP中,切面可以使用基于模式
)或者基于@Aspect注解
的方式来实现。
通俗的来讲:
狭义上就是当spring拦截下切入点后,将这些切入点交给处理类进行功能的增强,这个处理类就称之为切面
广义上来讲,将spring底层的 代理
切入点
和处理类
加在一起 对层与层之间调用过程进行增强的机制 称之为切面
通知(Advice)
在切面的某个特定的连接点上执行的动作,其中包括了 around before
和 after
等不同类型的通知,· 许多AOP框架(包括Spring)都是以拦截器做通知模型,并维护一个以连接点为中心的拦截器链。
同俗讲来讲:
在spring底层的代理拦截下切入点后,讲切入点交给切面,切面中要有处理这些切入点的方法,这些方法就称之为通知(也叫增强 增强方法),针对切入点执行的过程,通知还分为不同的类型,分别关注切入点在执行过程中的不同的时机
目标对象(Target Object)
被一个或者多个切面所通知的对象。也被称做被通知(advised)对象。 既然Spring AOP是通过运行时代理实现的,这个对象永远是一个被代理(proxied)对象
通俗的来讲:
就是真正希望被访问到的对象。spring底层的动态代理对他进行了代理,具体能不能真的访问到目标对象,或在目标对象真正执行之前和之后是否做一些额外的操作,取决于切面
2.使用spring实现aop
入门案例:
使用aop织入,需要导aspectjweaver包
<dependencies>
<!--aop织入包-->
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.9.4</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>5.2.16.RELEASE</version>
</dependency>
<!-- 注解支持 -->
<dependency>
<groupId>javax.annotation</groupId>
<artifactId>javax.annotation-api</artifactId>
<version>1.3.2</version>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjrt</artifactId>
<version>1.8.6</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13.2</version>
</dependency>
</dependencies>
编写业务:
UserController.java
package com.iweb;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
@Controller
public class UserController {
@Autowired
private UserService userService;
public void test(){
userService.addUser();
userService.queryUser();
userService.deleteUser();
}
}
UserServiceImpl.java
package com.iweb;
import org.springframework.stereotype.Service;
@Service
public class UserServiceImpl implements UserService {
@Override
public void addUser() {
System.out.println("添加用户");
}
@Override
public void queryUser() {
System.out.println("查询用户");
}
@Override
public void deleteUser() {
System.out.println("删除用户");
}
}
切面类:
package com.iweb.aspect;
import org.springframework.stereotype.Component;
@Component
public class FirstAspect {
public void before(){
System.out.println("#权限检查");
}
}
配置文件:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/aop https://www.springframework.org/schema/aop/spring-aop.xsd">
<!-- 包扫描-->
<context:component-scan base-package="com.iweb"></context:component-scan>
<!-- 注解注入-->
<context:annotation-config></context:annotation-config>
<!-- 配置切面 -->
<aop:config>
<aop:aspect ref="firstAspect">
<!--
配置通知
method 具体的通知方法
pointcut 切入点表达式
-->
<aop:before method="before"
pointcut="within(com.iweb.UserServiceImpl)"></aop:before>
</aop:aspect>
</aop:config>
</beans>
切入点表达式:
within表达式:
通过类名进行匹配,粗粒度的切入点表达式 。within(包名.类名)
pointcut:切入点
expression:表达式
例子:
这个类中的所有的连接点都会被表达式识别,成为切入点
<aop:pointcut id="pc01" expression="within(com.iweb.UserServiceImpl)"/>
在within表达式中可以使用*号匹配符,匹配指定包下所有的类,注意,只匹配当前包,不包括当前包的子孙包,如下表达式中,*代表service包下的所有类,但不包含子孙包
<aop:pointcut id="pc01" expression="within(com.iweb.service.*)"/>
在within表达式中也可以用*号匹配符,匹配包,如下表达式中:第一个*表示一层子目录,第二*代表这个子目录下的所有的类
<aop:pointcut id="pc01" expression="within(com.iweb.service.*.*)"/>
execution表达式:
细粒度的切入点表达式,可以以方法为单位定义切入点规则
execution(返回值类型 包名.类名.方法名(参数类型,参数类型…))
例子:
以下 切入点规则表示,切出指定包下指定类下指定名称指定参数指定返回值的方法
<aop:pointcut expression=
"execution(void com.iewb.service.UserServiceImpl.addUser(java.lang.String))" id="pc1"/>
以下切入点规则表示,切出指定包下所有的类中的query方法,要求无参,但返回值类型不限。
<aop:pointcut expression="execution(* com.iweb.service.*.query())" id="pc1"/>
以下切入点规则表示,切出指定包及其子孙包下所有的类中的任意方法,参数数量及类型不限,返回值类型不限。这种写法等价于within表达式的功能。
<aop:pointcut expression="execution(* com.iweb.service..*.*(..))" id="pc1"/>
3.五大通知类型
前置通知
在目标方法执行之前执行执行的通知
前置通知方法,可以没有参数,也可以额外接收一个JoinPoint,Spring会自动将该对象传入,代表当前的连接点,通过该对象可以获取目标对象 和 目标方法相关的信息。
注意,如果接收JoinPoint,必须保证其为方法的第一个参数,否则报错。
aop配置:
<!--aspect:切面 ref:要引用的类,会将这个类变成切面-->
<aop:aspect ref="noticeAspect">
<aop:config>
<aop:pointcut id="pc01" expression="execution(* com.iweb..*(..))"/>
<!--aspect:切面 ref:要引用的类,会将这个类变成切面-->
<aop:aspect ref="noticeAspect">
<!--point-ref表示在某个切入点切入-->
<aop:before method="before" pointcut-ref="pc01"></aop:before>
</aop:aspect>
</aop:config>
切面:
package com.iweb.aspect;
import org.springframework.stereotype.Component;
@Component
public class NoticeAspect {
/*
* 前置通知
* */
public void before(){
System.out.println("before()...");
}
}
接收JoinPoint的通知方法
/*
* JoinPoint 代表连接点对象
* */
public void before(JoinPoint jp){
//获取目标对象
Object target = jp.getTarget();
//获取目对象的类型
Class<?> aClass = target.getClass();
//获取方法对象
MethodSignature signature = (MethodSignature) jp.getSignature();
Method method = signature.getMethod();
System.out.println("###before.."+aClass+".."+method);
}
环绕通知
在目标方法执行之前和之后都可以执行额外代码的通知。
在环绕通知中必须显式的调用目标方法,否则目标方法不会执行。
这个显式调用是通过ProceedingJoinPoint来实现的,可以在环绕通知中接收一个此类型的形参,spring容器会自动将该对象传入,这个参数必须处在环绕通知的第一个形参位置。
ProceedingJoinPoint时JoinPoint的子类,要注意,只有环绕通知可以接收ProceedingJoinPoint,而其他通知只能接收JoinPoint。
aop配置:
<aop:config>
<aop:aspect ref="noticeAspect">
<aop:around method="around" pointcut="within(com.iweb.UserServiceImpl)"> </aop:around>
</aop:aspect>
</aop:config>
通知方法
public Object around(ProceedingJoinPoint pjp) throws Throwable {
System.out.println("before()....");
Object proceed = pjp.proceed();
System.out.println("after()...");
return proceed;
}
注意
:
环绕通知需要返回返回值,否则真正调用者将拿不到返回值,只能得到一个null。
环绕通知有 1.控制目标方法是否执行 2.目标方法执行之前或之后执行额外代码 3.控制是否返回返回值
4.改变返回值的能力
环绕通知虽然有这样的能力,但一定要慎用,要小心不要破坏了软件分层的“高内聚 低耦合”的目标。
后置通知
在目标方成功执行之后执行的通知。
在后置通知中也可以选择性的接收一个JoinPoint来获取连接点的额外信息,但是这个参数必须处在参数列表的第一个
aop配置:
<aop:config>
<aop:pointcut id="pc01" expression="execution(* com.iweb..*(..))"/>
<aop:aspect ref="noticeAspect">
<!-- 后置通知可以配置一个returning属性,接收目标方法返回值-->
<aop:before method="after-returning" returning="retObj" pointcut-ref="pc01"></aop:before>
</aop:aspect>
</aop:config>
切面:
package com.iweb.aspect;
import org.springframework.stereotype.Component;
@Component
public class NoticeAspect {
/*
* 前置通知
* */
public void afterReturning(){
System.out.println("after-returning()...");
}
}
接收JoinPoint,接收返回值的通知方法
/*
* JoinPoint 代表连接点对象
* */
public void afterReturning(JoinPoint jp,Object retObj){
//获取目标对象
Object target = jp.getTarget();
//获取目对象的类型
Class<?> aClass = target.getClass();
//获取方法对象
MethodSignature signature = (MethodSignature) jp.getSignature();
Method method = signature.getMethod();
System.out.println("###after-returning.."+aClass+".."+method);
}
异常通知
在目标方法抛出异常时执行的通知
可以配置传入JoinPoint获取目标对象和目标方法相关信息,但必须处在参数列表第一位
另外,还可以配置参数,让异常通知接收到目标方法抛出的异常对象
aop配置
<aop:config>
<aop:aspect ref="noticeAspect">
<aop:after-throwing method="afterThrowing" throwing="e" pointcut="within(com.iweb.UserServiceImpl)"></aop:after-throwing>
</aop:aspect>
</aop:config>
通知方法
public void afterThrowing(JoinPoint jp,Throwable e){
Class<?> clz = jp.getTarget().getClass();
String name = jp.getSignature().getName();
System.out.println(clz+"#"+name+e);
}
最终通知
是在目标方法执行之后执行的通知。
和后置通知不同之处在于,后置通知是在方法正常返回后执行的通知,如果方法没有正常返-例如抛出异常,则后置通知不会执行。而最终通知无论如何都会在目标方法调用过后执行,即使目标方法没有正常的执行完成。
另外,后置通知可以通过配置得到返回值,而最终通知无法得到
aop配置
<aop:config>
<aop:aspect ref="noticeAspect">
<aop:after method="after" pointcut="within(com.iweb.UserServiceImpl)"></aop:after>
</aop:aspect>
</aop:config>
通知方法
public void after(JoinPoint jp){
Class<?> clz = jp.getTarget().getClass();
String name = jp.getSignature().getName();
System.out.println(clz+"#"+name);
}
五种通知的执行顺序
-
目标方法没有抛出异常
- 前置通知
- 环绕通知的调用目标方法之前的代码//取决于配置顺序
- 目标方法
- 环绕通知的调用目标方法之后的代码
- 后置通知
- 最终通知//取决于配置顺序
-
目标方法抛出异常
- 前置通知
- 环绕通知的调用目标方法之前的代码//取决于配置顺序
- 目标方法 抛出异常
- 异常通知
- 最终通知//取决于配置顺序
-
如果存在多个切面
多切面执行时,采用了责任链设计模式。
切面的配置顺序决定了切面的执行顺序,多个切面执行的过程,类似于方法调用的过程,在环绕通知的proceed()执行时
五种通知的使用场景
通知类型 使用场景
前置通知 记录日志(方法将被调用)
环绕通知 控制事务 权限控制
后置通知 记录日志(方法已经成功调用)
异常通知 异常处理 控制事务
最终通知 记录日志(方法已经调用,但不一定成功)
SpringAOP的原理
Spring在创建bean时,除了创建目标对象bean之外,会根据aop的配置,生成目标对象的代理对象,将其存储,之后获取bean时得到的其实是代理对象,在代理对象中,根据配置的切入点规则,决定哪些方法不处理直接执行目标方法,哪些方法拦截后进行增强,需要增强的方法拦截后根据配置调用指定切面中的指定通知执行增强操作。
Spring自动为目标对象生成代理对象,默认情况下,如果目标对象实现过接口,则采用java的动态代理机制,如果目标对象没有实现过接口,则采用cglib动态代理。
开发者可以可以在spring中进行配置,要求无论目标对象是否实现过接口,都强制使用cglib动态代理。
<!--
proxy-target-class="true" 此选项要求无论目标对象是否实现过
接口都采用cglib来动态代理生成代理者
-->
<aop:config proxy-target-class="true">
<aop:aspect ref="noticeAspect">
<aop:after method="after" pointcut="within(com.iweb.UserServiceImpl)"></aop:after>
</aop:aspect>
</aop:config>
4.AOP注解实现
spring也支持注解方式实现AOP,相对于配置文件方式,注解配置更加的轻量级,配置、修改更加方便,是目前最流行的方式。
开启AOP的注解配置方式
<!--开启aop 注解配置-->
<aop:aspectj-autoproxy></aop:aspectj-autoproxy>
@Aspect
将指定的类标志为一个切面
通知配置
-
@Before
前置通知 -
@Around
环绕通知 -
@AfterReturning
后置通知在后置通知的注解中,也可以额外配置一个returning属性,来指定一个参数名接受目标方法执行后的返回值
-
@AfterThrowing
异常通知? 在异常通知的注解中,也可以额外配置一个throwing属性,来指定一个参数名接受目标方法抛出的异常对象
-
@After
最终通知
@Pointcut
如果一个切面中多个通知 重复使用同一个切入点表达式,则可以将该切入点表达式单独定义,后续引用,注意,在当前切面中通过注解定义的切入点只在当前切面中起作用,其他切面看不到
案例
异常信息收集
package com.iweb.aspect;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.AfterThrowing;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;
/*
* aop案例 异常信息收集
* */
@Component
@Aspect
public class ExceptionAspect {
private Logger logger = Logger.getLogger(ExceptionAspect.class);
@AfterThrowing(value = "execution(* com.iweb.service..*.*(..))",throwing = "e")
public void afterThrowing(JoinPoint jp,Throwable e){
//获取目标对象
Object obj = jp.getTarget();
//获取方法信息
MethodSignature signature = (MethodSignature) jp.getSignature();
//记录错误日志
logger.error("在访问["+obj+"]的["+signature.getMethod().getName()+"]方法时抛出了异常["+e.getMessage()+"]");
}
}
统计业务方法执行的时间
package com.iweb.aspect;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;
@Component
@Aspect
public class UseTimeAspect {
@Around("execution(* com.iweb.service..*.*(..))")
public Object around(ProceedingJoinPoint pjp) throws Throwable {
//开始时间
long begin = System.currentTimeMillis();
//调用目标方法
Object obj = pjp.proceed();
//结束时间
long end = System.currentTimeMillis();
//计算耗时
long useTime = end - begin;
//输出结果
System.out.println("###调用["+pjp.getTarget()+"]的["+pjp.getSignature().getName()+"]方法,用时["+useTime+"]毫秒###");
return obj;
}
}
通过aop来实现权限控制
-
通过自定义注解声明业务方法是否需要权限控制
-
通过权限注解上的属性声明需要什么样的权限
-
通过切面拦截业务方法,根据是否需要权限、是否具有权限,控制目标方法的执行
-
开发权限注解
枚举类:
public enum PrivEnum { ADMIN,SUPERADMIN,USER; }
@Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface PrivAnno { PrivEnum [] value(); }
应用时加上注解权限
package com.iweb.service; import org.springframework.stereotype.Service; import cn.tedu.anno.PrivAnno; import cn.tedu.enumration.PrivEnum; @Service public class UserServiceImpl implements UserService { @PrivAnno({PrivEnum.ADMIN,PrivEnum.SUPERADMIN}) @Override public void addUser() { System.out.println("add user..."); } @PrivAnno(PrivEnum.SUPERADMIN) @Override public void delUser() { System.out.println("del user..."); } @Override public void queryUser() { System.out.println("query user..."); } }
开发切面,实现环绕通知,检查用户是否具有权限,有权限则执行目标方法,没权限提示权限不足
package com.iweb.aspect; import java.lang.reflect.Method; import java.util.Arrays; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.reflect.MethodSignature; import org.springframework.stereotype.Component; import cn.tedu.anno.PrivAnno; import cn.tedu.enumration.PrivEnum; import cn.tedu.test.Test01; @Component @Aspect public class PrivAspect { @Around("execution(* cn.tedu.service..*.*(..))") public Object around(ProceedingJoinPoint pjp) throws Throwable { //1.获取目标对象 Object obj = pjp.getTarget(); //2.获取目标方法 MethodSignature signature = (MethodSignature) pjp.getSignature(); Method method = signature.getMethod();//接口上的方法 //3.获取实现类上的目标方法 Method instMethod = obj.getClass().getMethod(method.getName(), method.getParameterTypes()); //4.判断是否有注解 if(instMethod.isAnnotationPresent(PrivAnno.class)) {//需要权限 //获取具体需要什么权限 PrivAnno panno = instMethod.getAnnotation(PrivAnno.class); PrivEnum[] privs = panno.value(); //判断是否有权限 if(Arrays.asList(privs).contains(Test01.prive)) {//权限够 return pjp.proceed(); }else {//权限不够 throw new RuntimeException("权限不足"); } } else {//不需要权限 return pjp.proceed(); } } }