菜鸟之路——Spring MVC(十四)Spring AOP

     AOP是OOP的延续,是Aspect Oriented Programming的缩写,意思是面向切面编程。可以通过预编译方式和运行期动态代理实现在不修改源代码的情况下给程序动态统一添加功能的一种技术。AOP实际是GoF设计模式的延续,设计模式孜孜不倦追求的是调用者和被调用者之间的解耦,AOP可以说也是这种目标的一种实现。

  我们现在做的一些非业务,如:日志、事务、安全等都会写在业务代码中(也即是说,这些非业务类横切于业务类),但这些代码往往是重复,复制——粘贴式的代码会给程序的维护带来不便,AOP就实现了把这些业务需求与系统需求分开来做。这种解决的方式也称代理机制。

  一、什么是 AOP

        AOP, 即 面向切面编程, 它与 OOP( Object-Oriented Programming, 面向对象编程) 相辅相成, 提供了与 OOP 不同的抽象软件结构的视角。在 OOP 中, 我们以类(class)作为我们的基本单元, 而 AOP 中的基本单元是 Aspect(切面)。

  切面(Aspect)

  官方的抽象定义为“一个关注点的模块化,这个关注点可能会横切多个对象”。aspect 由 pointcount 和 advice 组成, 它既包含了横切逻辑的定义, 也包括了连接点的定义. Spring AOP就是负责实施切面的框架, 它将切面所定义的横切逻辑织入到切面所指定的连接点中。
  AOP的工作重心在于如何将增强织入目标对象的连接点上, 这里包含两个工作:
  1、如何通过 pointcut 和 advice 定位到特定的 joinpoint 上
  2、如何在 advice 中编写切面代码
  可以简单地认为, 使用 @Aspect 注解的类就是切面。“切面”在ApplicationContext中<aop:aspect>来配置。

  增强(advice)

  “切面”对于某个“连接点”所产生的动作。一个“切面”可以包含多个“Advice”。也就是说由 aspect 添加到特定的 join point(即满足 point cut 规则的 join point) 的一段代码。
  许多 AOP框架, 包括 Spring AOP, 会将 advice 模拟为一个拦截器(interceptor), 并且在 join point 上维护多个 advice, 进行层层拦截。
  例如 HTTP 鉴权的实现, 我们可以为每个使用 RequestMapping 标注的方法织入 advice, 当 HTTP 请求到来时, 首先进入到 advice 代码中, 在这里我们可以分析这个 HTTP 请求是否有相应的权限, 如果有, 则执行 Controller, 如果没有, 则抛出异常。这里的 advice 就扮演着鉴权拦截器的角色了。

  Advice的类型:

  前置通知(Before advice):在某连接点(JoinPoint)之前执行的通知,但这个通知不能阻止连接点前的执行。ApplicationContext中在<aop:aspect>里面使用<aop:before>元素进行声明。
  后置通知(After advice):当某连接点退出的时候执行的通知(不论是正常返回还是异常退出)。ApplicationContext中在<aop:aspect>里面使用<aop:after>元素进行声明。
  返回后通知(After return advice):在某连接点正常完成后执行的通知,不包括抛出异常的情况。ApplicationContext中在<aop:aspect>里面使用<after-returning>元素进行声明。
  环绕通知(Around advice):包围一个连接点的通知,类似Web中Servlet规范中的Filter的doFilter方法。可以在方法的调用前后完成自定义的行为,也可以选择不执行。ApplicationContext中在<aop:aspect>里面使用<aop:around>元素进行声明。
  抛出异常后通知(After throwing advice):在方法抛出异常退出时执行的通知。ApplicationContext中在<aop:aspect>里面使用<aop:after-throwing>元素进行声明。

  注:可以将多个通知应用到一个目标对象上,即可以将多个切面织入到同一目标对象。

  连接点(join point)

  程序执行过程中的某一行为,是程序运行中的一些时间点, 例如一个方法的执行, 或者是一个异常的处理。需要注意的是:在 Spring AOP 中, join point 总是方法的执行点, 即只有方法连接点。

  切点(point cut)

  匹配连接点的断言,在AOP中通知和一个切入点表达式关联。Advice 是和特定的 point cut 关联的, 并且在 point cut 相匹配的 join point 中执行。在 Spring 中, 所有的方法都可以认为是 joinpoint, 但是我们并不希望在所有的方法上都添加 Advice, 而 pointcut 的作用就是提供一组规则(使用 AspectJ pointcut expression language 来描述) 来匹配joinpoint, 给满足规则的 joinpoint 添加 Advice。

  目标对象(Target Object)

  被一个或者多个切面所通知的对象,是织入 advice 的目标对象。 目标对象也被称为 advised object,因为 Spring AOP 使用运行时代理的方式来实现 aspect, 因此 adviced object 总是一个代理对象(proxied object)。注意, adviced object 指的不是原来的类, 而是织入 advice 后所产生的代理类。

  AOP proxy

  在Spring AOP中有两种代理方式,JDK动态代理和CGLIB代理。默认情况下,TargetObject实现了接口时,则采用JDK动态代理;反之,如果需要为一个类实现代理,采用CGLIB代理。当一个业务逻辑对象没有实现接口时, 那么Spring AOP 就默认使用 CGLIB 来作为 AOP 代理,即如果我们需要为一个方法织入 advice, 但是这个方法不是一个接口所提供的方法, 则此时 Spring AOP 会使用 CGLIB 来实现动态代理。鉴于此, Spring AOP 建议基于接口编程, 对接口进行 AOP 而不是类。如强制使用CGLIB代理,需要将 <aop:config>的 proxy-target-class属性设为true。

  织入(Weaving)

  将 aspect 和其他对象连接起来, 并创建 adviced object 的过程。根据不同的实现技术, AOP织入有三种方式:
  1、编译器织入, 这要求有特殊的Java编译器。
  2、类装载期织入, 这需要有特殊的类装载器。
  3、动态代理织入, 在运行期为目标类添加增强(Advice)生成子类的方式。
  Spring 采用动态代理织入, 而AspectJ采用编译器织入和类装载期织入.

  彻底理解 aspect, join point, point cut, advice

  关于join point 和 point cut 的区别:
  在 Spring AOP 中, 所有的方法执行都是 join point。 而 point cut 是一个描述信息, 它修饰的是 join point, 通过 point cut, 我们就可以确定哪些 join point 可以被织入 Advice。 因此 join point 和 point cut 本质上就是两个不同纬度上的东西。即advice 是在 join point 上执行的, 而 point cut 规定了哪些 join point 可以执行哪些 advice。

  一个形象的比喻来描绘 AOP 中 aspect, jointpoint, pointcut 与 advice 之间的关系:

  让我们来假设一下, 从前有一个叫爪哇的小县城, 在一个月黑风高的晚上, 这个县城中发生了命案. 作案的凶手十分狡猾, 现场没有留下什么有价值的线索. 不过万幸的是, 刚从隔壁回来的老王恰好在这时候无意中发现了凶手行凶的过程, 但是由于天色已晚, 加上凶手蒙着面, 老王并没有看清凶手的面目, 只知道凶手是个男性, 身高约七尺五寸. 爪哇县的县令根据老王的描述, 对守门的士兵下命令说: 凡是发现有身高七尺五寸的男性, 都要抓过来审问. 士兵当然不敢违背县令的命令, 只好把进出城的所有符合条件的人都抓了起来.
  来让我们看一下上面的一个小故事和 AOP 到底有什么对应关系。
首先我们知道, 在 Spring AOP 中 join point 指代的是所有方法的执行点, 而 point cut 是一个描述信息, 它修饰的是 join point, 通过 point cut, 我们就可以确定哪些 join point 可以被织入 Advice。对应到我们在上面举的例子, 我们可以做一个简单的类比, join point 就相当于 爪哇的小县城里的百姓, point cut 就相当于 老王所做的指控, 即凶手是个男性, 身高约七尺五寸, 而 advice 则是县令所下的命令凡是发现有身高七尺五寸的男性, 都要抓过来审问。
  为什么可以这样类比呢?
  join point --> 爪哇的小县城里的百姓: 因为根据定义, join point 是所有可能被织入 advice 的候选的点, 在 Spring AOP中, 则可以认为所有方法执行点都是 join point。而在我们上面的例子中, 命案发生在小县城中, 按理说在此县城中的所有人都有可能是嫌疑人。
  point cut --> 男性, 身高约七尺五寸: 我们知道, 所有的方法(joint point) 都可以织入 advice, 但是我们并不希望在所有方法上都织入 advice, 而 pointcut 的作用就是提供一组规则来匹配joinpoint, 给满足规则的 joinpoint 添加 advice。同理, 对于县令来说, 他再昏庸, 也知道不能把县城中的所有百姓都抓起来审问, 而是根据凶手是个男性, 身高约七尺五寸, 把符合条件的人抓起来。在这里 凶手是个男性, 身高约七尺五寸 就是一个修饰谓语, 它限定了凶手的范围, 满足此修饰规则的百姓都是嫌疑人, 都需要抓起来审问。
  advice --> 抓过来审问, advice 是一个动作, 即一段 Java 代码, 这段 Java 代码是作用于 point cut 所限定的那些 join point 上的。同理, 对比到我们的例子中, 抓过来审问 这个动作就是对作用于那些满足 男性, 身高约七尺五寸 的爪哇的小县城里的百姓。
  aspect: aspect 是 point cut 与 advice 的组合, 因此在这里我们就可以类比: "根据老王的线索, 凡是发现有身高七尺五寸的男性, 都要抓过来审问" 这一整个动作可以被认为是一个 aspect。

  或则我们也可以从语法的角度来简单类比一下。我们在学英语时, 经常会接触什么 定语, 被动句 之类的概念, 那么可以做一个不严谨的类比, 即 joinpoint 可以认为是一个 宾语, 而 pointcut 则可以类比为修饰 joinpoint 的定语, 那么整个 aspect 就可以描述为: 满足 pointcut 规则的 joinpoint 会被添加相应的 advice 操作。

  二、怎么使用AOP    

     使用Spring AOP可以基于两种方式,一种是比较方便和强大的注解方式,另一种则是中规中矩的xml配置方式。

   注解方式:

  注解方式使用的是注解@AspectJ 。这是一种使用 Java 注解来实现 AOP 的编码风格。@AspectJ 可以以 XML 的方式或以注解的方式来使用, 不论以哪种方式使能@ASpectJ, 我们都必须保证 aspectjweaver.jar 在 classpath 中。

  使用 Java Configuration 方式使能@AspectJ

@Configuration
@EnableAspectJAutoProxy
public class AppConfig {
}
      使用 XML 方式使能@AspectJ。
<aop:aspectj-autoproxy/>
   使用CGLIB代理:

<aop:aspectj-autoproxy proxy-target-class="true"/>
    一般使用XML的方式,在xml文件中声明激活自动扫描组件功能,同时激活自动代理功能。

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xmlns:context="http://www.springframework.org/schema/context"
	xmlns:aop="http://www.springframework.org/schema/aop"
	xsi:schemaLocation="http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-3.1.xsd
		http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
		http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.0.xsd">

	<!-- 激活组件扫描功能,在具体的包及其子包下面自动扫描通过注解配置的组件 -->
	<context:component-scan base-package="...spring.aop"/>
	<!-- 激活自动代理功能,CGLIB代理可选,最好面向接口编程,不使用 -->
	<aop:aspectj-autoproxy proxy-target-class="true"/>
	
	<!-- 需要声明的具体的bean -->
	<bean id="***" class="***" />

</beans>

     然后就可以在代码中使用注解了。

    (一)定义 aspect(切面)

       当使用注解 @Aspect 标注一个 Bean 后, 那么 Spring 框架会自动收集这些 Bean, 并添加到 Spring AOP 中, 例如:
//声明这是一个组件
@Component
//声明这是一个切面Bean
@Aspect
public class MyTest {
}
     注意, 仅仅使用@Aspect 注解, 并不能将一个 Java 对象转换为 Bean, 因此我们还需要使用类似 @Component 之类的注解。
  如果一个 类被@Aspect 标注, 则这个类就不能是其他 aspect 的 advised object了, 因为使用 @Aspect 后, 这个类就会被排除在 auto-proxying 机制之外,即使用CGLIB代理。

    (二)声明 pointcut

      一个 pointcut 的声明由两部分组成:
  一个方法签名, 包括方法名和相关参数和一个 pointcut 表达式, 用来指定哪些方法执行是我们感兴趣的(即因此可以织入 advice)。

  在@AspectJ 风格的 AOP 中, 我们使用一个方法来描述 pointcut, 即:

@Pointcut("execution(* com.xys.service.UserService.*(..))") // 切点表达式
private void dataAccessOperation() {} 
      这个方法必须无返回值。pointcut 表达式使用@Pointcut 注解指定。上面我们简单地定义了一个 pointcut, 这个 pointcut 所描述的是: 匹配所有在包 com.xys.service.UserService 下的所有方法的执行。该方法无方法体,主要为方便同类中其他方法使用此处配置的切入点,为了方便。

  切点表达式由标志符(designator)和操作参数组成。 如 "execution(* greetTo(..))" 的切点表达式, execution 就是 标志符, 而圆括号里的 *** greetTo(..)** 就是操作参数。切点标志符(designator)有多种,比如:

      1、execution
  匹配 join point 的执行, 例如 "execution(* hello(..))" 表示匹配所有目标类中的 hello() 方法. 这个是最基本的 pointcut 标志符。

  2、within
  匹配特定包下的所有 join point, 例如 within(com.xys.*) 表示 com.xys 包中的所有连接点, 即包中的所有类的所有方法。而 within(com.xys.service.*Service) 表示在 com.xys.service 包中所有以 Service 结尾的类的所有的连接点.
  3、bean
  匹配 bean 名字为指定值的 bean 下的所有方法, 例如:

bean(*Service) // 匹配名字后缀为 Service 的 bean 下的所有方法
bean(myService) // 匹配名字为 myService 的 bean 下的所有方法
     4、args
  匹配参数满足要求的的方法。例如:

@Pointcut("within(com.xys.demo2.*)")
public void pointcut2() {
}
 
@Before(value = "pointcut2()  &&  args(name)")
public void doSomething(String name) {
    logger.info("---page: {}---", name);
}
@Service
public class NormalService {
    private Logger logger = LoggerFactory.getLogger(getClass());

    public void someMethod() {
        logger.info("---NormalService: someMethod invoked---");
    }


    public String test(String name) {
        logger.info("---NormalService: test invoked---");
        return "服务一切正常";
    }
}
      当 NormalService.test 执行时, 则 advice doSomething 就会执行, test 方法的参数 name 就会传递到 doSomething 中。
  类似的还有:

// 匹配只有一个参数 name 的方法
@Before(value = "aspectMethod()  &&  args(name)")
public void doSomething(String name) {
}
 
// 匹配第一个参数为 name 的方法
@Before(value = "aspectMethod()  &&  args(name, ..)")
public void doSomething(String name) {
}
 
// 匹配第二个参数为 name 的方法
Before(value = "aspectMethod()  &&  args(*, name, ..)")
public void doSomething(String name) {
}
   5、@annotation
  匹配由指定注解所标注的方法, 例如:

@Pointcut("@annotation(com.xys.demo1.AuthChecker)")
public void pointcut() {
}
       则匹配由自定义的注解 AuthChecker 所标注的方法。

  一些常见的切点表达式:

  1.匹配方法签名

// 匹配指定包中的所有的方法
execution(* com.xys.service.*(..))
 
// 匹配当前包中的指定类的所有方法
execution(* UserService.*(..))
 
// 匹配指定包中的所有 public 方法
execution(public * com.xys.service.*(..))
 
// 匹配指定包中的所有 public 方法, 并且返回值是 int 类型的方法
execution(public int com.xys.service.*(..))
 
// 匹配指定包中的所有 public 方法, 并且第一个参数是 String, 返回值是 int 类型的方法
execution(public int com.xys.service.*(String name, ..))
     2、匹配类型签名

// 匹配指定包中的所有的方法, 但不包括子包
within(com.xys.service.*)
 
// 匹配指定包中的所有的方法, 包括子包
within(com.xys.service..*)
 
// 匹配当前包中的指定类中的方法
within(UserService)
 
// 匹配一个接口的所有实现类中的实现的方法
within(UserDao+)
    3、匹配 Bean 名字
// 匹配以指定名字结尾的 Bean 中的所有方法
bean(*Service)
   4、切点表达式组合
// 匹配以 Service 或 ServiceImpl 结尾的 bean
bean(*Service || *ServiceImpl)
 
// 匹配名字以 Service 开头, 并且在包 com.xys.service 中的 bean
bean(*Service) && within(com.xys.service.*)

     (三)声明 advice

       advice 是和一个 pointcut 表达式关联在一起的, 并且会在匹配的 join point 的方法执行的前/后/周围 运行. pointcut 表达式可以是简单的一个 pointcut 名字的引用(这就是之前声明的空方法体的pointcut的好处), 或者是完整的 pointcut 表达式。下面我们以几个简单的 advice 为例子, 来看一下一个 advice 是如何声明的:

@Component
//声明这是一个切面Bean
@Aspect
public class ServiceAspect {

	private final static Log log = LogFactory.getLog(ServiceAspect.class);
	
	//配置切入点,该方法无方法体,主要为方便同类中其他方法使用此处配置的切入点
	@Pointcut("execution(* cn.ysh.studio.spring.aop.service..*(..))")
	public void aspect(){	}
	
	/*
	 * 配置前置通知,使用在方法aspect()上注册的切入点
	 * 同时接受JoinPoint切入点对象,可以没有该参数
	 */
	@Before("aspect()")
	public void before(JoinPoint joinPoint){
		if(log.isInfoEnabled()){
			log.info("before " + joinPoint);
		}
	}
	
	//配置后置通知,使用在方法aspect()上注册的切入点
	@After("aspect()")
	public void after(JoinPoint joinPoint){
		if(log.isInfoEnabled()){
			log.info("after " + joinPoint);
		}
	}
	
	//配置环绕通知,使用在方法aspect()上注册的切入点
	@Around("aspect()")
	public void around(JoinPoint joinPoint){
		long start = System.currentTimeMillis();
		try {
			((ProceedingJoinPoint) joinPoint).proceed();
			long end = System.currentTimeMillis();
			if(log.isInfoEnabled()){
				log.info("around " + joinPoint + "\tUse time : " + (end - start) + " ms!");
			}
		} catch (Throwable e) {
			long end = System.currentTimeMillis();
			if(log.isInfoEnabled()){
				log.info("around " + joinPoint + "\tUse time : " + (end - start) + " ms with exception : " + e.getMessage());
			}
		}
	}
	
	//配置后置返回通知,使用在方法aspect()上注册的切入点
	@AfterReturning("aspect()")
	public void afterReturn(JoinPoint joinPoint){
		if(log.isInfoEnabled()){
			log.info("afterReturn " + joinPoint);
		}
	}
	
	//配置抛出异常后通知,使用在方法aspect()上注册的切入点
	@AfterThrowing(pointcut="aspect()", throwing="ex")
	public void afterThrow(JoinPoint joinPoint, Exception ex){
		if(log.isInfoEnabled()){
			log.info("afterThrow " + joinPoint + "\t" + ex.getMessage());
		}
	}
	
}

       下面结合上面这个例子,我们来看看如何通过全XML配置的方式来使用AOP:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xmlns:context="http://www.springframework.org/schema/context"
	xmlns:aop="http://www.springframework.org/schema/aop"
	xsi:schemaLocation="http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-3.1.xsd
		http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
		http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.0.xsd">


	<!-- 系统服务组件的切面Bean -->
	<bean id="serviceAspect" class="cn.ysh.studio.spring.aop.aspect.ServiceAspect"/>
	<!-- AOP配置 -->
	<aop:config>
		<!-- 声明一个切面,并注入切面Bean,相当于@Aspect -->
		<aop:aspect id="simpleAspect" ref="serviceAspect">
			<!-- 配置一个切入点,相当于@Pointcut -->
			<aop:pointcut expression="execution(* cn.ysh.studio.spring.aop.service..*(..))" id="simplePointcut"/>
			<!-- 配置通知,相当于@Before、@After、@AfterReturn、@Around、@AfterThrowing -->
			<aop:before pointcut-ref="simplePointcut" method="before"/>
			<aop:after pointcut-ref="simplePointcut" method="after"/>
			<aop:after-returning pointcut-ref="simplePointcut" method="afterReturn"/>
			<aop:after-throwing pointcut-ref="simplePointcut" method="afterThrow" throwing="ex"/>
		</aop:aspect>
	</aop:config>

</beans>
     可见,当代码一复杂,XML方式的使用将使得XML文件变得很庞大、可读性会变得很差,所以我们一般使用注解方式来使用AOP。

  三、几个Spring AOP 实战例子

    (一)HTTP 接口鉴权

     首先让我们来想象一下如下场景: 我们需要提供的 HTTP RESTful 服务, 这个服务会提供一些比较敏感的信息, 因此对于某些接口的调用会进行调用方权限的校验, 而某些不太敏感的接口则不设置权限, 或所需要的权限比较低(例如某些监控接口, 服务状态接口等)。实现这样的需求的方法有很多, 例如我们可以在每个 HTTP 接口方法中对服务请求的调用方进行权限的检查, 当调用方权限不符时, 方法返回错误。当然这样做并无不可, 不过如果我们的 api 接口很多, 每个接口都进行这样的判断, 无疑有很多冗余的代码, 并且很有可能有某个粗心的家伙忘记了对调用者的权限进行验证, 这样就会造成潜在的 bug。
  那么除了上面的所说的方法外, 还有没有别的比较优雅的方式来实现呢? 当然有啦, 不然我在这啰嗦半天干嘛呢, 它就是我们今天的主角: AOP。
  让我们来提炼一下我们的需求:
  1、可以定制地为某些指定的 HTTP RESTful api 提供权限验证功能.
  2、当调用方的权限不符时, 返回错误.

  根据上面所提出的需求, 我们可以进行如下设计:
  1、自己设计一个特殊的注解 AuthChecker, 这个是一个方法注解, 有此注解所标注的 Controller 需要进行调用方权限的认证。
  2、利用 Spring AOP, 以 **@annotation** 切点标志符来匹配有注解 AuthChecker 所标注的 joinpoint。
  3、在 advice 中, 简单地检查调用者请求中的 Cookie 中是否有我们指定的 token, 如果有, 则认为此调用者权限合法, 允许调用, 反之权限不合法, 范围错误。
  根据上面的设计, 我们来看一下具体的源码吧。
  首先是 AuthChecker 注解的定义,AuthChecker.java:

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface AuthChecker {
}
       AuthChecker 注解是一个方法注解, 它用于注解 RequestMapping 方法。

  aspect 的实现,HttpAopAdviseDefine.java:

@Component
@Aspect
public class HttpAopAdviseDefine {

    // 定义一个 Pointcut, 使用 切点表达式函数 来描述对哪些 Join point 使用 advise.
    @Pointcut("@annotation(com.xys.demo1.AuthChecker)")
    public void pointcut() {
    }

    // 定义 advise
    @Around("pointcut()")
    public Object checkAuth(ProceedingJoinPoint joinPoint) throws Throwable {
        HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes())
                .getRequest();

        // 检查用户所传递的 token 是否合法
        String token = getUserToken(request);
        if (!token.equalsIgnoreCase("123456")) {
            return "错误, 权限不合法!";
        }

        return joinPoint.proceed();
    }

    private String getUserToken(HttpServletRequest request) {
        Cookie[] cookies = request.getCookies();
        if (cookies == null) {
            return "";
        }
        for (Cookie cookie : cookies) {
            if (cookie.getName().equalsIgnoreCase("user_token")) {
                return cookie.getValue();
            }
        }
        return "";
    }
}
    在这个 aspect 中, 我们首先定义了一个 pointcut, 以 @annotation切点标志符来匹配有注解 AuthChecker 所标注的 join point,然后再定义 advice。当被 AuthChecker 注解所标注的方法调用前, 会执行我们的这个 advice。这个 advice 的处理逻辑很简单, 即从 HTTP 请求中获取名为 user_token 的 cookie 的值, 如果它的值是 123456, 则我们认为此 HTTP 请求合法, 进而调用 joinPoint.proceed() 将 HTTP 请求转交给相应的控制器处理。如果user_token cookie 的值不是 123456, 或为空, 则认为此 HTTP 请求非法, 返回错误。
  接下来我们来写一个模拟的 HTTP 接口,DemoController.java:

@RestController
public class DemoController {
    @RequestMapping("/aop/http/alive")
    public String alive() {
        return "服务一切正常";
    }

    @AuthChecker
    @RequestMapping("/aop/http/user_info")
    public String callSomeInterface() {
        return "调用了 user_info 接口.";
    }
}
      注意到上面我们提供了两个 HTTP 接口, 其中 接口 /aop/http/alive 是没有 AuthChecker 标注的, 而 /aop/http/user_info 接口则用到了 @AuthChecker 标注。那么自然地, 当请求了 /aop/http/user_info 接口时, 就会触发我们所设置的权限校验逻辑。
  接下来我们来验证一下, 我们所实现的功能是否有效。
  首先在 Postman 中, 调用 /aop/http/alive 接口, 请求头中不加任何参数:

       可以看到, 我们的 HTTP 请求完全没问题。那么再来看一下请求 /aop/http/user_info 接口会怎样呢:

     当我们请求 /aop/http/user_info 接口时, 服务返回一个权限异常的错误, 为什么会这样呢? 自然就是我们的权限认证系统起了作为: 当一个方法被调用并且这个方法有 AuthChecker 标注时, 那么首先会执行到我们的 around advice, 在这个 advice 中, 我们会校验 HTTP 请求的 cookie 字段中是否有携带 user_token 字段时, 如果没有, 则返回权限错误。那么为了能够正常地调用 /aop/http/user_info 接口, 我们可以在 Cookie 中添加 user_token=123456:


       注意, Postman 默认是不支持 Cookie 的, 所以为了实现添加 Cookie 的功能, 我们需要安装 Postman 的 interceptor 插件。

    (二)Spring AOP拦截Controller,Service实现日志管理

      第一步定义两个注解:

import java.lang.annotation.*;    
    
/**  
 *自定义注解 拦截Controller  
 */    
    
@Target({ElementType.PARAMETER, ElementType.METHOD})    
@Retention(RetentionPolicy.RUNTIME)    
@Documented    
public  @interface SystemControllerLog {    
    
    String description()  default "";      
    
}    
import java.lang.annotation.*;    
    
/**  
 *自定义注解 拦截service  
 */    
    
@Target({ElementType.PARAMETER, ElementType.METHOD})    
@Retention(RetentionPolicy.RUNTIME)    
@Documented    
public  @interface SystemServiceLog {    
    
    String description()  default "";    
    
}    
     第二步创建一个切点类:
import com.model.Log;    
import com.model.User;    
import com.service.LogService;    
import com.util.DateUtil;    
import com.util.JSONUtil;    
import com.util.SpringContextHolder;    
import com.util.WebConstants;    
import org.aspectj.lang.JoinPoint;    
import org.aspectj.lang.annotation.*;    
import org.slf4j.Logger;    
import org.slf4j.LoggerFactory;    
import org.springframework.stereotype.Component;    
import org.springframework.web.context.request.RequestContextHolder;    
import org.springframework.web.context.request.ServletRequestAttributes;    
import javax.annotation.Resource;    
import javax.servlet.http.HttpServletRequest;    
import javax.servlet.http.HttpSession;    
import java.lang.reflect.Method;    
    
/**  
 * 切点类  
 */    
@Aspect    
@Component    
public  class SystemLogAspect {    
    //注入Service用于把日志保存数据库    
    @Resource    
     private LogService logService;    
    //本地异常日志记录对象    
     private  static  final Logger logger = LoggerFactory.getLogger(SystemLogAspect. class);    
    
    //Service层切点    
    @Pointcut("@annotation(com.annotation.SystemServiceLog)")    
     public  void serviceAspect() {    
    }    
    
    //Controller层切点    
    @Pointcut("@annotation(com.annotation.SystemControllerLog)")    
     public  void controllerAspect() {    
    }    
    
    /**  
     * 前置通知 用于拦截Controller层记录用户的操作  
     *  
     * @param joinPoint 切点  
     */    
    @Before("controllerAspect()")    
     public  void doBefore(JoinPoint joinPoint) {    
    
        HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();    
        HttpSession session = request.getSession();    
        //读取session中的用户    
        User user = (User) session.getAttribute(WebConstants.CURRENT_USER);    
        //请求的IP    
        String ip = request.getRemoteAddr();    
         try {    
            //*========控制台输出=========*//    
            System.out.println("=====前置通知开始=====");    
            System.out.println("请求方法:" + (joinPoint.getTarget().getClass().getName() + "." + joinPoint.getSignature().getName() + "()"));    
            System.out.println("方法描述:" + getControllerMethodDescription(joinPoint));    
            System.out.println("请求人:" + user.getName());    
            System.out.println("请求IP:" + ip);    
            //*========数据库日志=========*//    
            Log log = SpringContextHolder.getBean("logxx");    
            log.setDescription(getControllerMethodDescription(joinPoint));    
            log.setMethod((joinPoint.getTarget().getClass().getName() + "." + joinPoint.getSignature().getName() + "()"));    
            log.setType("0");    
            log.setRequestIp(ip);    
            log.setExceptionCode( null);    
            log.setExceptionDetail( null);    
            log.setParams( null);    
            log.setCreateBy(user);    
            log.setCreateDate(DateUtil.getCurrentDate());    
            //保存数据库    
            logService.add(log);    
            System.out.println("=====前置通知结束=====");    
        }  catch (Exception e) {    
            //记录本地异常日志    
            logger.error("==前置通知异常==");    
            logger.error("异常信息:{}", e.getMessage());    
        }    
    }    
    
    /**  
     * 异常通知 用于拦截service层记录异常日志  
     *  
     * @param joinPoint  
     * @param e  
     */    
    @AfterThrowing(pointcut = "serviceAspect()", throwing = "e")    
     public  void doAfterThrowing(JoinPoint joinPoint, Throwable e) {    
        HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();    
        HttpSession session = request.getSession();    
        //读取session中的用户    
        User user = (User) session.getAttribute(WebConstants.CURRENT_USER);    
        //获取请求ip    
        String ip = request.getRemoteAddr();    
        //获取用户请求方法的参数并序列化为JSON格式字符串    
        String params = "";    
         if (joinPoint.getArgs() !=  null && joinPoint.getArgs().length > 0) {    
             for ( int i = 0; i < joinPoint.getArgs().length; i++) {    
                params += JSONUtil.toJsonString(joinPoint.getArgs()[i]) + ";";    
            }    
        }    
         try {    
              /*========控制台输出=========*/    
            System.out.println("=====异常通知开始=====");    
            System.out.println("异常代码:" + e.getClass().getName());    
            System.out.println("异常信息:" + e.getMessage());    
            System.out.println("异常方法:" + (joinPoint.getTarget().getClass().getName() + "." + joinPoint.getSignature().getName() + "()"));    
            System.out.println("方法描述:" + getServiceMthodDescription(joinPoint));    
            System.out.println("请求人:" + user.getName());    
            System.out.println("请求IP:" + ip);    
            System.out.println("请求参数:" + params);    
               /*==========数据库日志=========*/    
            Log log = SpringContextHolder.getBean("logxx");    
            log.setDescription(getServiceMthodDescription(joinPoint));    
            log.setExceptionCode(e.getClass().getName());    
            log.setType("1");    
            log.setExceptionDetail(e.getMessage());    
            log.setMethod((joinPoint.getTarget().getClass().getName() + "." + joinPoint.getSignature().getName() + "()"));    
            log.setParams(params);    
            log.setCreateBy(user);    
            log.setCreateDate(DateUtil.getCurrentDate());    
            log.setRequestIp(ip);    
            //保存数据库    
            logService.add(log);    
            System.out.println("=====异常通知结束=====");    
        }  catch (Exception ex) {    
            //记录本地异常日志    
            logger.error("==异常通知异常==");    
            logger.error("异常信息:{}", ex.getMessage());    
        }    
         /*==========记录本地异常日志==========*/    
        logger.error("异常方法:{}异常代码:{}异常信息:{}参数:{}", joinPoint.getTarget().getClass().getName() + joinPoint.getSignature().getName(), e.getClass().getName(), e.getMessage(), params);    
    
    }    
    
    
    /**  
     * 获取注解中对方法的描述信息 用于service层注解  
     *  
     * @param joinPoint 切点  
     * @return 方法描述  
     * @throws Exception  
     */    
     public  static String getServiceMthodDescription(JoinPoint joinPoint)    
             throws Exception {    
        String targetName = joinPoint.getTarget().getClass().getName();    
        String methodName = joinPoint.getSignature().getName();    
        Object[] arguments = joinPoint.getArgs();    
        Class targetClass = Class.forName(targetName);    
        Method[] methods = targetClass.getMethods();    
        String description = "";    
         for (Method method : methods) {    
             if (method.getName().equals(methodName)) {    
                Class[] clazzs = method.getParameterTypes();    
                 if (clazzs.length == arguments.length) {    
                    description = method.getAnnotation(SystemServiceLog. class).description();    
                     break;    
                }    
            }    
        }    
         return description;    
    }    
    
    /**  
     * 获取注解中对方法的描述信息 用于Controller层注解  
     *  
     * @param joinPoint 切点  
     * @return 方法描述  
     * @throws Exception  
     */    
     public  static String getControllerMethodDescription(JoinPoint joinPoint)  throws Exception {    
        String targetName = joinPoint.getTarget().getClass().getName();    
        String methodName = joinPoint.getSignature().getName();    
        Object[] arguments = joinPoint.getArgs();    
        Class targetClass = Class.forName(targetName);    
        Method[] methods = targetClass.getMethods();    
        String description = "";    
         for (Method method : methods) {    
             if (method.getName().equals(methodName)) {    
                Class[] clazzs = method.getParameterTypes();    
                 if (clazzs.length == arguments.length) {    
                    description = method.getAnnotation(SystemControllerLog. class).description();    
                     break;    
                }    
            }    
        }    
         return description;    
    }    
}    
    XML文件的配置,参考前面的注解方式使用AOP。

  如果AOP拦截不到Controller,那是因为Controller被jdk代理了,我们只要把它交给cglib代理就可以了(具体看前面)。

  第三步使用:
  Controller层的使用:
/**  
    * 删除用户  
    *  
    * @param criteria 条件  
    * @param id       id  
    * @param model    模型  
    * @return 数据列表  
    */    
   @RequestMapping(value = "/delete")    
   //此处为记录AOP拦截Controller记录用户操作    
   @SystemControllerLog(description = "删除用户")    
    public String del(Criteria criteria, String id, Model model, HttpSession session) {    
        try {    
           User user = (User) session.getAttribute(WebConstants.CURRENT_USER);    
            if ( null != user) {    
                if (user.getId().equals(id)) {    
                   msg = "您不可以删除自己!";    
                   criteria = userService.selectByCriteriaPagination(criteria);    
               }  else {    
                   //删除数据并查询出数据    
                   criteria = userService.delete(id, criteria);    
                   msg = "删除成功!";    
               }    
           }    
       }  catch (Exception e) {    
           msg = "删除失败!";    
       }  finally {    
           model.addAttribute("msg", msg);    
           model.addAttribute("criteria", criteria);    
       }    
       //跳转列表页    
        return "user/list";    
   }    
     Service层的使用:
/**  
    * 按照分页查询  
    * @param criteria  
    * @return  
    */    
   //此处为AOP拦截Service记录异常信息。方法不需要加try-catch    
   @SystemServiceLog(description = "查询用户")    
    public Criteria<User> selectByCriteriaPagination(Criteria<User> criteria)    
   {    
       criteria.getList().get(0).getAccount();    
       //查询总数    
        long total=userMapper.countByCriteria(criteria);    
       //设置总数    
       criteria.setRowCount(total);    
       criteria.setList(userMapper.selectByCriteriaPagination(criteria));    
        return  criteria;    
   }    
        效果如下:

     (三)耗时统计

    作为程序员, 我们都知道服务监控对于一个服务能够长期稳定运行的重要性, 因此很多公司都有自己内部的监控报警系统, 或者是使用一些开源的系统, 例如小米的 Falcon 监控系统。
  那么在程序监控中, AOP 有哪些用武之地呢? 我们来假想一下如下场景:

  首先让我们来提炼一下需求:
  1、为服务中的每个方法调用进行调用耗时记录.
  2、将方法调用的时间戳, 方法名, 调用耗时上报到监控平台
  有了需求, 自然设计实现就很简单了。首先我们可以使用 around advice, 然后在方法调用前, 记录一下开始时间, 然后在方法调用结束后, 记录结束时间, 它们的时间差就是方法的调用耗时。我们来看一下具体的 aspect 实现,ExpiredAopAdviseDefine.java:

@Component
@Aspect
public class ExpiredAopAdviseDefine {
    private Logger logger = LoggerFactory.getLogger(getClass());

    // 定义一个 Pointcut, 使用 切点表达式函数 来描述对哪些 Join point 使用 advise.
    @Pointcut("within(SomeService)")
    public void pointcut() {
    }

    // 定义 advise
    // 定义 advise
    @Around("pointcut()")
    public Object methodInvokeExpiredTime(ProceedingJoinPoint pjp) throws Throwable {
        StopWatch stopWatch = new StopWatch();
        stopWatch.start();
        // 开始
        Object retVal = pjp.proceed();
        stopWatch.stop();
        // 结束

        // 上报到公司监控平台
        reportToMonitorSystem(pjp.getSignature().toShortString(), stopWatch.getTotalTimeMillis());

        return retVal;
    }


    public void reportToMonitorSystem(String methodName, long expiredTime) {
        logger.info("---method {} invoked, expired time: {} ms---", methodName, expiredTime);
        //
    }
}
      aspect 一开始定义了一个 pointcut, 匹配 SomeService 类下的所有的方法。
  接着呢, 定义了一个 around advice:

@Around("pointcut()")
public Object methodInvokeExpiredTime(ProceedingJoinPoint pjp) throws Throwable {
    StopWatch stopWatch = new StopWatch();
    stopWatch.start();
    // 开始
    Object retVal = pjp.proceed();
    stopWatch.stop();
    // 结束

    // 上报到公司监控平台
    reportToMonitorSystem(pjp.getSignature().toShortString(), stopWatch.getTotalTimeMillis());

    return retVal;
}
       advice 中的代码也很简单, 它使用了 Spring 提供的 StopWatch 来统计一段代码的执行时间。首先我们先调用 stopWatch.start() 开始计时, 然后通过 pjp.proceed() 来调用我们实际的服务方法, 当调用结束后, 通过 stopWatch.stop() 来结束计时。
  接着我们来写一个简单的服务, 这个服务提供一个 someMethod 方法用于模拟一个耗时的方法调用,SomeService.java:

@Service
public class SomeService {
    private Logger logger = LoggerFactory.getLogger(getClass());
    private Random random = new Random(System.currentTimeMillis());

    public void someMethod() {
        logger.info("---SomeService: someMethod invoked---");
        try {
            // 模拟耗时任务
            Thread.sleep(random.nextInt(500));
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
        这样当 SomeService 类下的方法调用时, 我们所提供的 advice 就会被执行, 因此就可以自动地为我们统计此方法的调用耗时, 并自动上报到监控系统中了。


阅读更多
换一批

没有更多推荐了,返回首页