前言
概念
学习JAVA刚开始都会接触到OOP(封装 继承 多态),这些概念将对象公共的行为抽离出来,使代码变得简洁易维护。但如果要将不同的对象引入一个公共的行为时,OOP就比较无能为力了。比较典型的就是项目中的日志功能,这个功能散布于各个不同的对象的每个层次中(执行前,执行后,异常),如果在每个需要日志功能的地方都添加一个,将会出现大量冗余的代码,且这些代码本身和业务并无太大联系,也使得业务更难理解,甚至还要加上更多处理才能让这块代码不影响到核心业务的执行。这时就引入了AOP的概念,它将不同对象的不同层次定义为切面,AOP也是面向切面编程,将核心业务行为和公共通用行为分离开,这就是它的目的。
术语
通知/增强(Advice)
需要加入的功能,比如上面提到的日志。
连接点(JoinPoint)
项目执行中允许你通知的地方。
切入点(Pointcut)
切入点是在连接点的基础上定义的,比如一个类中有10个连接点,你只想通知其中5个,那这5个连接点你就可以定义为切入点。
切面(Aspect)
切入点和通知的结合,切入点表示“在什么地方做”,通知表示“做什么”和“什么时候做”,两个结合表明“在什么时候什么地方做什么”,这就是一个完整的切面定义。
引入(introduction)
向现有的类添加方法属性。就是将切面在目标类中使用。
织入(weaving)
将切面引入目标对象创建一个新的代理类的过程。
提示
文章示例使用JDK1.8+SpringBoot框架进行讲解。小伙伴们要有一定的SpringBoot使用经验。
准备工作
1.aop
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
2.junit
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.11</version>
<scope>test</scope>
</dependency>
正文
测试代码
@Service
public class IAopServiceImpl implements IAopService {
@Override
public String test1(String param) {
System.out.println("我是test1");
return "test1";
}
@Override
public String test2() {
System.out.println("我是test2");
return "test2";
}
@Override
public String test3() {
System.out.println("我是test3");
return "test3";
}
}
@RunWith(SpringRunner.class)
@SpringBootTest
public class AopApplicationTests {
@Autowired
IAopService iAopService;
@Test
public void test() {
iAopService.test1("参数1");
iAopService.test2();
iAopService.test3();
}
}
前置增强
在目标方法执行前织入增强处理。 下面的这段就是在测试代码执行前获得请求的参数和IP地址,并打印日志。
@Aspect
@Component
@Slf4j
public class LogAspect {
/**
* [定义切入点] service包下所有函数
* @author pengyu
* @param
* @return void
* @date 2020/12/23 11:18
*/
@Pointcut("execution(public * com.pengyu.aop.service..*.*(..))")
public void testLog(){}
/**
* [前置通知,方法执行前]
* @author pengyu
* @param joinPoint
* @return void
* @date 2020/12/23 11:18
*/
@Before("testLog()")
public void doBefore(JoinPoint joinPoint){
RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
HttpServletRequest request = (HttpServletRequest) requestAttributes.resolveReference(RequestAttributes.REFERENCE_REQUEST);
//获得请求参数 转成map
Map<String, Object> map = getNameAndValue(joinPoint);
//获得请求ip
String ip=getIpAddr(request);
log.info("这是前置增强,请求参数-->{},ip-->{}",map.toString(),ip);
}
private static Map<String, Object> getNameAndValue(JoinPoint joinPoint) {
Map<String, Object> param = new HashMap<>();
Object[] paramValues = joinPoint.getArgs();
String[] paramNames = ((CodeSignature) joinPoint.getSignature()).getParameterNames();
for (int i = 0; i < paramNames.length; i++) {
param.put(paramNames[i], paramValues[i]);
}
return param;
}
public static String getIpAddr(HttpServletRequest request) {
String ip = request.getHeader("x-forwarded-for");
if (ip != null && ip.length() != 0 && !"unknown".equalsIgnoreCase(ip)) {
// 多次反向代理后会有多个ip值,第一个ip才是真实ip
if (ip.indexOf(",") != -1) {
ip = ip.split(",")[0];
}
}
if (ip != null || "127.0.0.1".equals(ip)) {
ip = null;
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("Proxy-Client-IP");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("WL-Proxy-Client-IP");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("HTTP_CLIENT_IP");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("HTTP_X_FORWARDED_FOR");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("X-Real-IP");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getRemoteAddr();
}
return ip;
}
}
执行后,控制台打印:
2020-12-23 14:34:23.453 INFO 14388 --- [ main] com.pengyu.aop.core.LogAspect : 这是前置增强,请求参数-->{param=参数1},ip-->127.0.0.1
我是test1
2020-12-23 14:34:23.460 INFO 14388 --- [ main] com.pengyu.aop.core.LogAspect : 这是前置增强,请求参数-->{},ip-->127.0.0.1
我是test2
2020-12-23 14:34:23.460 INFO 14388 --- [ main] com.pengyu.aop.core.LogAspect : 这是前置增强,请求参数-->{},ip-->127.0.0.1
我是test3
可以看到,在每个方法执行前,都打印了一条日志,这就是前置增强。
后置增强
在目标方法执行后织入增强处理。 这里有两个注解需要注意以下,@After和@AfterReturning,@AfterReturning如果方法异常或其他原因中断了,是通知不到的,@After是任何情况都能通知得到。我们这里用@AfterReturning。
@Aspect
@Component
@Slf4j
public class LogAspect {
/**
* [定义切入点] service包下所有函数
* @author pengyu
* @param
* @return void
* @date 2020/12/23 11:18
*/
@Pointcut("execution(public * com.pengyu.aop.service..*.*(..))")
public void testLog(){}
/**
* [后置通知]
* @author pengyu
* @param joinPoint
* @param result 方法返回值
* @return void
* @date 2020/12/23 15:00
*/
@AfterReturning(value = "testLog()",returning = "result")
public void doBefore(JoinPoint joinPoint,Object result){
RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
HttpServletRequest request = (HttpServletRequest) requestAttributes.resolveReference(RequestAttributes.REFERENCE_REQUEST);
//获得请求参数 转成map
Map<String, Object> map = getNameAndValue(joinPoint);
//获得请求ip
String ip=getIpAddr(request);
log.info("这是后置增强,请求参数-->{},ip-->{},返回值-->{}",map.toString(),ip,result.toString());
}
private static Map<String, Object> getNameAndValue(JoinPoint joinPoint) {
Map<String, Object> param = new HashMap<>();
Object[] paramValues = joinPoint.getArgs();
String[] paramNames = ((CodeSignature) joinPoint.getSignature()).getParameterNames();
for (int i = 0; i < paramNames.length; i++) {
param.put(paramNames[i], paramValues[i]);
}
return param;
}
public static String getIpAddr(HttpServletRequest request) {
String ip = request.getHeader("x-forwarded-for");
if (ip != null && ip.length() != 0 && !"unknown".equalsIgnoreCase(ip)) {
// 多次反向代理后会有多个ip值,第一个ip才是真实ip
if (ip.indexOf(",") != -1) {
ip = ip.split(",")[0];
}
}
if (ip != null || "127.0.0.1".equals(ip)) {
ip = null;
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("Proxy-Client-IP");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("WL-Proxy-Client-IP");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("HTTP_CLIENT_IP");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("HTTP_X_FORWARDED_FOR");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("X-Real-IP");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getRemoteAddr();
}
return ip;
}
}
后置增强可以拿到方法返回值,所以我们在日志输出里增加了个返回值,运行效果如下:
我是test1
2020-12-23 15:04:11.870 INFO 26348 --- [ main] com.pengyu.aop.core.LogAspect : 这是后置增强,请求参数-->{param=参数1},ip-->127.0.0.1,返回值-->test1
我是test2
2020-12-23 15:04:11.872 INFO 26348 --- [ main] com.pengyu.aop.core.LogAspect : 这是后置增强,请求参数-->{},ip-->127.0.0.1,返回值-->test2
我是test3
2020-12-23 15:04:11.872 INFO 26348 --- [ main] com.pengyu.aop.core.LogAspect : 这是后置增强,请求参数-->{},ip-->127.0.0.1,返回值-->test3
后置增强相对于前置增强的位置变了,前置增强是在目标方法执行前,而后置增强是在目标方法执行后,并且可以获得返回值。
环绕增强
在目标方法执行前后织入增强处理。 环绕增强从名字就可以看出,可以针对目标方法前后都进行增强处理。 举个栗子,我们要在方法执行前日志打印参数,方法执行后打印返回值,就可以这么写:
@Aspect
@Component
@Slf4j
public class LogAspect {
/**
* [定义切入点] service包下所有函数
* @author pengyu
* @param
* @return void
* @date 2020/12/23 11:18
*/
@Pointcut("execution(public * com.pengyu.aop.service..*.*(..))")
public void testLog(){}
/**
* [环绕通知]
* @author pengyu
* @param joinPoint
* @return void
* @date 2020/12/23 15:10
*/
@Around("testLog()")
public void doBefore(ProceedingJoinPoint joinPoint) throws Throwable {
RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
//获得请求参数 转成map
Map<String, Object> map = getNameAndValue(joinPoint);
log.info("这是环绕增强,方法执行前,参数-->{}",map.toString());
Object result=joinPoint.proceed();
log.info("这是环绕增强,方法执行后,返回值-->{}",result);
}
private static Map<String, Object> getNameAndValue(JoinPoint joinPoint) {
Map<String, Object> param = new HashMap<>();
Object[] paramValues = joinPoint.getArgs();
String[] paramNames = ((CodeSignature) joinPoint.getSignature()).getParameterNames();
for (int i = 0; i < paramNames.length; i++) {
param.put(paramNames[i], paramValues[i]);
}
return param;
}
public static String getIpAddr(HttpServletRequest request) {
String ip = request.getHeader("x-forwarded-for");
if (ip != null && ip.length() != 0 && !"unknown".equalsIgnoreCase(ip)) {
// 多次反向代理后会有多个ip值,第一个ip才是真实ip
if (ip.indexOf(",") != -1) {
ip = ip.split(",")[0];
}
}
if (ip != null || "127.0.0.1".equals(ip)) {
ip = null;
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("Proxy-Client-IP");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("WL-Proxy-Client-IP");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("HTTP_CLIENT_IP");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("HTTP_X_FORWARDED_FOR");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("X-Real-IP");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getRemoteAddr();
}
return ip;
}
}
结果如下:
2020-12-23 15:17:52.731 INFO 22380 --- [ main] com.pengyu.aop.core.LogAspect : 这是环绕增强,方法执行前,参数-->{param=参数1}
我是test1
2020-12-23 15:17:52.739 INFO 22380 --- [ main] com.pengyu.aop.core.LogAspect : 这是环绕增强,方法执行后,返回值-->test1
2020-12-23 15:17:52.739 INFO 22380 --- [ main] com.pengyu.aop.core.LogAspect : 这是环绕增强,方法执行前,参数-->{}
我是test2
2020-12-23 15:17:52.739 INFO 22380 --- [ main] com.pengyu.aop.core.LogAspect : 这是环绕增强,方法执行后,返回值-->test2
2020-12-23 15:17:52.740 INFO 22380 --- [ main] com.pengyu.aop.core.LogAspect : 这是环绕增强,方法执行前,参数-->{}
我是test3
2020-12-23 15:17:52.740 INFO 22380 --- [ main] com.pengyu.aop.core.LogAspect : 这是环绕增强,方法执行后,返回值-->test3
不知道大家有没有注意到,环绕增强的入参由JoinPoint变成了ProceedingJoinPoint,点进去可以看到ProceedingJoinPoint继承了JoinPoint并且在原本的接口上暴露了proceed()方法。这个方法就是让目标方法执行,这样才能支持环绕通知。
结尾
上面主要讲到了前置增强,后置增强和环绕增强这三种常用的,还有其他的比如异常增强等其他增强下篇文章再讨论。如果发现错误或者我理解错了的地方,欢迎大家及时指正。