SpringAOP简介
面向切面编程(Aspect Oriented Programming)提供了另一种角度来思考程序的结构,通过这种方式弥补面向对象编程(Object Oriented Programming)的不足。除了类以外,AOP提供了切面,切面对关注点进行模块化,例如横切多个类型和对象的事务管理(这些关注点术语通常称作横切(crosscutting)关注点)。Spring AOP是Spring的一个重要组件,但是Spring IOC并不依赖于Spring AOP,这意味着你可以自由选择是否使用AOP,AOP提供了强大的中间件解决方案,这使得Spring IOC更加完善。我们可以通过AOP来实现日志监听,事务管理,权限控制等等。
AOP概念
- 切面(Aspect):一个关注点的模块化,这个关注点可能会横切多个对象。事务管理是Java应用程序中一个关于横切关注点的很好的例子。在Spring AOP中,切面可以使用通过类(基于模式(XML)的风格)或者在普通类中以@Aspect注解(AspectJ风格)来实现。
- 连接点(Join point):程序执行过程中某个特定的点,比如某方法调用的时候或者处理异常的时候。在Spring AOP中一个连接点总是代表一个方法的执行。个人理解:AOP拦截到的方法就是一个连接点。通过声明一个org.aspectj.lang.JoinPoint类型参数我们可以在通知(Advice)中获得连接点的信息。这个在稍后会给出案例。
- 通知(Advice):在切面(Aspect)的某个特定连接点上(Join point)执行的动作。通知的类型包括"around","before","after"等等。通知的类型将在后面进行讨论。许多AOP框架,包括Spring 都是以拦截器作为通知的模型,并维护一个以连接点为中心的拦截器链。总之就是AOP对连接点的处理通过通知来执行。个人理解:Advice指当一个方法被AOP拦截到的时候要执行的代码。
- 切入点(Pointcut):匹配连接点(Join point)的断言。通知(Advice)跟切入点表达式关联,并在与切入点匹配的任何连接点上面运行。切入点表达式如何跟连接点匹配是AOP的核心,Spring默认使用AspectJ作为切入点语法。个人理解:通过切入点的表达式来确定哪些方法要被AOP拦截,之后这些被拦截的方法会执行相对应的Advice代码。
- 引入(Introduction):声明额外的方法或字段。Spring AOP允许你向任何被通知(Advice)对象引入一个新的接口(及其实现类)。个人理解:AOP允许在运行时动态的向代理对象实现新的接口来完成一些额外的功能并且不影响现有对象的功能。
- 目标对象(Target object):被一个或多个切面(Aspect)所通知(Advice)的对象,也称作被通知对象。由于Spring AOP是通过运行时代理实现的,所以这个对象永远是被代理对象。个人理解:所有的对象在AOP中都会生成一个代理类,AOP整个过程都是针对代理类在进行处理。
- AOP代理(AOP proxy):AOP框架创建的对象,用来实现切面契约(aspect contract)(包括通知方法执行等功能),在Spring中AOP可以是JDK动态代理或者是CGLIB代理。
- 织入(Weaving):把切面(aspect)连接到其他的应用程序类型或者对象上,并创建一个被通知对象。这些可以在编译时(例如使用AspectJ编译器),类加载时和运行时完成。Spring和其他纯AOP框架一样,在运行时完成织入。个人理解:把切面跟对象关联并创建该对象的代理对象的过程。
通知(Advice)的类型:
- 前置通知(Before advice):在某个连接点(Join point)之前执行的通知,但这个通知不能阻止连接点的执行(除非它抛出一个异常)。
- 返回后通知(After returning advice):在某个连接点(Join point)正常完成后执行的通知。例如,一个方法没有抛出任何异常正常返回。
- 抛出异常后通知(After throwing advice):在方法抛出异常后执行的通知。
- 后置通知(After(finally)advice):当某个连接点(Join point)退出的时候执行的通知(不论是正常返回还是发生异常退出)。
- 环绕通知(Around advice):包围一个连接点(Join point)的通知,如方法调用。这是最强大的一种通知类型。环绕通知可以在方法前后完成自定义的行为。它也会选择是否继续执行连接点或直接返回它们自己的返回值或抛出异常来结束执行。
Spring AOP简单流程图
把面向对象的过程从一个HttpRquest到访问数据库DB的整个流程看做是一条直线。AOP定义了一个切面(Aspect),一个切面包含了切入点,通知,引入,这个切面上定义了许多的切入点(Pointcut),一旦访问过程中有对象的方法跟切入点匹配那么就会被AOP拦截。此时该对象就是目标对象(Target Object)而匹配的方法就是连接点(Join Point)。紧接着AOP会用过JDK动态代理或者CGLIB生成一个目标对象的代理对象(AOP proxy),这个过程就是织入(Weaving)。这个时候我们就可以按照我们的需求对连接点进行一些拦截处理。可以看到,我们可以引入(Introduction)一个新的接口,让代理对象来实现这个接口来,以实现额外的方法和字段。也可以在连接点上进行通知(Advice),通知的类型包括了前置通知,返回后通知,抛出异常后通知,后置通知,环绕通知。最后也是最骚的是整个过程不会改变代码原有的逻辑。
Aspect:
@Controller
@Aspect
public class WebExceptionAspect {
//连接点是@RequestMapping注解的方法
@Pointcut("@annotation(org.springframework.web.bind.annotation.RequestMapping)")
private void webPointcut() {}
//切点在webpointCut()
@AfterThrowing(pointcut = "webPointcut()", throwing = "e")
//controller类抛出的异常在这边捕获
public void handleThrowing(JoinPoint joinPoint, Exception e) {
String className = joinPoint.getTarget().getClass().getName();
String methodName = joinPoint.getSignature().getName();
Object[] args = joinPoint.getArgs();
//开始打log
System.out.println("异常:" + e.getMessage());
System.out.println("异常所在类:" + className);
System.out.println("异常所在方法:" + methodName);
System.out.println("异常中的参数:");
System.out.println(methodName);
for (int i = 0; i < args.length; i++) {
System.out.println(args[i].toString());
}
}
@Before("execution(* com.xinjianqiao.mian.controller.*.*(..))")
public void beforeProcess(JoinPoint joinPoint) {
String className = joinPoint.getTarget().getClass().getName();
String methodName = joinPoint.getSignature().getName();
Object[] args = joinPoint.getArgs();
//在项目中最好记录当前操作的时间和用户
System.out.println("操作所在类:" + className);
System.out.println("操作所在方法:" + methodName);
System.out.println("操作中的参数:");
for (int i = 0; i < args.length; i++) {
System.out.println(args[i].toString());
}
}
@AfterReturning(value = "execution(* com.xinjianqiao.mian.controller.*.*(..))",returning = "returnVal")
public void returnProcess(JoinPoint joinPoint, Object returnVal) {
String className = joinPoint.getTarget().getClass().getName();
String methodName = joinPoint.getSignature().getName();
Object[] args = joinPoint.getArgs();
Class targetClass = null;
String operationName = "";
try {
targetClass = Class.forName(className);
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
Method[] methods = targetClass.getMethods();
for (Method method : methods) {
if (method.getName().equals(methodName)) {
Class[] clazzs = method.getParameterTypes();
if (clazzs != null && clazzs.length == args.length&&
//这块是取出我们注解ArchiveLog中的值,一遍在日志时明确这个操作的名称
method.getAnnotation(ArchivesLog.class)!=null) {
operationName = method.getAnnotation(ArchivesLog.class).operationName();
break;
}
}
}
System.out.println("操作名称:" + operationName);
System.out.println("方法正常返回的值:" + returnVal);
}
}
ArchivesLog:
@Target({ElementType.PARAMETER, ElementType.METHOD}) //注解可以用于参数或者方法上
@Retention(RetentionPolicy.RUNTIME) //保留至运行时
@Documented//被javadoc所记录
public @interface ArchivesLog {
/**
* 操作类型
* @return
*/
public String operationType() default "";
/**
* 操作名称
* @return
*/
public String operationName() default "";
}
Controller:
@Controller
@RequestMapping("/exception")
public class ExceptionController {
@RequestMapping(value = "/test/{id}", method = RequestMethod.GET,
produces = "application/json;charset=UTF-8" )
@ResponseBody
@ArchivesLog(operationType = "测试", operationName = "测试异常或者测试返回")
public JSONObject test(@PathVariable Integer id) throws Exception {
JSONObject result = new JSONObject();
try {//去掉注释可以测捕获的异常,不去掉注释可以测日志处理
int i = 1;
i=i/0;
} catch (Exception ex) {
throw new Exception("controller 层 异常");
}
return result;
}
}
SpringWebLauncher :
@EnableAsync
@SpringBootApplication
@ComponentScan(basePackages = {"com.biniu.*.*"}, excludeFilters = @ComponentScan.Filter(
type = FilterType.REGEX,
pattern = "com.biniu.api.config.CustomAutoConfiguration"))
@MapperScan({"com.biniu.common.dao", "com.biniu.api.mapper"})
@EnableAutoConfiguration(exclude = {FreeMarkerAutoConfiguration.class})
//@EnableScheduling
public class SpringWebLauncher extends SpringBootServletInitializer {
private static Logger logger = LoggerFactory.getLogger(SpringWebLauncher.class);
@Override
protected SpringApplicationBuilder configure(SpringApplicationBuilder application) {
return application.sources(SpringWebLauncher.class);
}
public static void main(String[] args) {
logger.info("-----服务已启动-----");
SpringApplication.run(SpringWebLauncher.class, args);
}
@Bean("sysTaskExecutor")
public TaskExecutor sysTaskExecutor() {
ThreadPoolTaskExecutor threadPoolTaskExecutor = new ThreadPoolTaskExecutor();
threadPoolTaskExecutor.setThreadNamePrefix("sysTaskExecutor");
threadPoolTaskExecutor.setCorePoolSize(10);
threadPoolTaskExecutor.setMaxPoolSize(30);
threadPoolTaskExecutor.setQueueCapacity(100);
threadPoolTaskExecutor.afterPropertiesSet();
return threadPoolTaskExecutor;
}
}
补充:
切入点表达式:
1)execution:用于匹配子表达式。
//匹配com.cjm.model包及其子包中所有类中的所有方法,返回类型任意,方法参数任意
@Pointcut("execution(* com.cjm.model..*.*(..))")
public void before(){}
2)within:用于匹配连接点所在的Java类或者包。
//匹配Person类中的所有方法
@Pointcut("within(com.cjm.model.Person)")
public void before(){}
//匹配com.cjm包及其子包中所有类中的所有方法
@Pointcut("within(com.cjm..*)")
public void before(){}
3) this:用于向通知方法中传入代理对象的引用。
@Before("before() && this(proxy)")
public void beforeAdvide(JoinPoint point, Object proxy){
//处理逻辑
}
4)target:用于向通知方法中传入目标对象的引用。
@Before("before() && target(target)
public void beforeAdvide(JoinPoint point, Object proxy){
//处理逻辑
}
5)args:用于将参数传入到通知方法中。
@Before("before() && args(age,username)")
public void beforeAdvide(JoinPoint point, int age, String username){
//处理逻辑
}
6)@within :用于匹配在类一级使用了参数确定的注解的类,其所有方法都将被匹配。
@Pointcut("@within(com.cjm.annotation.AdviceAnnotation)") - 所有被@AdviceAnnotation标注的类都将匹配
public void before(){}
7)@target :和@within的功能类似,但必须要指定注解接口的保留策略为RUNTIME。
@Pointcut("@target(com.cjm.annotation.AdviceAnnotation)")
public void before(){}
8)@args :传入连接点的对象对应的Java类必须被@args指定的Annotation注解标注。
@Before("@args(com.cjm.annotation.AdviceAnnotation)")
public void beforeAdvide(JoinPoint point){
//处理逻辑
}
9)@annotation :匹配连接点被它参数指定的Annotation注解的方法。也就是说,所有被指定注解标注的方法都将匹配。
@Pointcut("@annotation(com.cjm.annotation.AdviceAnnotation)")
public void before(){}
10)bean:通过受管Bean的名字来限定连接点所在的Bean。该关键词是Spring2.5新增的。
@Pointcut("bean(person)")
public void before(){}
JoinPoint :封装了SpringAop中切面方法的信息,
public interface JoinPoint {
/**
* 连接点所在位置的相关信息
* @return
*/
String toString();
/**
* 连接点所在位置的简短相关信息
* @return
*/
String toShortString();
/**
* 连接点所在位置的全部相关信息
* @return
*/
String toLongString();
/**
* 返回AOP代理对象,也就是com.sun.proxy.$Proxy18
* @return
*/
Object getThis();
/**
* 返回目标对象,
* 一般我们都需要它或者
* (也就是定义方法的接口或类,为什么会是接口呢?
* 这主要是在目标对象本身是动态代理的情况下,
* 例如Mapper。所以返回的是定义方法的对象
* 如aoptest.daoimpl.GoodDaoImpl
* 或com.b.base.BaseMapper<T, E, PK>)
* @return
*/
Object getTarget();
/**
* 返回被通知方法参数列表
* @return
*/
Object[] getArgs();
/**
* 返回当前连接点签名
* 其getName()方法返回方法的FQN,
* 如void aoptest.dao.GoodDao.delete()
* 或com.b.base.BaseMapper.insert(T)
* (需要注意的是,很多时候我们定义了子类继承父类的时候,
* 我们希望拿到基于子类的FQN,这直接可拿不到,
* 要依赖于AopUtils.getTargetClass(point.getTarget())
* 获取原始代理对象)
* @return
*/
Signature getSignature();
/**
* 返回连接点方法所在类文件中的位置
* @return
*/
SourceLocation getSourceLocation();
/**
* 连接点类型
* @return
*/
String getKind();
/**
* 返回连接点静态部分
* @return
*/
StaticPart getStaticPart();
}
JoinPoint.StaticPart:提供访问连接点的静态部分,如被通知方法签名、连接点类型等:
public interface StaticPart {
Signature getSignature(); //返回当前连接点签名
String getKind(); //连接点类型
int getId(); //唯一标识
String toString(); //连接点所在位置的相关信息
String toShortString(); //连接点所在位置的简短相关信息
String toLongString(); //连接点所在位置的全部相关信息
}