Spring中AOP总结

406 阅读10分钟

Spring中AOP总结

本文将给各位带来Spring中一个重要的知识点----Spring AOP

一、什么是AOP

1. 简介:
1.从最初的C语言开始,我们都是面向过程编程,直接了断。后来到我们Java语言,摒弃了C、C++语言的繁琐,我们变成面向对象编程,这样我们对类的编程有了更好的管理及维护。我们再学习了Spring框架,我们接触了AOP,面向切面编程,真的是越来简便。那我们来聊一聊什么是AOP吧!
2.面向切面对象(aspect-oriented programming)是一种将横切关注点与业务逻辑分离的编程方式。每个横切关注点都集中在一个地方,而不是分散在多处代码中。这样使我们的服务模块更加简洁,因为它们只包含了主要点的代码,而次要的功能或者辅助的功能被转移到切面中了。
2. AOP主要应用场景:
1. Authentication 权限
2. Caching 缓存
3. Context passing 内容传递
4. Error handing 错误处理
5. Lazy loding 懒加载
6. Debugging 调试
7. logging,tracing,profiling and monitoring 记录 跟踪 优化 校准
8. Performance optimzation 性能优化
9. Persistence 持久化
10. Resource pooling 资源池
11. Synchronization 同步
12. Transaction 事务

二、AOP核心知识点

1. 主要术语
  • Aspect(切面):切入业务流程的一个独立的模块,在一个应用程序可以插入任意数量的切面。

    如事物管理,就是切面的一个应用例子

  • Join Point(连接点):业务流程在运行过程中需要插入切面的具体位置。如执行某个特定的方法或者处理异常的时候

  • Advice(通知):是切面的具体实现方法。可分为前置通知(Before)、后置通知(AfterReturning)、异常通知(AfterThrowing)、最终通知(After)和环绕通知(Arounf)五种。实现方法具体属于哪类通知,是在配置文件和注解中指定的。

  • Pointcut(切入点):用于定义通知应该到哪些连接点,不同的通知通常需要切入到不同的连接点上。

  • Target(目标对象):被一个或者多个切面所通知的对象

  • Proxy(代理对象):将通知应用到目标对象之后被动态创建的对象。可以简单理解为,代理为,代理对象为目标对象的业务逻辑功能加上被切入的切面所形成的对象。

  • Weaving(切入):将切面应用到目标对象从而创建一个新的代理对象的过程。这个过程可以发生在编译期、类装在期及运行期。

2. 通知类型
Spring AOP 主要有五种通知类型,分别是前置通知(Before)、后置通知(AfterReturning)、异常通知(AfterThrowing)、最终通知(After)、环绕通知(Around)五种通知类型
  • Before(前置通知):在连接点之前完成之前执行的通知,但这个通知不能阻止连接点之前的执行流程(除非它抛出一个异常)
  • AfteringReturning(后置通知):在连接点正常执行完成之后的通知
  • AtferThrowing(异常通知):在连接点方法抛出异常时执行的通知
  • After(最终通知):在连接点执行完成后的通知(无论是正常执行,还是抛出异常退出执行)
  • Around(环绕通知):可以在连接点执行前后都执行的通知

三、AOP的两种代理啊方式

   ```

Spring提供了两种方式生成代理对象:JDKProxy和Cglib,具体使用哪种方式生成由AopProxyFactory根据AdvisedSupport对象的配置决定。默认的策略是如果目标类是接口,则使用JDK动态代理技术,否则使用Cglib来生成代理 ```

1. JDK动态接口代理
JDK动态代理只要涉及到Java.lang.reflect包中的两个类:ProxyInvocationHandlerInvocationHandler是一个接口,通过实现接口定义横切逻辑,并通过反射机制调用目标代码,动态将横切逻辑和业务逻辑编制在一起。Proxy利用InvocationHandler动态创建一个符合某一接口的实例,生成目标类的代理对象
2.CGLIB动态代理
CGLIB全称为Code Generation Libraay,是一个强大的高性能,高质量的代码生成类库,可以在运行期间扩展扩展Java类与实现Java接口,CGLIB封装了asm,可以在运行期间生成新的class。
和JDK动态代理相比较:JDK创建代理又一个限制,就是只能为创建对象代理实例,而没有通过接口定义业务方法的类,则通过CGLIB创建动态代理。

AOP的简单介绍就到这里了,那么我们开始我们的代理理解。。。😂😄

AOP简单实现(这里我们使用注解)

一、创建我们的SpringBoot项目

1. 还是老套路(先上我们的Pom文件)

<dependency>
           <groupId>org.springframework.boot</groupId>
           <artifactId>spring-boot-starter-aop</artifactId>
       </dependency>
       <!-- AOP -->
       <dependency>
           <groupId>org.springframework.boot</groupId>
           <artifactId>spring-boot-starter-web</artifactId>
       </dependency>
       <!-- slf4j -->
       <dependency>
           <groupId>org.slf4j</groupId>
           <artifactId>slf4j-api</artifactId>
       </dependency>

2. 规矩懂涩,依然是我们的application.yml文件

这里我们的配置文件什么也没有,启动端口默认是8080

3. 然后开始上我们的正餐(创建我们的基本对象了哦)

TestAspect


package com.example.demo.apect;
import org.aspectj.lang.annotation.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;

@Aspect
@Component
public class TestAspect {

  private static final Logger LOGGER = LoggerFactory.getLogger(TestAspect.class);
  
  /**
   * 切点
   */
  
  @Pointcut("execution(public * com.example.demo.controller..*(..))")
  public void TestPointCut(){};


  /**
   * 前置通知
   */
  @Before("TestPointCut()")
  public void before(){
  
      LOGGER.info("我是前置通知");
  }


  /**
   * 后置通知
   */
  @AfterReturning("TestPointCut()")
  public void afterReturning(){
  
      LOGGER.info("我是后置通知");
  }


  /**
   * 异常通知
   */
  @AfterThrowing("TestPointCut()")
  public void afterThrowing(){
  
      LOGGER.error("我是异常通知");
  }
  
  @After("TestPointCut()")
  public void after(){
  
      LOGGER.info("我是最终通知");
  }

}

上面的代码都是看的懂的吧,没有什么理解深入的地方,这里带领大家怎么使用,具体底层还需要大家自己去理解

它是怎么监控我们Controller?哦,原来是注解

@Pointcut("execution(public * com.example.demo.controller..*(..))")

这里的规则是,监控controller包及子包下面的所有public方法
  
  怪不得,这样啊!!!!
2. 创建我们测试Controller(开始测试我们的代码)

TestController


package com.example.demo.controller;


import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("test")
public class TestController {


   /**
     正常
   **/
   @GetMapping("success")
   public String testSuccess() {
   
       return "success";
   }

}

我们可以通过PostMan测试我们的接口

http://localhsot:8080/test/success

image-20200330185753987
我们从上面看出我们在代码没有错误的情况下,我们输出了我们的日志信息
2.1 接下来修改我们的Controller方法
package com.example.demo.controller;


import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("test")
public class TestController {


    /**
     * 正常
     * @return
     */
    @GetMapping("success")
    public String testSuccess() {

        return "success";
    }

    /**
     * 出现异常
     * @return
     */
    @GetMapping("throw")
    public String testThrow() throws Exception{

        Integer age  = 10 / 0;

        return "throw";

    }
}

测试接口:http://localhost:8080/test/throw

image-20200330211744779

我们从上面图片中看到,当我们出现异常时会监听到,打印我们得错误日志

还有重要的一点是,不管我们有没有出现异常,我们的最终通知都会执行。
2.2 接下来修改我们的TestaAspect类(测试我们的环绕通知)
package com.example.demo.apect;


import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;

@Aspect
@Component
public class TestAspect {


    private static final Logger LOGGER = LoggerFactory.getLogger(TestAspect.class);

    /**
     * 切点
     */

    @Pointcut("execution(public * com.example.demo.controller..*(..))")
    public void TestPointCut(){};


    /**
     * 前置通知
     */
    @Before("TestPointCut()")
    public void before(){

        LOGGER.info("我是前置通知");
    }


    /**
     * 后置通知
     */
    @AfterReturning("TestPointCut()")
    public void afterReturning(){

        LOGGER.info("我是后置通知");
    }


    /**
     * 异常通知
     */
    @AfterThrowing(value = "TestPointCut()", throwing = "e")
    public void afterThrowing(JoinPoint joinPoint, Exception e){

        LOGGER.error("我是异常通知");
    }


    /**
     * 最终通知
     */
    @After("TestPointCut()")
    public void after(){

        LOGGER.info("我是最终通知");
    }


    /**
     * 环绕通知
     * @param proceedingJoinPoint
     * @return
     */
    @Around("TestPointCut()")
    public Object around(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {

        LOGGER.info("环绕通知之前");

        Object proceed = proceedingJoinPoint.proceed();

        LOGGER.info("环绕通知之后");

        return proceed;
    }


}

测试接口:http://localhost:8080/test/success,http://localhost:8080/test/throw

最终的效果就是这样的

image-20200330214005630

二、备注说明:

  • ​ 在AOP中不管有没有出现异常信息,都会执行我们的最终通知
  • ​ 出现异常后,不会执行我们的后置通知。那是肯定涩,如果执行了,那还了得,若将AOP用在我们的银行中来使用,异常后,执行后置通知,那的下场,惨不忍睹。。。
  • ​ 环绕通知是我们在项目中使用的最多的通知,我们可以在环绕通知中获取很多信息,比如登陆权限啊,请求方式、请求方法、请求参数等等信息。可以在我们业务逻辑之前作出判断,这样我们的系统才更加稳固强壮。

实战操作(对日志的操作)

一、创建一个SpringBoot项目

1. 不用多说了,直接就是我们的POM文件
<dependencies>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</artifactId>
		</dependency>
		
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-test</artifactId>
			<scope>test</scope>
		</dependency>
		
		<dependency>
		    <groupId>org.springframework.boot</groupId>
		    <artifactId>spring-boot-starter-jdbc</artifactId>
		</dependency>
		
		<!-- aop依赖 -->
		<dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
        </dependency>
		
		<!-- mysql驱动 -->
		<dependency>
			<groupId>mysql</groupId>
			<artifactId>mysql-connector-java</artifactId>
		</dependency>
	</dependencies>
2. 还是我们熟悉的yml文件:

在这里没有什么配置选项,只需要配置好我们的端口好就行了

3. 创建我们的基本类对象:
3.1 首先我们要自定义一个注解:
Log
package com.springboot.annotation;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Log {
	String value() default "";
}

这里呢,自定义注解能够更好的融合到我们的AOP日志中

3.2 创建domain
SysLog
package com.springboot.domain;

import java.io.Serializable;
import java.util.Date;

public class SysLog implements Serializable{

	private static final long serialVersionUID = -6309732882044872298L;
	
	private Integer id;
	private String username;
	private String operation;
	private Integer time;
	private String method;
	private String params;
	private String ip;
	private Date createTime;
	public Integer getId() {
		return id;
	}
	public void setId(Integer id) {
		this.id = id;
	}
	public String getUsername() {
		return username;
	}
	public void setUsername(String username) {
		this.username = username;
	}
	public String getOperation() {
		return operation;
	}
	public void setOperation(String operation) {
		this.operation = operation;
	}
	public Integer getTime() {
		return time;
	}
	public void setTime(Integer time) {
		this.time = time;
	}
	public String getMethod() {
		return method;
	}
	public void setMethod(String method) {
		this.method = method;
	}
	public String getParams() {
		return params;
	}
	public void setParams(String params) {
		this.params = params;
	}
	public String getIp() {
		return ip;
	}
	public void setIp(String ip) {
		this.ip = ip;
	}
	public Date getCreateTime() {
		return createTime;
	}
	public void setCreateTime(Date createTime) {
		this.createTime = createTime;
	}
	
}
3.3 创建我们的工具类:
HttpContextUtils
package com.springboot.util;

import javax.servlet.http.HttpServletRequest;

import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

public class HttpContextUtils {
	public static HttpServletRequest getHttpServletRequest() {
		return ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
	}
}

IPUtils
package com.springboot.util;

import javax.servlet.http.HttpServletRequest;

public class IPUtils {

	/**
	 * 获取IP地址
	 * 
	 * 使用Nginx等反向代理软件, 则不能通过request.getRemoteAddr()获取IP地址
	 * 如果使用了多级反向代理的话,X-Forwarded-For的值并不止一个,而是一串IP地址,X-Forwarded-For中第一个非unknown的有效IP字符串,则为真实IP地址
	 */
	public static String getIpAddr(HttpServletRequest request) {

		String ip = request.getHeader("x-forwarded-for");
		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.getRemoteAddr();
		}
		return "0:0:0:0:0:0:0:1".equals(ip) ? "127.0.0.1" : ip;
	}

}
4. 创建我们今天的主角:(AOP实现)
LogAspect
package com.springboot.aspect;

import java.lang.reflect.Method;
import java.util.Date;

import javax.servlet.http.HttpServletRequest;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.LocalVariableTableParameterNameDiscoverer;
import org.springframework.stereotype.Component;

import com.springboot.annotation.Log;
import com.springboot.dao.SysLogDao;
import com.springboot.domain.SysLog;
import com.springboot.util.HttpContextUtils;
import com.springboot.util.IPUtils;

@Aspect
@Component
public class LogAspect {

	@Autowired
	private SysLogDao sysLogDao;

	@Pointcut("@annotation(com.springboot.annotation.Log)")
	public void pointcut() {
	}

	@Around("pointcut()")
	public void around(ProceedingJoinPoint point) {
		long beginTime = System.currentTimeMillis();
		try {
			// 执行方法
			point.proceed();
		} catch (Throwable e) {
			e.printStackTrace();
		}
		// 执行时长(毫秒)
		long time = System.currentTimeMillis() - beginTime;
		// 保存日志
		saveLog(point, time);
	}

	private void saveLog(ProceedingJoinPoint joinPoint, long time) {
		MethodSignature signature = (MethodSignature) joinPoint.getSignature();
		Method method = signature.getMethod();
		SysLog sysLog = new SysLog();
		Log logAnnotation = method.getAnnotation(Log.class);
		if (logAnnotation != null) {
			// 注解上的描述
			sysLog.setOperation(logAnnotation.value());
		}
		// 请求的方法名
		String className = joinPoint.getTarget().getClass().getName();
		String methodName = signature.getName();
		sysLog.setMethod(className + "." + methodName + "()");
		// 请求的方法参数值
		Object[] args = joinPoint.getArgs();
		// 请求的方法参数名称
		LocalVariableTableParameterNameDiscoverer u = new LocalVariableTableParameterNameDiscoverer();
		String[] paramNames = u.getParameterNames(method);
		if (args != null && paramNames != null) {
			String params = "";
			for (int i = 0; i < args.length; i++) {
				params += "  " + paramNames[i] + ": " + args[i];
			}
			sysLog.setParams(params);
		}
		// 获取request
		HttpServletRequest request = HttpContextUtils.getHttpServletRequest();
		// 设置IP地址
		sysLog.setIp(IPUtils.getIpAddr(request));
		// 模拟一个用户名
		sysLog.setUsername("mrbird");
		sysLog.setTime((int) time);
		Date date = new Date();
		sysLog.setCreateTime(date);
		// 保存系统日志
		sysLogDao.saveSysLog(sysLog);
	}
}

可能会有报错信息,那是因为我们的有些类还没有创建

5. 创建Dao
SysLogDao
package com.springboot.dao;

import com.springboot.domain.SysLog;

public interface SysLogDao {
	void saveSysLog(SysLog syslog);
}

6. 创建DaoIpl
SysLogDaoImp
package com.springboot.dao.impl;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.namedparam.BeanPropertySqlParameterSource;
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;
import org.springframework.stereotype.Repository;

import com.springboot.dao.SysLogDao;
import com.springboot.domain.SysLog;

@Repository
public class SysLogDaoImp implements SysLogDao {

	@Autowired
	private JdbcTemplate jdbcTemplate;

	@Override
	public void saveSysLog(SysLog syslog) {
		StringBuffer sql = new StringBuffer("insert into sys_log ");
		sql.append("(id,username,operation,time,method,params,ip,create_time) ");
		sql.append("values(seq_sys_log.nextval,:username,:operation,:time,:method,");
		sql.append(":params,:ip,:createTime)");

		NamedParameterJdbcTemplate npjt = new NamedParameterJdbcTemplate(this.jdbcTemplate.getDataSource());
		npjt.update(sql.toString(), new BeanPropertySqlParameterSource(syslog));
	}
}

OK,这里我们的业务逻辑就全部完成了,接下来是我们的Controller层

7. 创建我们的Controller
TestController
package com.springboot.controller;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import com.springboot.annotation.Log;

@RestController
public class TestController {

  @Log("执行方法一")
  @GetMapping("/one")
  public void methodOne(String name) {
  	
  }

  @Log("执行方法二")
  @GetMapping("/two")
  public void methodTwo() throws InterruptedException {
  	Thread.sleep(2000);
  }

  @Log("执行方法三")
  @GetMapping("/three")
  public void methodThree(String name, String age) {
  	
  }
}

二、补充说明

  • ​ 这里没有附上数据库,可以自己去设置添加,在配置文件中配置好
  • ​ AOP做日志也很多的,可以有效的监控我们的请求、方法、参数等

感谢你的阅读,不知道有没有帮助到你,可以在留言区留言,🙏🙏