ssm-spring之aop(xml+annotation)
ssm-spring之aop(xml+annotation)
AOP为Aspect Oriented Programming的缩写,意为:面向切面编程,通过预编译方式和运行期间动态代理实现程序功能的统一维护的一种技术。
AOP是OOP的延续,是软件开发中的一个热点,也是Spring框架中的一个重要内容,是函数式编程的一种衍生范型。利用AOP可以对业务逻辑的各个部分进行隔离,
从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率。
主要功能应用场景:日志记录,性能统计,安全控制,事务处理,异常处理等等。将这些场景的代码从业务逻辑代码中划分出来,
通过对这些行为的分离,我们希望可以将它们独立到非指导业务逻辑的方法中,进而改变这些行为的时候不影响业务逻辑的代码。
通俗的理解:依赖注入维度是对象,而aop注入维度是方法。在Spring的应用中,aop主要分为配置和注解两种实现方式,下面举例简单分析:
pom依赖
不管是配置还是注解,都需要在pom文件中添加以下依赖:
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.9.7</version>
</dependency>
配置实现AOP
现在假设有这样的场景:服务登录->进行各种业务场景->服务退出登录。下面模拟这个场景的简单实现:
基础实现:
- 编写service:
- 编写service实现类:
- 添加bean配置:
- 编写测试代码:
- 查看运行结果:
public interface UserService {
void login(String token);
void logout();
}
public class UserServiceImpl implements UserService {
@Override
public void login(String token) {
System.out.println("执行登录:登录成功 token:" + token);
}
@Override
public void logout() {
System.out.println("退出登录");
}
}
<bean id="userService" class="com.zx.demo.spring.aop.UserServiceImpl"/>
public class AopTest {
@Test
public void test() {
ApplicationContext context = new ClassPathXmlApplicationContext("beans.xml");
UserService userService = (UserService) context.getBean("userService");
userService.login("123");
System.out.println("模拟各种业务场景...");
userService.logout();
}
}
简单的场景实现已经完成,现在假设项目经理说,需要添加登录登出的日志搜集看看每天多少用户登录过。现在一些同学可能会想到在登录登出接口中添加日志代码,
这样确实可以实现项目经理的需求,但是如果后期项目经理需求增加,需要在其他复杂业务接口上也添加日志,那我们可能要进行多次编码并且最大的问题是会改动原有的实现代码,
稍不注意一个简单的日志逻辑就可能对原有逻辑产生新的bug。那更好的方法是使用AOP动态的往接口实现中注入日志代码。下面看看如何在不改动上述代码的基础上,
实现日志的织入。
aop注入到指定业务方法:
- 编写日志实现方法:
- aop相关的xml配置,注解上说明了各属性的含义:
- 再次运行上面的测试代码:
public class Log {
public void logBefore() {
System.out.println("注解日志:======业务方法执行前注入======(前)" );
}
public void logAfter() {
System.out.println("注解日志:======业务方法执行后注入======(后)" );
}
}
<bean id="log" class="com.zx.demo.spring.aop.Log"/>
<aop:config>
<aop:aspect ref="log">
<aop:pointcut id="p1" expression="execution(void com.zx.demo.spring.aop.UserService.login(String))"/>
<aop:before method="logBefore" pointcut-ref="p1"/>
<aop:after method="logAfter" pointcut-ref="p1"/>
</aop:aspect>
</aop:config>
两次运行结果对比可以发现,第二次在未改动业务代码实现逻辑的前提下成功完成了aop日志注入。
aop注入到多个业务方法:
- aop相关的xml配置,注解上说明了各属性的含义:
- 第三次运行上面的测试代码:
和上一次运行对比可以发现,这次不仅在登录逻辑前后注入了日志,退出登录也有注入。
aop注入之环绕拦截
前面个的两个注入方法可以在多个业务方法逻辑的前后实现注入日志,before和after通知的局限是无法获取业务参数也无法拦截阻断业务方法逻辑。
如果现在有这样的需求在不改动原有逻辑代码的前提下,将长度小于5的token视为无效token不执行登录逻辑。这里要用到一个新的环绕通知:around
。
下面来看个例子:
- 编写需要注入的验证实现:
- 此对象只能在around通知场景;
- 注入方法中若没有添加此参数对象,将会阻断拦截后续通知和业务方法逻辑;
- 注入方法中若有添加此参数对象,但方法中未调用该对象的proceed()方法,同样会阻断拦截后续通知和业务方法逻辑;
- 从此对象中可以获取到业务方法参数值;
- 要阻断后续通知,必须在aop配置中将around通知配置在其他通知之前以顺序执行,否则无效。
- 编写aop配置:
- token长度>=5的场景测试:
- token长度<5的场景测试:
public class Verify {
public void check(ProceedingJoinPoint joinPoint) throws Throwable {
Object[] args = joinPoint.getArgs();
if (args == null || !(args[0] instanceof String) || args[0].toString().length() < 5) {
System.out.println("登录异常:无效token");
} else {
//1、在环绕通知场景,当且仅当调用此方法,让后续通知和业务逻辑才会继续执行,否则将阻断和拦截后续通知和业务逻辑。
//2、若本方法入口没有添加参数ProceedingJoinPoint,默认阻断拦截后续通知和业务逻辑。
joinPoint.proceed();
}
}
}
关于ProceedingJoinPoint对象需要注意:
public class AopTest {
@Test
public void test() {
ApplicationContext context = new ClassPathXmlApplicationContext("beans.xml");
UserService userService = (UserService) context.getBean("userService");
userService.login("12345");
}
}
此场景会走到check方法的else逻辑中,在执行完joinPoint.proceed()后,业务方法可正常登录:
public class AopTest {
@Test
public void test() {
ApplicationContext context = new ClassPathXmlApplicationContext("beans.xml");
UserService userService = (UserService) context.getBean("userService");
userService.login("123");
}
}
此场景没有执行joinPoint.proceed(),因此登录业务逻辑将会被拦截阻断:
注解注入注意点:
- 如果配置了多个切面,则按照顺序依次执行;
- 环绕通知只会阻断配置在其后方的通知,其前方的通知会正常注入。
纯注解实现AOP
上面已经在不改动业务逻辑代码的前提下,通过配置的方式实现了代码注入。除了使用配置实现AOP,还可以采用更简单的注解方式。
- 在spring配置文件中,添加一行配置,使aop支持注解:
- 编写切面类:
- 运行测试代码:
<aop:aspectj-autoproxy/>
//@Aspect:切面注解
@Aspect
public class AnnotationLog {
//@Before:前置通知,同配置-aop:before
@Before("execution(* com.zx.demo.spring..*(..))")
public void logBefore() {
System.out.println("日志:操作前");
}
//@logAfter:前置通知,同配置-aop:after
@After("execution(* com.zx.demo.spring..*(..))")
public void logAfter() {
System.out.println("日志:操作后");
}
//@Around:前置通知,同配置-aop:around
@Around("execution(* com.zx.demo.spring..*(..))")
public void logAround(ProceedingJoinPoint joinPoint) throws Throwable {
System.out.println("获取到业务参数:" + joinPoint.getArgs()[0]);
System.out.println("环绕通知开始");
joinPoint.proceed();
System.out.println("环绕通知结束");
}
}
可以看到注解更加简单的实现了切面注入,并且注解通知的执行顺序为:环绕通知->前置通知->业务方法->后置通知。
配置和注解的方式都可以实现切面代码注入,到了spring boot后,更多的是使用简介的注解方式。