先理解一下这三个缩写意义
AOP(Aspect Oriented Programming 面向切面编程)
IoC(Inversion of Control 控制反转)
DI(Dependency Injection 依赖注入)
1. AOP
1.1 概念分析
AOP:面向切面编程,通过预编译方式和运行期动态代理实现程序功能的统一维护的一种技术。而@Aspect 就是把一个类定义为切面供容器读取。
AOP相关概念,为了更好的理解AOP,我们有必要先了解AOP的相关术语。
切面(Aspect)
横切关注点的模块化(跨越应用程序多个模块的功能,比如 日志功能),这个关注点实现可能另外横切多个对象。切点(Pointcut)
指定一个通知将被引发的一系列连接点的集合。AOP 通过切点定位到特定的连接点。切点和连接点不是一对一的关系,一个切点匹配多个连接点,切点通过 org.springframework.aop.Pointcut 接口进行描述,它使用类和方法作为连接点的查询条件。每个类都拥有多个连接点,例如 ArithmethicCalculator类的所有方法实际上都是连接点。连接点(Join point)
连接点是在应用执行过程中能够插入切面的一个点。这个点可以是类的某个方法调用前、调用后、方法抛出异常后等。切面代码可以利用这些点插入到应用的正常流程之中,并添加行为。通知(Advice)
在特定的连接点,AOP框架执行的动作。
Spring AOP 提供了5种类型的通知:
前置通知(Before):在目标方法被调用之前调用通知功能。
后置通知(After):在目标方法完成之后调用通知,无论该方法是否发生异常。
后置返回通知(After-returning):在目标方法成功(没有异常)执行之后调用通知。
后置异常通知(After-throwing):在目标方法抛出异常后调用通知。
环绕通知(Around):通知包裹了被通知的方法,在被通知的方法调用之前和调用之后执行自定义的行为。引入(Introduction)
添加方法或字段到被通知的类。 Spring允许引入新的接口到任何被通知的对象。例如,你可以使用一个引入使任何对象实现 IsModified接口,来简化缓存。Spring中要使用Introduction, 可有通过DelegatingIntroductionInterceptor来实现通知,通过DefaultIntroductionAdvisor来配置Advice和代理类要实现的接口目标对象(Target Object)
包含连接点的对象。也被称作被通知或被代理对象。AOP代理(AOP Proxy)
AOP框架创建的对象,包含通知。 在Spring中,AOP代理可以是JDK动态代理或者CGLIB代理。织入(Weaving)
织入描述的是把切面应用到目标对象来创建新的代理对象的过程。 Spring AOP 的切面是在运行时被织入,原理是使用了动态代理技术。Spring支持两种方式生成代理对象:JDK动态代理和CGLIB,默认的策略是如果目标类是接口,则使用JDK动态代理技术,否则使用CGLIB来生成代理。
**1.2 定义分析
表达式标签
- execution():用于匹配方法执行的连接点
- this(): 用于匹配当前AOP代理对象类型的执行方法;注意是AOP代理对象的类型匹配,这样就可能包括引入接口也类型匹配
- @annotation:用于匹配当前执行方法持有指定注解的方法
- args(): 用于匹配当前执行的方法传入的参数为指定类型的执行方法
- @args():于匹配当前执行的方法传入的参数持有指定注解的执行
- within(): 用于匹配指定类型内的方法执行
- @within():用于匹配所以持有指定注解类型内的方法
- target(): 用于匹配当前目标对象类型的执行方法;注意是目标对象的类型匹配,这样就不包括引入接口也类型匹配
- @target():用于匹配当前目标对象类型的执行方法,其中目标对象持有指定的注解
其中execution是用的最多的
execution格式:
execution(modifiers-pattern? ret-type-pattern declaring-type-pattern? name-pattern(param-pattern)throws-pattern?)
其中带 ? 号的 modifiers-pattern?,declaring-type-pattern?,hrows-pattern?是可选项
ret-type-pattern,name-pattern,parameters-pattern 是必选项;
- modifier-pattern? 修饰符匹配,如 public 表示匹配公有方法
- ret-type-pattern 返回值匹配,* 表示任何返回值,全路径的类名等
- declaring-type-pattern? 类路径匹配
- name-pattern 方法名匹配,* 代表所有,set* 代表以 set 开头的所有方法
- (param-pattern) 参数匹配,指定方法参数(声明的类型)
(..) 代表所有参数, () 代表一个参数, (,String) 代表第一个参数为任何值,第二个为String类型
- throws-pattern? 异常类型匹配
AspectJ类型匹配的通配符:
- 符号 * 匹配任何数量字符
- 符号 . . 匹配任何数量字符的重复,如在类型模式中匹配任何数量子包;而在方法参数模式中匹配任何数量参数
- 符号 + 匹配指定类型的子类型;仅能作为后缀放在类型模式后边
如:
- java.lang.String 匹配 String类型
- java.*.String 匹配 java 包下的任何一级子包下的 String 类型;如匹配 java.lang.String,但不匹配 java.lang.sss.String
- java..* 匹配 java 包及任何子包下的任何类型; 如匹配 java.lang.String、java.lang.annotation.Annotation
- java.lang.*ing 匹配任何 java.lang 包下的以 ing 结尾的类型;
- java.lang.Number+ 匹配 java.lang 包下的任何 Number 的自类型;如匹配 java.lang.Integer,也匹配 java.math.BigInteger
execution
execution:使用 execution(方法表达式) 匹配方法执行
- execution(public * *(..)) 定义任意公共方法的执行
- execution(* set*(..)) 定义任何一个以 set 开始的方法的执行
- execution(* com.point.user.service.AdminService.*(..)) 定义 AdminService 接口的任意方法的执行
- execution(* com.point.user.service.*.*(..)) 定义在 service 包里的任意方法的执行
- execution(* com.point.user.service..*.*(..)) 定义在 service 包和所有子包里的任意类的任意方法的执行
- execution(* com.point.user.aspect…AdminAspectTest.*(…)) 定义在aspect 包和所有子包里的 AdminAspectTest 类的任意方法的执行:
说明:最靠近(..)的为方法名,靠近.∗(..))的为类名或者接口名
this
this:使用 this (类型全限定名) 匹配当前AOP代理对象类型的执行方法;注意是AOP代理对象的类型匹配,这样就可能包括引入接口方法也可以匹配;注意this中使用的表达式必须是类型全限定名,不支持通配符
this(com.point.user.aspect.Interface) 实现了 Interface 接口的所有类,如果 Interface 不是接口,限定 Interface 单个类.
@annotation
@annotation:使用 @annotation(注解类型) 匹配当前执行方法持有指定注解的方法;注解类型也必须是全限定类型名
@annotation(org.springframework.transaction.annotation.Transactional) 带有@Transactional 标注的任意方法
args 和 @args
args:使用 args(参数类型列表) 匹配当前执行的方法传入的参数为指定类型的执行方法;注意是匹配传入的参数类型,不是匹配方法签名的参数类型;参数类型列表中的参数必须是类型全限定名,通配符不支持;args属于动态切入点,这种切入点开销非常大,非特殊情况最好不要使用;
args(String) 参数为 String 类型(运行是决定)的方法
@target:使用 @target(注解类型) 匹配当前目标对象类型的执行方法,其中目标对象持有指定的注解;注解类型也必须是全限定类型名;
@args(org.springframework.transaction.annotation.Transactional) 参数带有@Transactional 标注的方法
within 和 @within
within:使用 within(类型表达式) 匹配指定类型内的方法执行
within(com.point.user.aspect.*) aspect 包里的任意类
within(com.point.user.aspect…*) aspect 包和所有子包里的任意类
@within:使用 @within(注解类型) 匹配所以持有指定注解类型内的方法,注解类型也必须是全限定类型名
@within(org.springframework.transaction.annotation.Transactional) 带有 @Transactional 标注的所有类的任意方法
target 和 @target
target:使用 target(类型全限定名) 匹配当前目标对象类型的执行方法;注意是目标对象的类型匹配,这样就不包括引入接口也类型匹配;注意target中使用的表达式必须是类型全限定名,不支持通配符
target(com.point.user.service.AdminService) 定义 AccountService 接口的任意方法的执行
@target:使用 @target(注解类型) 匹配当前目标对象类型的执行方法,其中目标对象持有指定的注解,注解类型也必须是全限定类型名
@target(org.springframework.transaction.annotation.Transactional) 带有@Transactional** 标注的所有类的任意方法
注意:
- @within和@target针对类的注解,@annotation是针对方法的注解,@args是针对参数的注解
- 当一个实现了接口的类被AOP的时候,用getBean方法必须cast为接口类型,不能为该类的类型
1.3 技术点实践
package com.point.user.aspect;
import cn.hutool.json.JSONUtil;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.*;
import org.aspectj.lang.reflect.SourceLocation;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
@Slf4j
@Aspect //这个类需要使用@Aspect进行标注, 也就是所谓的切面
@Component
public class AdminAspectTest {
private final String POINT_CUT = "execution(* com.point.user.controller.AdminController.*(..))";
/**
* 注意这个方法是要空方法体的,如果不是虽然不会报错,但是注解会有提示
*
* @Pointcut(value="切入点表达式", argNames="参数名列表")
*/
@Pointcut(POINT_CUT)
public void pointCut() {
}
/**
* 前置通知
*
* @Before(value="切入点表达式或命名切入点", argNames="参数列表参数名")
*/
@Before("pointCut()")
public void before(JoinPoint joinPoint) {
log.info("@Before 在目标方法被调用之前调用通知功能, 参数 joinPoint{}", JSONUtil.toJsonStr(joinPoint));
//获取目标方法参数信息
Object[] args = joinPoint.getArgs();
//aop代理对象
Object aThis = joinPoint.getThis();
//被代理对象
Object target = joinPoint.getTarget();
//获取连接点的方法签名对象
Signature signature = joinPoint.getSignature();
//连接点类型
String kind = joinPoint.getKind();
//返回连接点方法所在类文件中的位置 打印报异常
SourceLocation sourceLocation = joinPoint.getSourceLocation();
///返回连接点静态部分
JoinPoint.StaticPart staticPart = joinPoint.getStaticPart();
//attributes可以获取request信息 session信息等
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = attributes.getRequest();
}
/**
* 后置通知
*
* @After(value="切入点表达式或命名切入点", argNames="参数列表参数名")
*/
@After(POINT_CUT)
public void after(JoinPoint joinPoint) {
log.info("@After 在目标方法完成之后调用通知,无论该方法是否发生异常, 参数 joinPoint{}", JSONUtil.toJsonStr(joinPoint));
}
/**
* 后置返回通知
* 如果第一个参数为JoinPoint,则第二个参数为返回值的信息
* 如果第一个参数不为JoinPoint,则第一个参数为returning中对应的参数
* returning:限定了只有目标方法返回值与通知方法参数类型匹配时才能执行后置返回通知,否则不执行
* 参数为Object类型将匹配任何目标返回值
*
* @AfterReturning(value="切入点表达式或命名切入点", pointcut="切入点表达式或命名切入点", argNames="参数列表参数名", returning="返回值对应参数名")
*/
@AfterReturning(value = "pointCut()", returning = "result")
public void afterReturning(JoinPoint joinPoint, Object result) {
log.info("@AfterReturning 在目标方法成功执行之后调用通知, 方法发生异常则不通知, 参数 joinPoint{} result{}", JSONUtil.toJsonStr(joinPoint), JSONUtil.toJsonStr(result));
}
/**
* 后置异常通知
* 定义一个名字,该名字用于匹配通知实现方法的一个参数名,当目标方法抛出异常返回后,将把目标方法抛出的异常传给通知方法
* throwing:限定了只有目标方法抛出的异常与通知方法相应参数异常类型时才能执行后置异常通知,否则不执行
* 对于throwing对应的通知方法参数为Throwable类型将匹配任何异常
* 注意:如果throwing参数不存在也会匹配任何异常
*
* @AfterThrowing(value="切入点表达式或命名切入点", pointcut="切入点表达式或命名切入点", argNames="参数列表参数名", throwing="异常对应参数名")
*/
@AfterThrowing(value = "pointCut()", throwing = "exception")
public void afterThrowing(JoinPoint joinPoint, Throwable exception) {
log.info("@AfterThrowing 在目标方法抛出异常后调用通知, 参数 joinPoint{} exception {}", JSONUtil.toJsonStr(joinPoint), JSONUtil.toJsonStr(exception));
}
/**
* 环绕通知:
* 环绕通知非常强大,可以决定目标方法是否执行,什么时候执行,执行时是否需要替换方法参数,执行完毕是否需要替换返回值
* 环绕通知第一个参数必须是org.aspectj.lang.ProceedingJoinPoint类型
*
* @Around(value="切入点表达式或命名切入点", argNames="参数列表参数名")
*/
@Around("pointCut()")
public Object around(ProceedingJoinPoint proceedingJoinPoint) {
log.info("@Around 通知包裹了被通知的方法,在被通知的方法调用之前和调用之后执行自定义的行为, 参数 proceedingJoinPoint{}", JSONUtil.toJsonStr(proceedingJoinPoint));
//org.aspectj.lang.ProceedingJoinPoint: 为JoinPoint的子类,多了两个方法
//proceed() 调用下一个advice或者执行目标方法,返回值为目标方法返回值,因此可以通过更改返回值,修改方法的返回值
//proceed(Object[] args) 参数为目标方法的参数 因此可以通过修改参数改变方法入参
//注意一定要执行 proceed()/proceed(Object[] args) 才会执行被通知的方法
Object result = null;
try {
result = proceedingJoinPoint.proceed();
} catch (Throwable e) {
//捕获与否都不会影响到 @AfterThrowing 后置异常通知
log.error("@Around 被通知的方法异常: ", e);
}
log.info("@Around 通知包裹了被通知的方法,在被通知的方法调用之前和调用之后执行自定义的行为");
return result;
}
}
2. IOC/DI
2.1 概念分析
控制反转(IOC)和依赖注入(DI)是同一个概念,目的在于降低系统耦合,将类的实例化工作交给Spring代理,主要用到的设计模式为工厂模式,通过Java反射机制实现类的自动注入。
IoC(Inverse of Control:控制反转)是⼀种设计思想,就是 将原本在程序中⼿动创建对象的控制 权,交由Spring框架来管理。 IoC 在其他语⾔中也有应⽤,并⾮ Spring 特有。 IoC 容器是 Spring ⽤来实现 IoC 的载体, IoC 容器实际上就是个Map(key,value),Map 中存放的是各 种对象。
将对象之间的相互依赖关系交给 IoC 容器来管理,并由 IoC 容器完成对象的注⼊。这样可以很⼤ 程度上简化应⽤的开发,把应⽤从复杂的依赖关系中解放出来。 IoC 容器就像是⼀个⼯⼚⼀ 样,当我们需要创建⼀个对象的时候,只需要配置好配置⽂件/注解即可,完全不⽤考虑对象是如 何被创建出来的。
2.2 技术点实践:DI 的三种注入方式
- 属性注入
@Autowired
private RedisTemplate redisTemplate;
@Resource
private RedisTemplate redisTemplate;
- set 方法注入
private RedisTemplate redisTemplate;
@Autowired
public void setRedisTemplate(RedisTemplate redisTemplate) {
this.redisTemplate = redisTemplate;
}
private RedisTemplate redisTemplate;
@Resource
public void setRedisTemplate(RedisTemplate redisTemplate) {
this.redisTemplate = redisTemplate;
}
2.2.3 构造方法注入(@Resource 不支持构造方法注入)
private RedisTemplate redisTemplate;
@Autowired //Spring4.3+之后,constructor注入 支持非显示注入方式,也就是说可以不写这个注解
public AdminServiceImpl(RedisTemplate redisTemplate) {
this.redisTemplate = redisTemplate;
}