一、spring AOP的应用场景
1、spring AOP s是什么? (what)
AOP 面向切面编程,其通过预编译方式和运行期动态代理实现在不修改源代码的情况下给程序动态统一添加功能的一种技术。其核心使用了java的动态代理来实现的,一定程度上做到了业务和系统级别服务的解耦
2、spring AOP 可以用来做什么 ()
日志记录,性能统计,安全控制,事务处理,异常处理等通用服务 可以通过AOP横切进入业务逻辑,这样做在实际的开发中将功能性业务和一些非功能的通用服务分离,解耦,需求变更的时候,如果是通用服务逻辑变更,只需要开发日志等通用服务模块的开发人员进行更改(通用服务代码不会和业务代码耦合在一起),业务上的需求变更只需更改业务逻辑即可。
二、spring AOP的相关术语
1、 AOP中的相关术语
- Aspect,切面,一个关注点的模块。包含有具体实现的切面类。
- JoinPoint, 连接点,程序执行中的某个点,某个位置。
- PointCut,切点,切面匹配连接点的点,一般与切点表达式相关,就是切面如何切点。 例子中,@PointCut注解就是切点表达式,匹配对应的连接点后连接点即变为了切点。
- Advice,通知,指在切面的某个特定的连接点上执行的动作。 例子中,before()与after()方法中的代码。
- TargetObject,目标对象,指被切入的对象。 例子中,从ctx中取出的testBean则是目标对象。
- Weave,织入,将Advice作用在JoinPoint的过程。
2、spring AOP的五种通知
- @Before,前置通知,执行方法前执行
- @AfterReturn,返回通知,正常返回方法后执行
- @After,后置通知,方法最终结束后执行,相当于finaly
- @Around,环绕通知,围绕整个方法
- @AfterThrowing,异常通知,抛出异常后执行
三、spring AOP的相关demo
使用两种形式实现Spring的AOP 第一个是使用切入点表达式,第二种使用注解的形式,个人喜欢使用注解的形式因为注解灵活且便于理解
1、使用切入点表达式来实现
1.1、切面类aspect
/**
* 切面打印服务层日志信息
*/
@Aspect
@Component
public class LogInfoAspect {
private static final Logger logger = LoggerFactory.getLogger(LogInfoAspect.class);
//定义一个切入点表达式
@Pointcut("execution(* com.xiu.sb.aopTest.service.*.*(..))")
public void logInfo() {
}
//在进入切点业务代码未执行之前执行
@Before("logInfo()")
public void logBefore(JoinPoint pjp){
MethodSignature methodSignature = ( MethodSignature) pjp.getSignature();
String methodName = methodSignature.getMethod().getName();
String className = pjp.getTarget().getClass().getName();
Object[] argsInfo = pjp.getArgs();
logger.info("日志打印开始 class info : {}, method info : {}, args info: {}",className,methodName, JsonUtil.obj2str(argsInfo));
}
//在执行完切点后执行
@After("logInfo()")
public void logAfter(JoinPoint pjp){
MethodSignature methodSignature = ( MethodSignature) pjp.getSignature();
String methodName = methodSignature.getMethod().getName();
String className = pjp.getTarget().getClass().getName();
Object[] argsInfo = pjp.getArgs();
logger.info("日志打印结束 class info : {}, method info : {}, args info: {}",className,methodName, JsonUtil.obj2str(argsInfo));
}
}
切入点表达式为com.xiu.sb.aopTest.service 该包下的所有类的所有方法都会被织入日志打印的功能增强,无法灵活可控的进行处理。
2、使用注解来实现
2.1、切面类的代码和上面的代码比较相似
/**
* 切面打印服务层日志信息 使用注解的形式
*/
@Aspect
@Component
public class LogInfoAnnoAspect {
private static final Logger logger = LoggerFactory.getLogger(LogInfoAnnoAspect.class);
// 后期 切入点 可以为指定的注解(比如时间花费注解,日志注解等)
//需要实现一个相关的注解
@Pointcut("@annotation(com.xiu.sb.aopTest.annonation.LogInfoAnnotation)")
public void logAnonation() {}
@Before("logAnonation()")
public void logBefore(JoinPoint pjp){
MethodSignature methodSignature = ( MethodSignature) pjp.getSignature();
String methodName = methodSignature.getMethod().getName();
String className = pjp.getTarget().getClass().getName();
Object[] argsInfo = pjp.getArgs();
logger.info("日志打印开始 class info : {}, method info : {}, args info: {}",className,methodName, JsonUtil.obj2str(argsInfo));
}
@After("logAnonation()")
public void logAfter(JoinPoint pjp){
MethodSignature methodSignature = ( MethodSignature) pjp.getSignature();
String methodName = methodSignature.getMethod().getName();
String className = pjp.getTarget().getClass().getName();
Object[] argsInfo = pjp.getArgs();
logger.info("日志打印结束 class info : {}, method info : {}, args info: {}",className,methodName, JsonUtil.obj2str(argsInfo));
}
}
2.2、注解形式实现的注解:定义一个打印日志的注解 LogInfoAnnotation类
/**
* author Administrator
* date 2018/9/17
*/
@RestController
//修饰类,则类中的所有方法均会织入打印日志的功能
@LogInfoAnnotation
public class StudentService extends BaseController{
@Autowired
private StudentService sudentService;
@RequestMapping("/demo")
//不放置到类上仅仅放置到方法上,只有当前被注解修饰的方法会织入日志打印功能
@LogInfoAnnotation
public ResponseDto<Student> studentList(){
List<Student> students = new ArrayList<>();
for(int i =0;i <5; i++){
Student student = new Student("student"+i,"man",24);
students.add(student);
}
Map<String,Object> data = new HashMap();
data.put("studentList",students);
return success(data);
}
@RequestMapping("/logInfo")
public ResponseDto<Student> studentAll(){
List<Student> students = sudentService.getStuList();
Map<String,Object> data = new HashMap();
data.put("studentList",students);
return success(data);
}
@RequestMapping("/logInfo2")
public ResponseDto<Student> findAloneStudent(){
Student student = sudentService.findStudent(0);
Map<String,Object> data = new HashMap();
data.put("studentList",student);
return success(data);
}
}
/**
* author xieqx
* createTime 2018/7/20
* desc 记录花费时间注解
*/
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface LogInfoAnnotation {
String value() default "";
}
2.3、注解形式的使用
注解之所以比切入点表达式灵活的原因在于,例子中的切入点表达式会对指定包下的所有方法起作用,如果需要精确配置,需要设置特定的切入点表达式来进行精确匹配,不够灵活,使用注解则不然,我们可以将注解放置到我们需要起作用的方法上,如果需要在类的所有方法上起作用,只需使用注解来修饰类,例子如下所示:
@Service
public class StudentService {
private static List<Student> students = new ArrayList<>();
static {
students.add(new Student("xieqx","man",24));
students.add(new Student("xieqx2","man",25));
students.add(new Student("xieqx3","man",26));
}
//只有当前被注解修饰的方法会织入日志打印功能
@LogInfoAnnotation
public List<Student> getStuList(){
return students;
}
public Student findStudent(Integer index){
return students.get(index);
}
}
四、spring的5种通知
1、前面的例子中已经提到了两种通知,一种是前置通知,另一种是后置通知,所谓前置通知是指在执行切入点方法之前被执行所谓后置通知是执行切入点方法之后被执行,这两种通知都比较简单,此处不造叙述。这里主要着重指出后三种通知
@Around 环绕通知,@AfterReturn,返回通知,@AfterThrow 异常通知
2、@Around 使用环绕通知实现一个输出每一个接口的执行时间
首先定义切面类(这里直接使用注解的形式)
/**
* 使用切面类来查看controller接口的执行时间
*/
@Aspect
//@Order(4)
@Component
public class TimeCostAspect {
private static final Logger logger = LoggerFactory.getLogger(TimeCostAspect.class);
//后期 切入点 可以为指定的注解(比如时间花费注解,日志注解等)
@Pointcut("@annotation(com.xiu.sb.aopTest.annonation.TimeCostAnnotation)")
public void timecostAnonation() {
}
@Around("timecostAnonation()")
public Object aroundAnnotation(ProceedingJoinPoint pjp) throws Throwable {
long startTime = System.currentTimeMillis();
Object o;
try {
//执行当前切入点方法
o = pjp.proceed(pjp.getArgs());
} catch (Throwable var7) {
String message = var7.getClass().getName() + " / " + var7.getMessage() + " / " + var7.getStackTrace()[0];
logger.error("error", message);
throw var7;
}
//获取当前执行方法名称
Signature s = pjp.getSignature();
MethodSignature ms = (MethodSignature)s;
String methodName = ms.getMethod().getName();
logger.info("time cost " + (System.currentTimeMillis() - startTime) + " ms path is " + pjp.getTarget().getClass().getName() +"."+methodName);
//logger.info("[time cost] " + (System.currentTimeMillis() - startTime) + " ms [path is " +ms.toString()+ "]");
return o;
}
}
2、@AfterReturning 返回通知 会获取到切入点的返回值在该方法中,可以对其返回值进行处理
该注解提供了一个returning 变量,这里用来声明切入点方法的返回值的形参名称,必须和方法中形参名称保持一致,即returning = "result", 则该返回通知的方法中对应的返回值参数名称也应该为result。
@Aspect
@Component
public class AfterReturnAspect {
private static final Logger logger = LoggerFactory.getLogger(AfterReturnAspect.class);
//后期 切入点 可以为指定的注解(比如时间花费注解,日志注解等)
@Pointcut("@annotation(com.xiu.sb.aopTest.annonation.AfterReturnAnnotation)")
public void afterReturn() {
}
@AfterReturning(value = "afterReturn()",returning = "result")
public void aroundAnnotation(Object result) throws Throwable {
logger.info("方法的返回值:{}", JsonUtil.obj2str(result) );
}
}
3、 @AfterThrowing 返回异常 用来处理对应的方法抛出异常
该注解提供了一个throwing 参数 用来声明捕获的异常变量名称,也必须保持注解中声明的名称和通知方法中声明的变量名称一致。
@Aspect
@Component
public class AfterThrowAspect {
private static final Logger logger = LoggerFactory.getLogger(AfterThrowAspect.class);
//后期 切入点 可以为指定的注解(比如时间花费注解,日志注解等)
@Pointcut("@annotation(com.xiu.sb.aopTest.annonation.AfterThrowAnnotation)")
public void afterThrow() {
}
//该方法即为返回异常通知
@AfterThrowing(value = "afterThrow()",throwing = "ex")
public void aroundAnnotation(NullPointerException ex) throws Throwable {
logger.info("打印日志信息:");
ex.printStackTrace();
}
}
到此为止,有关spring AOP 的基本只是以及相关代码应用就介绍到这里,相关aop的原理 动态代理的内容会以后奉上。