Spring AOP
Spring IoC 解决了软件分层中的耦合的问题,但是在很多地方仍然存在非该层应该实现的功能,造成了 无法“高内聚”的现象,同时存在大量存在重复代码,开发效率低下。
示例:
@Service
public class UserServiceImpl implements UserService {
@Autowired
private UserDao userDao;
@Override
public void addUser(User user) {
try {
// 功能型代码
System.out.println("记录日志...");
System.out.println("权限控制...");
System.out.println("开启事务");
// 业务型代码(核心)
userDao.addUser(user);
// 功能型代码
System.out.println("提交事务");
} catch (Exception e) {
e.printStackTrace();
System.out.println("回滚事务");
}
}
@Override
public void delUser(User user) {
try {
// 功能型代码
System.out.println("记录日志...");
System.out.println("权限控制...");
System.out.println("开启事务");
// 业务型代码(核心)
userDao.deleteUser(user.getId());
// 功能型代码
System.out.println("提交事务");
} catch (Exception e) {
e.printStackTrace();
System.out.println("回滚事务");
}
}
}
解决方案:AOP面向切面编程
在不改变一个类的情况下,增强该类的对象:
- 继承,重写
- 装饰设计模式
- 代理设计模式
三种方式的比较:
interface Animal{
void eat();
void bark();
}
class Dog implements Animal{
@Override
public void eat(){
System.out.println("The dog is eating bones");
}
// 待改造的方法
@Override
public void bark(){
System.out.println("汪汪。。。");
}
}
public class Test01 {
/*
* 改造或者增强不喜欢的方法 --- 继承,重写
* 缺点:只对新创建的对象有效,之前已经创建的对象没有影响
* */
@Test
public void test1(){
Dog oldDog = new Dog();
class NewDog extends Dog{
@Override
public void bark() {
System.out.println("Woof, woof...");
}
}
Dog dog = new NewDog();
// 缺点:只对新创建的对象有效,之前已经创建的对象没有影响
oldDog.bark();
dog.bark();
}
/*
* 改造或者增强不喜欢的方法 --- 装饰设计模式
* 缺点:其他不想改造的方法也需要重写,存在方法的冗余
* */
@Test
public void test2(){
class NewDog implements Animal{
private Animal animal = null;
public NewDog(Animal animal) {
this.animal = animal;
}
@Override
public void eat() {
animal.eat();
}
@Override
public void bark() {
System.out.println("Woof, woof...");
}
}
Dog dog = new Dog();
NewDog dog1 = new NewDog(dog);
dog1.bark();
dog1.eat();
}
/*
* 改造或者增强不喜欢的方法 --- 代理设计模式 (静态代理)
* 缺点:其他不想改造的方法也需要重写,存在方法的冗余
* */
@Test
public void test3(){
Dog dog = new Dog();
class ProxDog implements Animal{
@Override
public void eat() {
dog.eat();
}
@Override
public void bark() {
System.out.println("Woof, woof...");
}
}
ProxDog proxDog = new ProxDog();
proxDog.eat();
proxDog.bark();
}
}
代理设计模式
- 在不改变一个类的情况下,增强该类的对象
静态代理设计模式
静态代理设计模式特点:
-
优点:结构清晰 易于理解
-
缺点:如果被代理者有多个方法,则代理者也需要开发多个方法,其中往往存在大量重复代码,仍然存在代码重复。
示例:
interface Star{
void sign();
void eat();
}
class BJT implements Star{
@Override
public void sign(){
System.out.println("问bjt要签名...");
}
@Override
public void eat(){
System.out.println("和bjt吃饭...");
}
}
public class Test02 {
/*
* 静态代理:存在代码的冗余
* */
@Test
public void test1(){
// 目标对象
BJT bjt = new BJT();
// 代理者
class Assistant implements Star{
@Override
public void sign() {
System.out.println("你是粉丝?");
bjt.sign();
System.out.println("记录一下");
}
@Override
public void eat() {
System.out.println("你是粉丝?");
bjt.eat();
System.out.println("记录一下");
}
}
// 有事找代理不能直接找目标对象
Assistant assistant = new Assistant();
assistant.sign();
assistant.eat();
}
静态代理设计模式解决了软件分层过程中,额外的功能代码侵入模块的问题,将额外的功能代码提取到了代理者中进行,但是静态代理实现的代理者中存在大量重复的代码,并没有解决代码重复问题。所以在真正开发中 — 包括Spring的底层,基本不会使用静态代理。
动态代理设计模式
在JDK中提供了动态代理实现的工具类,直接使用该工具类就可以创建出代理者,并且可以通过内置的回调函数指定代理在工作时的执行逻辑,从而实现基于JDK原生API的动态代理机制。
示例:
@Test
public void test02(){
// 目标对象
BJT bjt = new BJT();
// 生成代理者
Star proxy = (Star)Proxy.newProxyInstance(
bjt.getClass().getClassLoader(), // 类加载器 --- 当初加载目标对象的类加载器就适合用来生成该目标对象的代理对象
bjt.getClass().getInterfaces(), // 实现和目标对象相同的接口
new InvocationHandler() { // InvocationHandler接口的实现类
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("你是粉丝?");
// 调用目标对象对应的方法
Object retObject = method.invoke(bjt,args);
System.out.println("记录一下");
return retObject;
} // 处理类,在其中的invoke()中编写代理逻辑
}
);
// 有事找代理不能直接找目标对象
proxy.eat();
proxy.sign();
}
java动态代理的原理图:
java动态代理的特点:
-
优点:不需要像静态代理一样被代理方法都要实现一遍,而只需要在回调函数中进行处理就可以了,重复代码只需编写一次。
-
缺点:java的动态代理是通过代理者实现和被代理者相同的接口来保证两者具有相同的方法的,如果被代理者想要被代理的方法不属于任何接口,则生成的代理者自然无法具有这个方法,也就无法实现对该方法的代理。
-
因此java的动态代理机制是基于接口进行的,受制于要代理的方法是否有接口的支持。
应用动态代理改造对象:
interface Animal{
void eat();
void bark();
}
class Dog implements Animal{
@Override
public void eat(){
System.out.println("The dog is eating bones");
}
@Override
public void bark(){
System.out.println("汪汪。。。");
}
}
public class Test01 {
/*
* 改造或者增强不喜欢的方法 --- 代理设计模式 (动态代理)
* */
@Test
public void test4(){
Dog dog = new Dog();
Animal proxy = (Animal) Proxy.newProxyInstance(
dog.getClass().getClassLoader(),
dog.getClass().getInterfaces(),
new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
if ("bark".equals(method.getName())){
System.out.println("Woof, woof...");
return null;
}
else {
return method.invoke(dog);
}
}
}
);
proxy.eat();
proxy.bark();
}
}
第三方包CGLIB实现的动态代理
-
CGLIB是第三方提供的动态代理的实现工具,不管有没有接口都可以实现动态代理。
-
CGLIB实现动态代理的原理:生成的动态代理是被代理者的子类,所以代理者具有和父类 (被代理者)相同的方法,从而实现代理,这种方式基于继承,不再受制于接口。
-
CGLIB相关包:spring-core-x.x.x.RELEASE.jar
案例演示:
interface Star{
void sign();
void eat();
}
class BJT implements Star{
@Override
public void sign(){
System.out.println("问bjt要签名...");
}
@Override
public void eat(){
System.out.println("和bjt吃饭...");
}
public void piano(){
System.out.println("弹钢琴。。。");
}
}
/*
* CGLIB动态代理
*
* */
@Test
public void test03(){
BJT bjt = new BJT();
// 1. 创建增强器
Enhancer enhancer = new Enhancer();
// 2. 指定代理要实现的接口 --- 不是强制的
enhancer.setInterfaces(bjt.getClass().getInterfaces());
// 3. 指定代理者的父类 --- 强制
enhancer.setSuperclass(bjt.getClass());
// 4. 指定回掉函数
enhancer.setCallback(new MethodInterceptor() {
@Override
public Object intercept(Object o, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
System.out.println("你是粉丝?");
// 调用目标对象对应的方法
// retObject接受方法的返回值
Object retObject = method.invoke(bjt,args);
System.out.println("记录一下");
return retObject;
}
});
// 5. 创建代理对象
BJT proxy = (BJT) enhancer.create();
proxy.piano();
}
}
CGLIB动态代理的特点:
-
优点:无论是否有接口都可以实现动态代理,使用场景基本不受限
-
缺点:第三方提供的动态代理机制,不是原生的,需要导入第三方开发包才可以使用。
Spring AOP
基本概念
-
连接点 (Joinpoint):在程序执行过程中某个特定的点,比如某方法调用的时候或者处理异常的时候。在Spring AOP中,一个连接点总是表示一个方法的执行。
通俗讲:层与层之间方法的调用过程称之为连接点。
-
切入点 (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底层的动态代理对其进行了代理,具体能不能真的访问到目标对象,或在目标对象真正执行之前和之后是否做一些额外的操作,取决于切面。
示例:
导入AOP相关开发包:
配置文件:
<?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
http://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop.xsd
">
<!-- 指定扫描哪些包-->
<!--Spring容器会在启动时自动扫描指定包极其子孙包-->
<context:component-scan base-package="cn.zss"/>
<context:annotation-config/>
<!-- 配置切面-->
<aop:config>
<aop:aspect ref="firstAspect">
<!-- 配置通知-->
<aop:before method="myBefore" pointcut="within(cn.zss.service.UserServiceImpl)"/>
</aop:aspect>
</aop:config>
</beans>
Web层:
package cn.zss.web;
@Controller
public class UserServlet {
@Autowired
private UserService userService = null;
public void addUser(){
System.out.println("userServlet ... add ...");
userService.addUser();
}
public void delUser(){
System.out.println("userServlet ... del ...");
userService.delUser();
}
}
Service层:
a. 接口UserService
package cn.zss.service;
public interface UserService {
public void addUser();
public void delUser();
}
b. 接口的实现类UserServiceImpl
package cn.zss.service;
@Service
public class UserServiceImpl implements UserService {
@Override
public void addUser() {
System.out.println("增加用户");
}
@Override
public void delUser() {
System.out.println("删除用户");
}
}
切面类:
package cn.zss.aspect;
@Component
public class FirstAspect {
public void myBefore(JoinPoint joinPoint) throws Throwable {
// 织入 --- 增加功能型代码
System.out.println("记录日志");
}
}
切入点表达式
within表达式
- 通过类名进行匹配,粗粒度的切入点表达式
- 语法:within(包名.类名)
- 类中的所有连接点都会被表达式识别,成为切入点,如within(cn.zss.service.UserServiceImpl)
- 在within表达式中可以使用*号匹配符,匹配指定包下所有的类;注意,只匹配当前包,不包括当前包的子孙包,如within(cn.zss.service.*)
- 在within表达式中也可以用*号匹配符,匹配包,如within(cn.zss.service.*.*),第一个*代表一层子目录,第二个*代表这个子目录下的所有类
- 在within表达式中也可以用…*号匹配符,匹配指定包下极其子孙包下的所有类,如within(cn.zss.service…*)
execution()表达式
- 细粒度的切入点表达式,可以以方法为单位定义切入点规则
- 语法:execution(返回值类型 包名.类名.方法名(参数类型,参数类型…))
- 同样支持*通配符,用法类似于within表达式
示例:
<aop:config>
<!-- 1. 匹配cn.zss.service包下UserServiceImpl类的delUser方法,返回值为void类型,参数列表为(int)类型-->
<aop:pointcut id="pc01" expression="execution(void cn.zss.service.UserServiceImpl.delUser(int))"/>
<!-- 2. 匹配cn.zss.service包下所有类的以del开头的方法,返回值不限参数为空-->
<aop:pointcut id="pc02" expression="execution(* cn.zss.service.*.del*())"/>
<!-- 3. 匹配cn.zss.service包极其子孙包下所有类的以del开头的方法,返回值不限参数为空-->
<aop:pointcut id="pc03" expression="execution(* cn.zss.service..*.del*())"/>
<!-- 4. 匹配cn.zss.service包极其子孙包下所有类的以del开头的方法,不限参数和返回值-->
<aop:pointcut id="pc04" expression="execution(* cn.zss.service..*.del*(..))"/>
<!-- 5. 匹配cn.zss.service包极其子孙包下所有类的所有方法,不限参数和返回值-->
<aop:pointcut id="pc05" expression="execution(* cn.zss.service..*.*(..))"/>
<!-- 6. 上面的表达式可以简写为:-->
<aop:pointcut id="pc06" expression="execution(* cn.zss.service..*(..))"/>
<!--定义切面和通知-->
<aop:aspect ref="firstAspect"><aop:before method="myBefore" pointcut-ref="pc06" />
</aop:aspect>
</aop:config>
Spring的五大通知类型
前置通知
-
在目标方法执行之前执行执行的通知
-
前置通知方法,可以没有参数,也可以额外接收一个JoinPoint,Spring会自动将该对象传入,代表当前的连接点,通过该对象可以获取目标对象和目标方法相关的信息。
-
注意,如果接收JoinPoint,必须保证其为方法的第一个参数,否则报错。
示例:
<!-- 配置切面-->
<aop:config>
<aop:pointcut id="pc01" expression="execution(* cn.zss.service..*(..))"/>
<aop:aspect ref="firstAspect">
<aop:before method="before" pointcut-ref="pc01"/>
</aop:aspect>
</aop:config>
/*
* 前置通知
*/
@Component
public class FirstAspect {
public void before(JoinPoint joinPoint){
// 获取目标对象
Object target = joinPoint.getTarget();
System.out.println(target);
// 获取目标方法
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
System.out.println(signature);
System.out.println("前置通知");
}
}
环绕通知
-
在目标方法执行之前和之后都可以执行额外代码的通知。
-
在环绕通知中必须显式的调用目标方法,否则目标方法不会执行。
-
显式调用通过ProceedingJoinPoint来实现,可以在环绕通知中接收一个此类型的形参,spring容器会自动将该对象传入,这个参数必须处在环绕通知的第一个形参位置。
-
ProceedingJoinPoint是JoinPoint的子类,要注意,只有环绕通知可以接收ProceedingJoinPoint,而其他通知只能接收JoinPoint。
示例:
<!-- 配置切面-->
<aop:config>
<aop:pointcut id="pc01" expression="execution(* cn.zss.service..*(..))"/>
<aop:aspect ref="firstAspect">
<!-- 环绕通知-->
<aop:around method="arround" pointcut-ref="pc01"/>
</aop:aspect>
</aop:config>
/*
* 环绕通知
* 必须手动调用目标方法,否则目标方法不会执行
* 可以在环绕通知方法中,接收一个ProceedingJoinPoint类型的参数
* 通过此参数的proceed()方法调用目标方法
* 在proceed()方法之前可以编写目标方法执行之前要执行的逻辑
* 在proceed()方法之后可以编写目标方法执行之后要执行的逻辑
* 需要在环绕通知中将方法执行后的结果手动的返回才能让被调用者得到此返回值,否则只能得到null
* 环绕通知甚至可以改变返回的结果,但是这种做法破坏了高内聚的特性,不建议使用
* ProceedingJoinPoint是JoinPoint的子类,所以通过此对象,也可以获得目标对象和目标方法相关的信息
* ProceedingJoinPoint如果出现,必须出现在通知方法参数列表的首位,且只能被环绕通知接收
* */
public Object arround(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
System.out.println("环绕通知前...");
Object retObj = proceedingJoinPoint.proceed();
System.out.println("arround"+retObj);
System.out.println("环绕通知后");
return retObj;
}
注意:
-
环绕通知需要返回返回值,否则真正调用者将拿不到返回值
-
环绕通知有的功能有:
- 控制目标方法是否执行
- 目标方法执行之前或之后执行额外代码
- 控制是否返回返回值
- 改变返回值
-
环绕通知虽然有这样的能力,但一定要慎用,要小心不要破坏了软件分层的“高内聚 低耦合”的目标。
后置通知
-
在目标方法正确执行完成之后 执行的通知
-
在后置通知中也可以选择性的接收一个JoinPoint来获取连接点的额外信息,但是这个参数必须处在参数列表的首位,否则会抛出异常
-
在后置通知中,还可以通过配置获取目标方法的返回值
示例:
<!-- 配置切面-->
<aop:config>
<aop:pointcut id="pc01" expression="execution(* cn.zss.service..*(..))"/>
<aop:aspect ref="firstAspect">
<!--后置通知-->
<!--returning="retObj" 配置目标方法执行后的返回值-->
<aop:after-returning method="afterReturning" pointcut-ref="pc01" returning="retObj"/>
</aop:aspect>
</aop:config>
/*
* 后置通知
* 在目标方法正确执行完成之后执行的通知
* 可以接受JointPoint对象,通过此对象可以获取目标对象和目标方法相关的信息
* 可以配置returning="retObj",再在方法中接收该名称的参数,得到目标方法执行后的返回值
* 可以接受JointPoint对象必须在参数列表的首位
* */
public void afterReturning(JoinPoint jp,Object retObj){
// 获取目标对象
Object target = jp.getTarget();
System.out.println(target);
// 获取目标方法
MethodSignature signature = (MethodSignature) jp.getSignature();
System.out.println(signature);
System.out.println("后置通知"+"..."+retObj);
}
异常通知
- 在目标方法抛出异常时执行的通知
- 可以配置传入JoinPoint获取目标对象和目标方法相关信息,但必须处在参数列表第一位
- 还可以配置参数(throwing = “e”),让异常通知接收到目标方法抛出的异常对象
示例:
<!-- 配置切面-->
<aop:config>
<aop:pointcut id="pc01" expression="execution(* cn.zss.service..*(..))"/>
<aop:aspect ref="firstAspect">
<!--异常通知-->
<aop:after-throwing method="afterThrowing" pointcut-ref="pc01" throwing="e"/>
</aop:aspect>
</aop:config>
public void afterThrowing(JoinPoint jp, Throwable e){
// 获取目标对象信息
Object target = jp.getTarget();
System.out.println(target);
// 获取目标方法信息
MethodSignature signature = (MethodSignature) jp.getSignature();
System.out.println(signature);
System.out.println(e);
System.out.println("异常通知");
}
最终通知
- 在目标方法执行之后执行的通知
- 后置通知是在方法正常返回后执行的通知,如果方法没有正常返-例如抛出异常,则后置通知不会执行
- 最终通知无论如何都会在目标方法调用过后执行,即使目标方法没有正常的执行完成
- 此外,后置通知可以通过配置得到返回值,而最终通知无法得到
示例:
<!-- 配置切面-->
<aop:config>
<aop:pointcut id="pc01" expression="execution(* cn.zss.service..*(..))"/>
<aop:aspect ref="firstAspect">
<!--最终通知-->
<aop:after method="after" pointcut-ref="pc01"/>
</aop:aspect>
</aop:config>
/*
* 最终通知
* 在目标方法执行之后执行的通知
* 可以接受JointPoint对象,通过此对象可以获取目标对象和目标方法相关的信息
* */
public void after(JoinPoint jp){
// 获取目标对象
Object target = jp.getTarget();
System.out.println(target);
// 获取目标方法
MethodSignature signature = (MethodSignature) jp.getSignature();
System.out.println(signature);
System.out.println("最终通知...");
}
五种通知执行的顺序
- 在一个切面的情况下,执行顺序与配置顺序有关;前置通知和环绕通知的调用目标方法之前的代码在目标方法之前执行,其他在目标方法之后执行
- 如果存在多个切面:
- 多切面执行时,采用了责任链设计模式。
- 切面的配置顺序决定了切面的执行顺序,多个切面执行的过程,类似于方法调用的过程
Spring AOP原理
Spring在创建bean时,除了创建目标对象bean之外,会根据AOP的配置,生成目标对象的代理对象,将其存储,之后获取bean时得到的其实是代理对象,在代理对象中,根据配置的切入点规则,决定哪些方法不处理直接执行目标方法,哪些方法拦截后进行增强,需要增强的方法拦截后根据配置调用指定切面中的指定通知执行增强操作。
Spring自动为目标对象生成代理对象,默认情况下,如果目标对象实现过接口,则采用java的动态代理机制,如果目标对象没有实现过接口,则采用CGLIB动态代理。
开发者可以在Spring中进行配置,要求无论目标对象是否实现过接口,都强制使用CGLIB动态代理。配置方式:<aop:config proxy-target-class="true">
AOP的注解方式实现
spring也支持注解方式实现AOP,相对于配置文件方式,注解配置更加的轻量级,配置、修改更加方便,是目前最流行的方式。
a. 开启AOP的注解配置方式
<!-- 配置注解方式AOP-->
<aop:aspectj-autoproxy/>
b. 应用@Aspect标签将指定的类标志为一个切面
@Component
@Aspect
public class FirstAspect {
// ...
}
c. 配置通知,指定切入点规则
public class FirstAspect {
/**
* 最终通知
*/
@After("execution(* cn.zss.service..*(..))")
public void after(JoinPoint jp){
System.out.println("最终通知..");
}
/**
* 异常通知
*/
@AfterThrowing(value = "execution(* cn.zss.service..*(..))",throwing = "e")
public void afterThrowing(JoinPoint jp, Throwable e){
System.out.println("异常通知.." + e);
}
/**
* 后置通知
*/
@AfterReturning(value = "execution(* cn.zss.service..*(..))",returning = "ro")
public void afterReturning(JoinPoint jp, Object ro){
System.out.println("后置通知.."+ro);
}
/**
* 环绕通知
*/
@Around("execution(* cn.zss.service..*(..))")
public Object around(ProceedingJoinPoint pjp) throws Throwable {
System.out.println("环绕通知前..");
Object retObj = pjp.proceed();//执行目标方法
System.out.println("环绕通知后..");
return retObj;
}
/**
* 前置通知
*/
@Before("execution(* cn.zss.service..*(..))")
public void before(JoinPoint jp){
System.out.println("前置通知..");
}
}
d. 如果一个切面中多个通知重复使用同一个切入点表达式,则可以将该切入点表达式单独定义,后续引用;注意,在当前切面中通过注解定义的切入点只在当前切面中起作用,其他切面看不到。
/**
* 注解方式实现AOP开发
* 引用预定义切入点表达式
* */
@Component
@Aspect
public class SecondAspect {
/**
* 提前定义切入点表达式,绑定到方法名
* 之后可以根据方法名引用此切入点表达式
* */
@Pointcut("execution(* cn.zss.service..*(..))")
public void mx(){}
/**
* 最终通知
*/
@After("mx()")
public void after(JoinPoint jp){
System.out.println("second最终通知..");
}
/**
* 异常通知
*/
@AfterThrowing(value = "mx()",throwing = "e")
public void afterThrowing(JoinPoint jp, Throwable e){
System.out.println("second异常通知.." + e);
}
/**
* 后置通知
*/
@AfterReturning(value = "mx()",returning = "ro")
public void afterReturning(JoinPoint jp, Object ro){
System.out.println("second后置通知.."+ro);
}
/**
* 环绕通知
*/
@Around("mx()")
public Object around(ProceedingJoinPoint pjp) throws Throwable {
System.out.println("second环绕通知前..");
Object retObj = pjp.proceed();//执行目标方法
System.out.println("second环绕通知后..");
return retObj;
}
/**
* 前置通知
*/
@Before("mx()")
public void before(JoinPoint jp){
System.out.println("second前置通知..");
}
}
SpringAOP案例
异常信息收集
在业务方法执行时,如果有异常抛出,则根据异常信息记录日志
/**
* 记录异常信息切面
* */
@Component
@Aspect
public class ExceptionAspect {
@AfterThrowing(value = "execution(* cn.zss.service..*(..))",throwing = "e")
public void afterThrowing(JoinPoint jp, Throwable e){
// 哪个类
Class<?> clz = jp.getTarget().getClass();
// 哪个方法
MethodSignature signature = (MethodSignature) jp.getSignature();
Method m = signature.getMethod();
// 什么异常
String msg = e.getMessage();
// 记录异常
System.out.println("在访问["+clz+"]的["+m+"]方法时抛出了异常["+msg+"]");
}
}
通过AOP进行权限控制
- 通过自定义注解声明业务方法是否需要权限控制
- 通过权限注解上的属性声明需要什么样的权限
- 通过切面拦截业务方法,根据是否需要权限、是否具有权限,控制目标方法的执行
a. 自定义权限注解
public enum Roles {
VISITOR,USER,ADMIN
}
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface PrivAnno {
public Roles[] value() default Roles.ADMIN;
}
b. 通过权限注解上的属性声明需要什么样的权限
@Service
public class UserServiceImpl implements UserService {
@PrivAnno(Roles.VISITOR)
@Override
public void addUser(User user) {
System.out.println("新增用户..");
}
@PrivAnno({Roles.ADMIN,Roles.USER})
@Override
public void updateUser(User user) {
System.out.println("修改用户..");
}
@PrivAnno({Roles.ADMIN})
@Override
public void delUser(int id) {
System.out.println("删除用户..");
}
@Override
public User queryUser(int id) {
System.out.println("查询用户..");
return new User(999,"zs",19,"bj");
}
}
c. 定义切面类,实现环绕通知,检查用户是否具有权限,有权限则执行目标方法,没权限抛出异常
@Component
@Aspect
public class PrivAspect {
// 方法一
// @Around("execution(* cn.zss.service..*(..))")
// public Object around(ProceedingJoinPoint pjp) throws Throwable {
// // 获取当前用户角色
// Roles role = Test01.role;
// // 获取当前方法上的标注的注解,获知当前方法需要的角色
// MethodSignature signature = (MethodSignature) pjp.getSignature();
// // 得到接口的方法
// Method intfMethod = signature.getMethod();
// // 获得实现类的Class
// Class insClz = pjp.getTarget().getClass();
// Method method = insClz.getMethod(intfMethod.getName(),intfMethod.getParameterTypes());
//
// // 开启权限检查
// if (method.isAnnotationPresent(PrivAnno.class)){
// // 当前方法上有注解需要权限
// PrivAnno privAnno = method.getAnnotation(PrivAnno.class);
// Roles[] roles = privAnno.value();
// // 查看是否匹配
// if(Arrays.asList(roles).contains(role)){
// // 匹配,则执行目标方法
// return pjp.proceed();
// }
// else{
// // 不匹配则抛出异常
// throw new RuntimeException("权限不足");
// }
// }
// else{
// // 当前方法上没有注解,不需要权限,直接放行
// return pjp.proceed();
// }
// }
// 方法二(可以直接拦截有对应助解的方法,获取注解)
@Around("execution(* cn.zss.service..*(..)) && @annotation(privAnno)")
/*
* 匹配符合 (* cn.zss.service..*(..))及含有PrivAnno注解的连接点
*/
public Object around(ProceedingJoinPoint pjp, PrivAnno privAnno) throws Throwable {
// 获取当前用户角色
Roles role = Test01.role;
// 获取需要的角色
Roles[] roles = privAnno.value();
//判断角色是否匹配
if(Arrays.asList(roles).contains(role)){
// 匹配,放行
return pjp.proceed();
}else{
// 不匹配,抛出异常
throw new RuntimeException("权限不足");
}
}
}
d. 测试
public class Test01 {
public static Roles role = Roles.VISITOR;
@Test
public void test01(){
ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
UserServlet us = (UserServlet) context.getBean("userServlet");
us.test();
((ClassPathXmlApplicationContext) context).close();
}
}
实现事务控制
- 开发事务注解,通过业务方法上是否有注解来标识方法是否需要事务
- 在切面中通过判断目标方法是否具有事务注解决定是否执行事务
- 通过事务管理器管理事务防止耦合
a. 自定义事务注解
/**
* 控制目标方法是否含有事务的注解
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Trans {
}
b. 在业务层使用注解
@Service
public class UserServiceImpl implements UserService {
@Autowired
private UserDao userDao = null;
@Trans
@Override
public void addUser(User user) {
userDao.insertUser(user);
int i = 1/0;
}
@Trans
@Override
public void updateUser(User user) {
userDao.updateUser(user);
}
@Trans
@Override
public void delUser(int id) {
userDao.delteUser(id);
}
@Override
public User queryUser(int id) {
User user = userDao.queryUser(id);
return user;
}
}
c. 定义TransactionManager类专门用于管理事务(防止Service层使用conn造成耦合)
/**
* 事务管理器
*/
public class TransactionManager {
private static Connection conn =JDBCUtiles.getConn();
private TransactionManager(){}
/**
* 开启事务
*/
public static void startTrans(){
try {
conn.setAutoCommit(false);
} catch (SQLException throwables) {
throwables.printStackTrace();
throw new RuntimeException(throwables);
}
}
/**
* 获取连接
* @return conn
*/
public static Connection getConn(){
return conn;
}
/**
* 提交事务
*/
public static void commitTrans(){
try {
conn.commit();
} catch (SQLException throwables) {
throwables.printStackTrace();
throw new RuntimeException(throwables);
}
}
/**
* 回滚事务
*/
public static void rollbackTrans(){
try {
conn.rollback();
} catch (SQLException throwables) {
throwables.printStackTrace();
throw new RuntimeException(throwables);
}
}
/**
* 释放资源
*/
public static void release(){
JDBCUtiles.release(null,null,conn);
}
}
d. 定义切面类,实现环绕通知,根据目标方法是否有@Trans决定要不要增强事务控制
@Component
@Aspect
public class TransAspect {
@Around("execution(* cn.zss.service..*(..)) && @annotation(trans)")
public Object around(ProceedingJoinPoint pjp,Trans trans) throws Throwable {
try {
TransactionManager.startTrans();
Object retObj = pjp.proceed();
TransactionManager.commitTrans();
return retObj;
} catch (Throwable throwable) {
TransactionManager.rollbackTrans();
throw throwable;
} finally {
TransactionManager.release();
}
}
}