参考牛客网高级项目教程
狂神说Spring教程笔记
功能需求及处理策略
- 1.前面的异常处理,只是在组件发生异常时,会触发控制器,有一定局限性
- 如果想在业务组件中记录日志,需要用到aop编程,以及springAop提供的组件
- 2.即,无需在每个业务组件处理记录日志等系统业务,只需要通过代理模式,统一使用aop处理增强业务逻辑
- 3**.即将系统业务与处理业务分离,减少程序的耦合性、灵活性**
1. AOP概念回顾
1.1 AOP思想
- Aspect Oriented Programing,
- 即面向方面(切面)编程。
- 实现通过预编译方式和运行期动态代理实现程序功能的统一维护的一种技术
- AOP是一种编程思想,是对OOP的补充,
- 可以单独定义一个系统组件与业务组件独立,并管理业务组件,以代理模式对业务组件功能增强
- 利用AOP可以对业务逻辑的各个部分进行隔离,
- 从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率。
1.2 AOP相关概念
切面(ASPECT):
- 横切关注点 ,被模块化 的特殊对象。即,它是一个类。
- 在此类中定义系统组件,用来管理和增强业务组件
- 里面主要包括
- 切入点(PointCut):
- 先定位到要处理哪些方法
- 即织入到哪些连接点
- 通知(Advice):
- 再定位到方法中具体的逻辑位置:前、后、返回后、异常、环绕
- 以及在这些位置要增强哪些系统逻辑方法
- 切入点(PointCut):
切入点(PointCut):
- 切面通知 执行的 “地点”的定义。
- 声明切点:要处理哪些类的哪些方法的哪些参数,
通知(Advice):
- 切面必须要完成的工作。即,它是类中的一个方法。
- 定位到具体系统逻辑的具体位置
- before:连接点之前
- After:连接点之后
- AfterReturning:返回值之后
- AfterThrowing:抛出异常时织入
- Around:连接点前后都织入
目标(Target):
- 被通知的原始真实对象。
代理(Proxy):
- 向目标对象应用通知之后创建的代理对象。
- 动态代理一般都是对代理对象的操作
连接点(JointPoint):
- 具体每个业务组件织入的位置
- 与切入点匹配的执行点
三种织入方式:
-
编译时织入,需使用特殊的编译器
- 此种方式,编译器一级准备好了,运行时快,但对运行时的变量情况不明确,不灵活
-
装载时织入,需使用特殊的类装载器。
-
运行时织入,需为目标生成代理对象。
- 此种方式,灵活、但效率低些
1.3 AOP实现方式
AspectJ
- AspectJ是语言级的实现,它扩展了Java语言,定义了AOP语法。
- AspectJ在编译期织入代码,它有一个专门的编译器,用来生成遵守Java字节码规范的class文件。
- 因此不够灵活,可作为Spring AOP的补充
Spring AOP
- Spring AOP使用纯Java实现,它不需要专门的编译过程,也不需要特殊的类装载器。
- Spring AOP在运行时通过代理的方式织入代码,只支持方法类型的连接点。
- 其实大部分的实际开发中都是织入到方法中
- 因此,此种方式性比较最高
- Spring支持对AspectJ的集成。
- 如果有特殊织入点,比如不在方法中的,可以用AspectJ补充
SpringAop的两种动态代理方式
- 当目标对象有接口时,使用JDK动态代理
- 当目标对象没有接口时,使用CGLib动态代理,在子类实例中织入代码
JDK动态代理
- Java提供的动态代理技术,可以在运行时创建接口的代理实例。
- Spring AOP默认采用此种方式,在接口的代理实例中织入代码。
CGLib动态代理
- 采用底层的字节码技术,在运行时创建子类代理实例。
- 当目标对象不存在接口时,Spring AOP会采用此种方式,在子类实例中织入代码。
- java支持多态,可以用子类对象代替父类的引用
2. Spring AOP实现示例
2.1 实现的三种具体方式
1)通过 Spring API 实现
-
直接继承实现接口,重新实现的方法
-
前置增强-连接点之前处理
public class Log implements MethodBeforeAdvice { //method : 要执行的目标对象的方法 //objects : 被调用的方法的参数 //Object : 目标对象 @Override public void before(Method method, Object[] objects, Object o) throws Throwable { System.out.println( o.getClass().getName() + "的" + method.getName() + "方法被执行了"); } }
-
后置增强-连接点之后处理
public class AfterLog implements AfterReturningAdvice { //returnValue 返回值 //method被调用的方法 //args 被调用的方法的对象的参数 //target 被调用的目标对象 @Override public void afterReturning(Object returnValue, Method method, Object[] args, Object target) throws Throwable { System.out.println("执行了" + target.getClass().getName() +"的"+method.getName()+"方法," +"返回值:"+returnValue); } }
-
spring的文件中注册 , 并实现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:aop="http://www.springframework.org/schema/aop" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd"> <!--注册bean--> <bean id="userService" class="com.kuang.service.UserServiceImpl"/> <bean id="log" class="com.kuang.log.Log"/> <bean id="afterLog" class="com.kuang.log.AfterLog"/> <!--aop的配置--> <aop:config> <!--切入点 expression:表达式匹配要执行的方法--> <aop:pointcut id="pointcut" expression="execution(* com.kuang.service.UserServiceImpl.*(..))"/> <!--执行环绕; advice-ref执行方法 . pointcut-ref切入点--> <aop:advisor advice-ref="log" pointcut-ref="pointcut"/> <aop:advisor advice-ref="afterLog" pointcut-ref="pointcut"/> </aop:config> </beans>
-
2)自定义类来实现Aop-xml配置
-
可以自定义处理增强业务的类,然后在xml配置中注入到ioc容器中管理
public class DiyPointcut { public void before(){ System.out.println("---------方法执行前---------"); } public void after(){ System.out.println("---------方法执行后---------"); } }
<!--第二种方式自定义实现--> <!--注册bean--> <bean id="diy" class="com.kuang.config.DiyPointcut"/> <!--aop的配置--> <aop:config> <!--第二种方式:使用AOP的标签实现--> <aop:aspect ref="diy"> <aop:pointcut id="diyPonitcut" expression="execution(* com.kuang.service.UserServiceImpl.*(..))"/> <aop:before pointcut-ref="diyPonitcut" method="before"/> <aop:after pointcut-ref="diyPonitcut" method="after"/> </aop:aspect> </aop:config>
3) 自定义类来实现Aop-注解方式
-
与上述逻辑类似,采用注解方式,可以不用在xml配置中配置,直接使用注解注入即可
-
本项目采用的是第三种,具体示例如下:
2.2 使用注解方式实现Spring AOP示例
@Aspect
- 注明是切面组件,结合@Component,交给IOC管理
package com.nowcoder.community.aspect;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;
@Component
@Aspect
public class AlphaAspect {
}
@Pointcut(“execution(* com.nowcoder.community.service..(…))”)
-
声明切点-并定义织入位置
- 位置可以在直接的值中定义,比较灵活
/** * 声明切点-并定义织入位置 * 所有返回类型 * 所有service包下所有Service业务组件 * 所有业务组件下的所有方法 * 所有方法中的所有参数 */ @Pointcut("execution(* com.nowcoder.community.service.*.*(..))") public void pointCut() {}
Advice:具体位置及处理逻辑
- 示例中,采用打印到控制台的方法代理日志处理
@Before(“pointCut()”)
-
连接点之前织入
-
注解的值传入之前定义好的切点 pointCut()
/** * 连接点之前织入 */ @Before("pointCut()") public void before() { System.out.println("before"); }
@After(“pointCut()”)
-
连接点之后织入
-
注解的值传入之前定义好的切点 pointCut()
/** * 连接点之后织入 */ @After("pointCut()") public void after() { System.out.println("after"); }
@AfterReturning(“pointCut()”)
-
方法返回后织入
-
注解的值传入之前定义好的切点 pointCut()
/** * 方法返回后织入 */ @AfterReturning("pointCut()") public void afterReturning() { System.out.println("afterReturning"); }
@AfterThrowing(“pointCut()”)
-
方法出现异常时织入
-
注解的值传入之前定义好的切点 pointCut()
/** * 方法出现异常时织入 */ @AfterThrowing("pointCut()") public void afterThrowing() { System.out.println("afterThrowing"); }
@Around(“pointCut()”)
-
连接点前后均织入
-
注解的值传入之前定义好的切点 pointCut()
/** * 连接点前后均织入 * @param joinPoint 原始目标子类代理对象的连接点 * @return 原始目标子类代理组件连接点中执行方法后的返回值 * @throws Throwable */ @Around("pointCut()") public Object around(ProceedingJoinPoint joinPoint) throws Throwable { System.out.println("around before"); Object obj = joinPoint.proceed(); System.out.println("around after"); return obj; }
测试结果
3. 对所有业务组件统计记录日志
@Aspect 定义切面组件
- 工厂模式创建日志对象
@Component
@Aspect
public class ServiceAspect {
private Logger logger = LoggerFactory.getLogger(this.getClass());
}
定义系统组件逻辑
@Pointcut定义切点
/**
* 定义切点
*/
@Pointcut("execution(* com.nowcoder.community.service.*.*(..))")
public void pointCut() {}
@Before业务组件逻辑之前记录日志
JoinPoint:连接点
-
通过连接点,可定位到具体的业务组件子类代理对象
/** * 在连接点之前-处理业务组件之前记录日志 * @param joinPoint */ @Before("pointCut()") public void before(JoinPoint joinPoint) { }
RequestContextHolder.getRequestAttributes()
-
上下文组件容器可以获取请求上下文
// 用户[1.2.3.4],在[xxx],访问了[com.nowcoder.community.service.xxx()]. // 获取用户ip // 获取请求上下文 ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
requestAttributes.getRequest()
-
获取请求
// 获取请求 HttpServletRequest request = requestAttributes.getRequest();
joinPoint.getSignature()
-
此方法可以获取原始方法的反射信息接口
// 连接点织入的业务组件子类代理(原始方法)信息-类全路径名-方法名 String target = joinPoint.getSignature().getDeclaringTypeName() + "." + joinPoint.getSignature().getName();
@Component
@Aspect
public class ServiceAspect {
private Logger logger = LoggerFactory.getLogger(this.getClass());
/**
* 定义切点
*/
@Pointcut("execution(* com.nowcoder.community.service.*.*(..))")
public void pointCut() {}
/**
* 在连接点之前-处理业务组件之前记录日志
* @param joinPoint
*/
@Before("pointCut()")
public void before(JoinPoint joinPoint) {
// 用户[1.2.3.4],在[xxx],访问了[com.nowcoder.community.service.xxx()].
// 获取用户信息
// 获取请求上下文
ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
// 获取请求
HttpServletRequest request = requestAttributes.getRequest();
// 用户ip
String ip = request.getRemoteHost();
// 当前时间
String now = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date());
// 连接点织入的业务组件子类代理(原始方法)信息-类全路径名-方法名
String target = joinPoint.getSignature().getDeclaringTypeName() +
"." + joinPoint.getSignature().getName();
// String target = joinPoint.getSignature().toString();
// 记录日志
logger.info(String.format("用户[%s], 在[%s], 访问了[%s].", ip, now, target));
}
}