12. Spring高级-注解驱动AOP开发入门
1、写在最前
a.Spring的aop是基于ioc的。所以需要有spring的ioc基础。(本篇内容不对ioc进行讲解)
b.本章节我们只是对aop的使用做基本功能展示,目的是为了以此讲解aop中的注解和执行原 理分析。
2、注解驱动入门案例介绍
需求:
实现在执行service方法时输出执行日志。(除了业务层外,表现层和持久层也可以实现)
public class User implements Serializable {
private String id;
private String username;
private String password;
private String email;
private Date birthday;
private String gender;
private String mobile;
private String nickname;
}
/*** 业务层接口: */
public interface UserService {
/*** 保存用户 * @param user */
void save(User user);
} :
/*** 业务层实现类 */
@Service("userService")
public class UserServiceImpl implements UserService {
@Override
public void save(User user) {
System.out.println("保存用:" + user);
}
}
/*** 记录日志的工具类 */
@Component
@Aspect
public class LogUtil {
/*** 通用切入点表达式 */
@Pointcut("execution(* com.itheima.service.impl.*.*(..))")
private void pt1() {
}
/*** 前置通知 */
@Before("pt1()")
public void beforeLog() {
System.out.println("执行切入点方法前记录日志");
}
/*** 后置通知 */
@AfterReturning("pt1()")
public void afterReturningLog() {
System.out.println("正常执行切入点方法后记录日志");
}
/*** 异常通知 */
@AfterThrowing("pt1()")
public void afterThrowingLog() {
System.out.println("执行切入点方法产生异常后记录日志");
}
/*** 最终通知 */
@After("pt1()")
public void afterLog() {
System.out.println("无论切入点方法执行是否有异常都记录日志");
}
/*** 环绕通知 */
@Around("pt1()")
public Object arountPrintLog(ProceedingJoinPoint pjp) {
//1.定义返回值 Object rtValue = null;
try {
//前置通知
System.out.println("执行切入点方法前记录日志");
// 2.获取方法执行所需的参数
Object[] args = pjp.getArgs();
// 3.执行切入点方法
rtValue = pjp.proceed(args);
// 后置通知 System.out.println("正常执行切入点方法后记录日志");
}
catch (Throwable t) {
// 异常通知 System.out.println("执行切入点方法产生异常后记录日志");
}
finally { //最终通知
System.out.println("无论切入点方法执行是否有异常都记录日志");
}
return rtValue;
}
}
// 配置类:
/*** 配置类 */
@Configuration
@ComponentScan("com.itheima")
@EnableAspectJAutoProxy
public class SpringConfiguration {
}
/**
* spring的aop环境准备 测试类
*/
public class SpringAOPTest { public static void main(String[] args) {
//1.获取容器
AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(SpringConfiguration.class);
// 2.获取bean对象
UserService userService = ac.getBean("userService",UserService.class);
// 3.准备数据
User user = new User();
user.setId("1");
user.setUsername("test");
user.setNickname("泰斯特");
// 4.执行方法
userService.save(user);
}
}
3. AOP常用注解分析
@EnableAspectJAutoProxy
用于开启注解AOP支持的
作用:
表示开启spring对注解aop的支持。
它有两个属性,分别是指定采用的代理方式和是否暴露代理对象,通过AopContext可以进行访问。从定义可以看得出,它引入
AspectJAutoProxyRegister.class对象,该对象是基于注解@EnableAspectJAutoProxy注册一个AnnotationAwareAspectJAutoProxyCreator,该对象通过调用
AopConfigUtils.registerAspectJAnnotationAutoProxyCreatorIfNecessary(regist
ry);
注册一个aop代理对象生成器。关于AnnotationAwareAspectJAutoProxyCreator请参考第五章第二小节《
AnnotationAwareAspectJAutoProxyCreator对象的分析》 属性:
proxyTargetClass: 指定是否采用cglib进行代理。默认值是false,表示使用jdk的代理。
exposeProxy: 指定是否暴露代理对象,通过AopContext可以进行访问。
使用场景:
当我们注解驱动开发时,在需要使用aop实现某些功能的情况下,都需要用到此注解。
示例 :
/*** @author 黑马程序员 * */
@Configuration
@ComponentScan("com.itheima")
@EnableAspectJAutoProxy public class SpringConfiguration { }
@Aspect
用于配置切面的
说明 :
作用: 声明当前类是一个切面类。
属性: value: 默认我们的切面类应该为单例的。但是当切面类为一个多例类时,指定预 处理的切入点表达式。用法是perthis(切入点表达式)。
它支持指定切入点表达式,或者是用@Pointcut修饰的方法名称(要求全 限定方法名)
使用场景:此注解也是一个注解驱动开发aop的必备注解。
实例:
/*** 记录日志的工具类 */
@Component
@Scope("prototype")//注意:通常情况下我们的切面类是不需要多例的。
@Aspect(value = "execution(* com.itheima.service.impl.*.*(..))")
public class LogUtil {
/*** 用于配置当前方法是一个前置通知 */
@Before("execution(* com.itheima.service.impl.*.*(..))")
public void printLog() {
System.out.println("执行打印日志的功能");
}
}
**@Pointcut **
用于配置切入点表达式的
作用: 此注解是用于指定切入点表达式的。
属性: value: 用于指定切入点表达式。表达式的配置详解请参考第五章节第三小节《切 入点表达式的写法》
argNames:用于指定切入点表达式的参数。参数可以是execution中的,也可以是 args中的。通常情况下不使用此属性也可以获得切入点方法参数。
使用场景:在实际开发中,当我们的多个通知需要执行,同时增强的规则确定的情况下,就可 以把切入点表达式通用化。
此注解就是代替xml中的<aop:pointcut>标签,实现切入点表达 式的通用化。
示例
@Component
@Aspect
public class LogUtil {
/*** 通用切入点表达式
* 在value属性的中使用了&&符号,表示并且的关系。
* &&符号后面的args和execution一样,都是切入点表达式支持的关键字,表示匹配 参数。指定的内容
* 可以是全限定类名,或者是名称。当指定参数名称时,要求与方法中形参名称相 同。
* argNames属性,是定义参数的名称,该名称必须和args关键字中的名称一致。
*/
@Pointcut(value = "execution(* com.itheima.service.impl.*.* (com.itheima.domain.User))&& args(user)",argNames = "user")
private void pt1(User user){
}
}
@Before
用于前置通知的
作用: 被此注解修饰的方法为前置通知。前置通知的执行时间点是在切入点方法执行之前。
属性: value: 用于指定切入点表达式。可以是表达式,也可以是表达式的引用。
argNames:用于指定切入点表达式参数的名称。
它要求和切入点表达式中的参数名称 一致。
通常不指定也可以获取切入点方法的参数内容。
使用场景:在实际开发中,我们需要对切入点方法执行之前进行增强, 此时就用到了前置通 知。
在通知(增强的方法)中需要获取切入点方法中的参数进行处理时,就要配合切入点表达 式参数来使用。
/*** 前置通知 */
@Before(value = "pt1(user)",argNames = "user")
public void beforeLog(User user){
System.out.println("执行切入点方法前记录日志"+user);
}
@AfterReturning
作用:
用于配置后置通知。后置通知的执行是在切入点方法正常执行之后执行。 需要注意的是,由于基于注解的配置时,spring创建通知方法的拦截器链时,后置通知在最终通知之后,所以会先执行@After注解修饰的方法。
属性:
value: 用于指定切入点表达式,可以是表达式,也可以是表达式的引用。
pointcut:它的作用和value是一样的。
returning:指定切入点方法返回值的变量名称。它必须和切入点方法返回值名称一致。
argNames: 用于指定切入点表达式参数的名称。它要求和切入点表达式中的参数名称 一致。通常不指定也可以获取切入点方法的参数内容。
使用场景:
此注解是用于配置后置增强切入点方法的。被此注解修饰方法会在切入点方法正常 执行之后执行。在我们实际开发中,像提交事务,记录访问日志,统计方法执行效率等等都可以利用后置通知实现。
示例
// 切入点方法:
@Override
public User findById(String id) {
System.out.println("切入点方法开始执行。。。");
User user = new User();
user.setId(id);
user.setUsername("heima");
user.setNickname("黑马小王子");
return user;
}
/*** 后置通知 */
@AfterReturning(value = "execution(* com.itheima.service.impl.*.* (..))&&args(param)", returning = "user")
public void afterReturningLog(String param, Object user) {
System.out.println("正常执行切入点方法后记录日志,切入点方法的参数 是:" + param);
System.out.println("正常执行切入点方法后记录日志,切入点方法的返回值 是:" + user);
}
@AfterThrowing
说明:
作用: 用于配置异常通知。
属性:
value: 用于指定切入点表达式,可以是表达式,也可以是表达式的引用。
pointcut:它的作用和value是一样的。
throwing:指定切入点方法执行产生异常时的异常对象变量名称。它必须和异常变量 名称一致。
argNames:用于指定切入点表达式参数的名称。它要求和切入点表达式中的参数名称 一致。通常不指定也可以获取切入点方法的参数内容。
使用场景:
用此注解修饰的方法执行时机是在切入点方法执行产生异常之后执行。
示例
//切入点方法:
@Override
public User findById(String id) {
System.out.println("切入点方法开始执行。。。");
User user = new User();
user.setId(id);
user.setUsername("heima");
user.setNickname("黑马小王子");
int i = 1 / 0;
return user;
}
/**
* 异常通知
*/
@AfterThrowing(value = "execution(* com.itheima.service.impl.*.* (..))&&args(param)", throwing = "e")
public void afterThrowingLog(String param, Throwable e) {
System.out.println("执行切入点方法产生异常后记录日志,切入点方法的参数 是:" + param);
System.out.println("执行切入点方法产生异常后记录日志,切入点方法的异常 是:" + e);
}
@After
作用: 用于指定最终通知。
属性:
value: 用于指定切入点表达式,可以是表达式,也可以是表达式的引用。
argNames:
用于指定切入点表达式参数的名称。它要求和切入点表达式中的参数名称
一致。通常不指定也可以获取切入点方法的参数内容。
使用场景:
最终通知的执行时机,是在切入点方法执行完成之后执行,无论切入点方法执行是否产生异常最终通知都会执行。所以被此注解修饰的方法,通常都是做一些清理操作。
@DeclareParents
作用: 用于给被增强的类提供新的方法。(实现新的接口)
属性:
value: 用于指定目标类型的表达式。当在全限定类名后面跟上+时,表示当前类及其子类
defaultImpl: 指定提供方法或者字段的默认实现类。
使用场景:
当我们已经完成了一个项目的某个阶段开发,此时需要对已完成的某个类加入一些 新的方法,我们首先想到的是写一个接口,然后让这些需要方法的类实现此接口,但是如果目标类非常复杂,牵一发而动全身,改动的话可能非常麻烦。此时就可以使用此注解,然后建一个代理类,同时代理该类和目标类。
@EnableLoadTimeWeaving
作用: 用于切换不同场景下实现增强。
属性:
aspectjWeaving:是否开启LTW的支持。
ENABLED 开启LTW
DISABLED 不开启LTW
AUTODETECT 如果类路径下能读取到META‐INF/aop.xml文件,则开启LTW,否则关闭
使用场景:
在Java 语言中,从织入切面的方式上来看,存在三种织入方式:编译期织入、类 加载期织入和运行期织入。编译期织入是指在Java编译期,采用特殊的编译器,将切面织入 到Java类中;而类加载期织入则指通过特殊的类加载器,在类字节码加载到JVM时,织入切 面;运行期织入则是采用CGLib工具或JDK动态代理进行切面的织入。 AspectJ提供了两种切面织入方式,第一种通过特殊编译器,在编译期,将AspectJ 语言编写的切面类织入到Java类中,可以通过一个Ant或Maven任务来完成这个操作;第二种 方式是类加载期织入,也简称为LTW(Load Time Weaving)