Spring AOP简介

  • Post author:
  • Post category:其他


一AOP概述

1AOP的定义

AOP(Aspect Orient Programming)是一种设计思想,是软件设计领域中的面向切面编程,它是面向对象编程(OOP)的一种补充和完善。它以通过预编译方式和运行期动态代理方式,实现在不修改源代码的情况下给程序动态统一添加额外功能的一种技术。

2AOP的应用场景

实际项目中通常会将系统分为两大部分,一部分是核心业务,一部分是非核业务。在编程实现时我们首先要完成的是核心业务的实现,非核心业务一般是通过特定方式切入到系统中,这种特定方式一般就是借助AOP进行实现。

AOP就是要基于OCP(开闭原则),在不改变原有系统核心业务代码的基础上动态添加一些扩展功能并可以”控制”对象的执行。例如AOP应用于项目中的日志处理,事务处理,权限处理,缓存处理等等。

3AOP的应用原理

Spring AOP底层基于代理机制实现功能扩展:

  1. 假如目标对象(被代理对象)实现接口,则底层可以采用JDK动态代理机制为目标对象创建代理对象(目标类和代理类会实现共同接口)。
  2. 假如目标对象(被代理对象)没有实现接口,则底层可以采用CGLIB代理机制为目标对象创建代理对象(默认创建的代理类会继承目标对象类型)。

    说明:Spring boot2.x 中AOP现在默认使用的CGLIB代理,假如需要使用JDK动态代理可以在配置文件(applicatiion.properties)中进行如下配置:
spring.aop.proxy-target-class=false

4AOP的相关术语

▪ 切面(aspect): 横切面对象,一般为一个具体类对象(可以借助@Aspect声明)。

▪ 通知(Advice):在切面的某个特定连接点上执行的动作(扩展功能),例如around,before,after等。

▪ 连接点(joinpoint):程序执行过程中某个特定的点,一般指被拦截到的的方法。

▪ 切入点(pointcut):对多个连接点(Joinpoint)一种定义,一般可以理解为多个连接点的集合。

二Spring AOP快速实践

1业务描述

基于项目中的核心业务,添加简单的日志操作,借助SLF4J日志API输出目标方法的执行时长。

2项目创建与配置‘

创建maven项目或在已有项目基础上添加AOP启动依赖:

<dependency>
       <groupId>org.springframework.boot</groupId>
       <artifactId>spring-boot-starter-aop</artifactId>
</dependency>

说明:基于此依赖spring可以整合AspectJ框架快速完成AOP的基本实现。AspectJ 是一个面向切面的框架,他定义了AOP的一些语法,有一个专门的字节码生成器来生成遵守java规范的class文件。

3业务分析及实现

3.1创建日志切面类对象

将此日志切面类作为核心业务增强(一个横切面对象)类,用于输出业务执行时长,其关键代码如下:

package com.cy.pj.common.aspect;
@Aspect
@Slf4j
@Component
public class SysLogAspect {
	 @Pointcut("bean(sysUserServiceImpl)")
	 public void logPointCut() {}

	 @Around("logPointCut()")
	 public Object around(ProceedingJoinPoint jp)
	 throws Throwable{
		 try {
		   log.info("start:"+System.currentTimeMillis());
		   Object result=jp.proceed();//调用下一个切面方法或目标方法
		   log.info("after:"+System.currentTimeMillis());
		   return result;
		 }catch(Throwable e) {
		   log.error(e.getMessage());
		   throw e;
		 }
	 }
}

说明:

▪ @Aspect 注解用于标识或者描述AOP中的切面类型,基于切面类型构建的对象用于为目标对象进行功能扩展或控制目标对象的执行。

▪ @Pointcut注解用于描述切面中的方法,并定义切面中的切入点(基于特定表达式的方式进行描述),在本案例中切入点表达式用的是bean表达式,这个表达式以bean开头,bean括号中的内容为一个spring管理的某个bean对象的名字。

▪ @Around注解用于描述切面中方法,这样的方法会被认为是一个环绕通知(核心业务方法执行之前和之后要执行的一个动作),@Aournd注解内部value属性的值为一个切入点表达式或者是切入点表达式的一个引用(这个引用为一个@PointCut注解描述的方法的方法名)。

▪ ProceedingJoinPoint类为一个连接点类型,此类型的对象用于封装要执行的目标方法相关的一些信息。一般用于@Around注解描述的方法参数。

3.2业务切面测试实现

启动项目测试或者进行单元测试,其中Spring Boot项目中的单元测试代码如下:

@SpringBootTest
public class AopTests {
	 @Autowired
	 private SysUserService userService;
	 @Test
	 public void testSysUserService() {
		 PageObject<SysUserDeptVo> po=
		 userService.findPageObjects("admin",1);
		 System.out.println("rowCount:"+po.getRowCount());
	 }
}

三Spring AOP编程增强

1切面通知

@Component
@Aspect
public class SysTimeAspect {
	@Pointcut("bean(sysUserServiceImpl)")
	public void doTime(){}

	@Before("doTime()")
	public void doBefore(JoinPoint jp){
		System.out.println("time doBefore()");
	}
	@After("doTime()")
	public void doAfter(){
		System.out.println("time doAfter()");
	}
	/**核心业务正常结束时执行* 说明:假如有after,先执行after,再执行returning*/
	@AfterReturning("doTime()")
	public void doAfterReturning(){
		System.out.println("time doAfterReturning");
	}
	/**核心业务出现异常时执行说明:假如有after,先执行after,再执行Throwing*/
	@AfterThrowing("doTime()")
	public void doAfterThrowing(){
		System.out.println("time doAfterThrowing");
	}
	@Around("doTime()")
	public Object doAround(ProceedingJoinPoint jp)
			throws Throwable{
		System.out.println("doAround.before");
         try{
		 Object obj=jp.proceed();
           System.out.println("doAround.after");
          return obj;
		 }catch(Throwable e){
          System.out.println(e.getMessage());
          throw e;
         }
		
	}
}

▪ @Around 优先级最高)

▪ 对于@AfterThrowing通知只有在出现异常时才会执行,所以当做一些异常监控时可在此方法中进行代码实现。

常用业务:

定义一个异常监控切面,对目标页面方法进行异常监控,并以日志信息形式输出异常

package com.cy.pj.common.aspect;
import lombok.extern.slf4j.Slf4j;
@Slf4j
@Aspect
@Component
public class SysExceptionAspect {
	@AfterThrowing(pointcut="bean(*ServiceImpl)",throwing = "e")
	public void doHandleException(JoinPoint jp,Throwable e) {
		MethodSignature ms=(MethodSignature)jp.getSignature();
		log.error("{}'exception msg is
{}",ms.getName(),e.getMessage());
	}

}

说明:AfterThrowing中throwing属性的值,需要与它描述的方法的异常参数名相同。

2切入点表达式

2.1bean表达式

bean表达式一般应用于类级别,实现粗粒度的切入点定义,案例分析:

▪ bean(“userServiceImpl”)指定一个userServiceImpl类中所有方法。

▪ bean(“

ServiceImpl”)指定所有后缀为ServiceImpl的类中所有方法。

说明:bean表达式内部的对象是由spring容器管理的一个bean对象,表达式内部的名字应该是spring容器中某个bean的name。

2.2within表达式

within表达式应用于类级别,实现粗粒度的切入点表达式定义,案例分析:

▪ within(“aop.service.UserServiceImpl”)指定当前包中这个类内部的所有方法。

▪ within(“aop.service.

”) 指定当前目录下的所有类的所有方法。

▪ within(“aop.service…

“) 指定当前目录以及子目录中类的所有方法。

2.3execution表达式

execution表达式应用于方法级别,实现细粒度的切入点表达式定义,案例分析:

语法:execution(返回值类型 包名.类名.方法名(参数列表))。

▪ execution(void aop.service.UserServiceImpl.addUser())匹配addUser方法。

▪ execution(void aop.service.PersonServiceImpl.addUser(String)) 方法参数必须为String的addUser方法。

▪ execution(

aop.service…

.

(…)) 万能配置。

2.4@annotation表达式

@annotaion表达式应用于方法级别,实现细粒度的切入点表达式定义,案例分析

▪ @annotation(anno.RequiredLog) 匹配有此注解描述的方法。

▪ @annotation(anno.RequiredCache) 匹配有此注解描述的方法。

其中:RequiredLog为我们自己定义的注解,当我们使用@RequiredLog注解修饰业务层方法时,系统底层会在执行此方法时进行日扩展操作。

具体案例分析

定义一Cache相关切面,使用注解表达式定义切入点,并使用此注解对需要使用cache的业务方法进行描述,代码分析如下:

第一步:定义注解RequiredCache

package com.cy.pj.common.annotation;
/**
 * 自定义注解,一个特殊的类,所有注解都默认继承Annotation接口
 */
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface RequiredCache {
   //...
}

第二步:定义SysCacheAspect切面对象。

package com.cy.pj.common.aspect;
@Aspect
@Component
public class SysCacheAspect {
	    @Pointcut("@annotation(com.cy.pj.common.annotation.RequiredCache)")
	  public void doCache() {}
	  @Around("doCache()")
	  public Object around(ProceedingJoinPoint jp)throws Throwable{
		  System.out.println("Get data from cache");
		  Object obj=jp.proceed();
		  System.out.println("Put data to cache");
		  return obj;
	  }
    
}

第三步:使用@RequiredCache注解对特定业务目标对象中的查询方法进行描述。

 @RequiredCache
	@Override
	public List<Map<String, Object>> findObjects() {.
		return list;
	}

3切面优先级

切面的优先级需要借助@Order注解进行描述,数字越小优先级越高,默认优先级比较低。例如:

定义日志切面并指定优先级:

@Order(1)
@Aspect
@Component
public class SysLogAspect {}

定义缓存切面并指定优先级:

@Order(2)
@Aspect
@Component
public class SysCacheAspect {}

4用户行为日志记录实现

1定义注解

package com.cy.pj.common.annotation;

/**

  • 自定义注解,一个特殊的类,所有注解都默认继承Annotation接口

    */

    @Retention(RetentionPolicy.RUNTIME)

    @Target(ElementType.METHOD)

    public @interface RequiredLog {


    //…

    }

    使用@RequiredCache注解对用户模块业务层方法进行描述。

    2定义日志切面类对象,通过环绕通知处理日志记录操作。关键代码如下
@Aspect
@Component
public class SysLogAspect {
	private Logger log=LoggerFactory.getLogger(SysLogAspect.class);
    @Autowired
	private SysLogService sysLogService;
	@Pointcut("@annotation(com.cy.pj.common.annotation.RequiredLog)")
	public void logPointCut(){}
    @Around("logPointCut()")
    public Object around(ProceedingJoinPoint //连接点
    		jointPoint) throws Throwable{
    	long startTime=System.currentTimeMillis();
    	//执行目标方法(result为目标方法的执行结果)
    	Object result=jointPoint.proceed();
    	long endTime=System.currentTimeMillis();
    	long totalTime=endTime-startTime;
    	log.info("方法执行的总时长为:"+totalTime);
    	saveSysLog(point,totalTime);
    	return result;
    }
    //**面向对象中的封装思想**
    private void saveSysLog(ProceedingJoinPoint point,
    		  long totleTime) throws NoSuchMethodException, SecurityException, JsonProcessingException{
    	//**3**
    	//1.获取日志信息
    	MethodSignature ms= (MethodSignature)point.getSignature();
    	Class<?> targetClass=point.getTarget().getClass();
    	String className=targetClass.getName();
    	//获取接口声明的方法
    	String methodName=ms.getMethod().getName();
    	Class<?>[] parameterTypes=ms.getMethod().getParameterTypes();
    	//获取目标对象方法(AOP版本不同,可能获取方法对象方式也不同)
    	Method targetMethod=targetClass.getDeclaredMethod(
    			    methodName,parameterTypes);
       //获取用户名,学完shiro再进行自定义实现,没有就先给固定值
    	String username=ShiroUtils.getPrincipal().getUsername();
    	//获取方法参数
    	Object[] paramsObj=point.getArgs();
    	System.out.println("paramsObj="+paramsObj);
    	//将参数转换为字符串
    	String params=new ObjectMapper()
    	.writeValueAsString(paramsObj);
        //**2**
    	//2.封装日志信息
    	//**要一个对象就new一个对象(一种常用的编程思想)**
    	SysLog log=new SysLog();
    	log.setUsername(username);//登陆的用户
    	//假如目标方法对象上有注解,我们获取注解定义的操作值
    	RequiredLog requestLog=
    	targetMethod.getDeclaredAnnotation(RequiredLog.class);
 	   if(requestLog!=null){
    	log.setOperation(requestLog.value());
    	}
log.setMethod(className+"."+methodName);//className.methodName()
    	log.setParams(params);//method params
    	log.setIp(IPUtils.getIpAddr());//ip 地址
    	log.setTime(totleTime);//
    	log.setCreateDate(new Date());
    	//**1**
    	//3.保存日志信息
    	sysLogService.saveObject(log);
    }
}

Aspect切面中的代码,取代control层的功能

方法中用到的ip地址获取需要提供一个如下的工具类:(不用自己实现,直接用)

public class IPUtils {
	private static Logger logger = LoggerFactory.getLogger(IPUtils.class);
public static String getIpAddr() {
	HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
		String ip = null;
		try {
			ip = request.getHeader("x-forwarded-for");
			if (StringUtils.isEmpty(ip) || "unknown".equalsIgnoreCase(ip)) {
				ip = request.getHeader("Proxy-Client-IP");
			}
		if (StringUtils.isEmpty(ip) || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
				ip = request.getHeader("WL-Proxy-Client-IP");
			}
			if (StringUtils.isEmpty(ip) || "unknown".equalsIgnoreCase(ip)) {
				ip = request.getHeader("HTTP_CLIENT_IP");
			}
			if (StringUtils.isEmpty(ip) || "unknown".equalsIgnoreCase(ip)) {
				ip = request.getHeader("HTTP_X_FORWARDED_FOR");
			}
			if (StringUtils.isEmpty(ip) || "unknown".equalsIgnoreCase(ip)) {
				ip = request.getRemoteAddr();
			}
		} catch (Exception e) {
			logger.error("IPUtils ERROR ", e);
		}
		return ip;
	}
}

3编写数据层业务层代码

Dao

int insertObject(SysLog entity);
<insert id="insertObject">
       insert into sys_logs
       (username,operation,method,params,time,ip,createdTime)
       values
(#{username},#{operation},#{method},#{params},#{time},#{ip},#{createdTime})
</insert>

Service

void saveObject(SysLog entity)
@Override
	public void saveObject(SysLog entity) {
	  sysLogDao.insertObject(entity);
}

四Spring AOP事务处理

1事务管理实现

  1. 启用声明式事务管理,在配置类上添加@EnableTransactionManagement,新版本中也可不添加(例如新版Spring Boot项目)。
  2. 将@Transactional注解添加到合适的业务类或方法上,并设置合适的属性信息。
 @Transactional(timeout = 30,
               readOnly = false,
               isolation = Isolation.READ_COMMITTED,
               rollbackFor = Throwable.class,
               propagation = Propagation.REQUIRED)
  @Service
  public class implements SysUserService {
        @Transactional(readOnly = true)
    @Override
	 public PageObject<SysUserDeptVo> findPageObjects(
			String username, Integer pageCurrent) {}
}

其中,代码中的@Transactional注解用于描述类或方法,告诉spring框架我们要在此类的方法执行时进行事务控制,其具体说明如下:。

▪ 当@Transactional注解应用在类上时表示类中所有方法启动事务管理,并且一般用于事务共性的定义。

▪ 当@Transactional描述方法时表示此方法要进行事务管理,假如类和方法上都有@Transactional注解,则方法上的注解一般用于事务特性的定义。

@Transactional 常用属性应用说明:

▪ timeout:事务的超时时间,默认值为-1,表示没有超时显示。如果配置了具体时间,则超过该时间限制但事务还没有完成,则自动回滚事务。

▪ read-only:指定事务是否为只读事务,默认值为 false;为了忽略那些不需要事务的方法,比如读取数据,可以设置 read-only 为 true。

▪ rollback-for:用于指定能够触发事务回滚的异常类型,如果有多个异常类型需要指定,各类型之间可以通过逗号分隔。

▪ no-rollback- for 抛出 no-rollback-for 指定的异常类型,不回滚事务。

▪ isolation事务的隔离级别,默认值采用 DEFAULT。

2事务的传播性

▪ @Transactional(propagation=Propagation.REQUIRED)

@Transactional(propagation = Propagation.REQUIRED)
	@Override
	public List<Node> findZtreeMenuNodes() {
		return sysMenuDao.findZtreeMenuNodes();
	}

当有一个业务对象调用如上方法时,此方法始终工作在一个已经存在的事务方法,或者是由调用者创建的一个事务方法中。

▪ @Transactional(propagation=Propagation.REQUIRES_NEW)

@Transactional(propagation = Propagation.REQUIRES_NEW)
	@Override
	public void saveObject(SysLog entity) {
	  sysLogDao.insertObject(entity);
	}

当有一个业务对象调用如上业务方法时,此方法会始终运行在一个新的事务中。

五Spring AOP 异步操作实现

1在基于注解方式的配置中,借助@EnableAsync注解进行异步启动声明,Spring Boot版的项目中,将@EnableAsync注解应用到启动类上,代码示例如下:

 @EnableAsync //spring容器启动时会创建线程池
   @SpringBootApplication
   public class Application {
	public static void main(String[] args) {
		SpringApplication.run(Application.class, args);
	}
}

2在需要异步执行的业务方法上,使用@Async方法进行异步声明。

	@Async
	@Transactional(propagation = Propagation.REQUIRES_NEW)
	@Override
	public void saveObject(SysLog entity) {
      System.out.println("SysLogServiceImpl.save:"+
Thread.currentThread().getName());
	  sysLogDao.insertObject(entity);
	  //try{Thread.sleep(5000);}catch(Exception e) {}
	}

假如需要获取业务层异步方法的执行结果,可参考如下代码设计进行实现:

@Transactional(propagation = Propagation.REQUIRES_NEW)
   @Async
	@Override
	public Future<Integer> saveObject(SysLog entity) {
		System.out.println("SysLogServiceImpl.save:"+
Thread.currentThread().getName());
		int rows=sysLogDao.insertObject(entity);
		//try{Thread.sleep(5000);}catch(Exception e) {}
	    return new AsyncResult<Integer>(rows);
	}

其中,AsyncResult对象可以对异步方法的执行结果进行封装,假如外界需要异步方法结果时,可以通过Future对象的get方法获取结果。

当我们需要自己对spring框架提供的连接池进行一些简易配置,可以参考如下代码:

spring:
  task:
    execution:
      pool:
        queue-capacity: 128
        core-size: 5
        max-size: 128
        keep-alive: 60000
        thread-name-prefix: db-service-task-

对于spring框架中线程池配置参数的涵义,可以参考ThreadPoolExecutor对象中的解释。

六Spring AOP中Cache操作实现

1缓存场景分析

在业务方法中我们可能调用数据层方法获取数据库中数据,假如访问数据的频率比较高,为了提高的查询效率,降低数据库的访问压力,可以在业务层对数据进行缓存.

2Spring 中业务缓存应用实现

2.1启动缓存配置

在项目(SpringBoot项目)的启动类上添加@EnableCaching注解,以启动缓存配置。关键代码如下:

package com.cy;
/**
* 异步的自动配置生效).
 * @EnableCaching 注解表示启动缓存配置
 */
@EnableCaching
@SpringBootApplication
public class Application {
	public static void main(String[] args) {
		SpringApplication.run(Application.class, args);
	}

}

2.2业务方法上应用缓存配置

在需要进行缓存的业务方法上通过@Cacheable注解对方法进行相关描述.表示方法的

返回值要存储到Cache中,假如在更新操作时需要将cache中的数据移除,可以在更新方法上使用@CacheEvict注解对方法进行描述。例如:

第一步:在相关模块查询相关业务方法中,使用缓存,关键代码如下:

@Cacheable(value = "menuCache")
@Transactional(readOnly = true)
public List<Map<String,Object>> findObjects() {
....
}

其中,value属性的值表示要使用的缓存对象,名字自己指定,其中底层为一个map对象,当向cache中添加数据时,key默认为方法实际参数的组合。

第二步:在相关模块更新时,清除指定缓存数据,关键代码如下:

 @CacheEvict(value="menuCache",allEntries=true)
    @Override
    public int saveObject(SysDept entity) {...}

其中,allEntries表示清除所有。

七Spring AOP原生方式实现

应用于框架的编写中



版权声明:本文为weixin_49074309原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。