AOP

名称:面向切面编程.

一句话总结: 在不改变原有代码的条件下,对功能进行扩展.
公式: AOP = 切入点表达式 + 通知方法.

专业术语:
1.连接点: 在执行正常的业务过程中满足了切入点表达式时进入切面的点.(织入) 多个(连接点(joinpoint):程序执行过程中某个特定的点,一般指被拦截到的的方法。)
2.通知: 在切面中执行的具体的业务(扩展) 方法(通知(Advice):在切面的某个特定连接点上执行的动作(扩展功能),例如around,before,after等。)
3.切入点: 能够进入切面的一个判断 if判断 一个(@Pointcut注解用于描述切面中的方法,并定义切面中的切入点)
4.目标方法: 将要执行的真实的业务逻辑.

1.前置通知: 目标方法执行之前执行
2.后置通知: 目标方法执行之后执行
3.异常通知: 目标方法执行之后抛出异常时执行
4.最终通知: 不管什么时候都要执行的方法.
说明:上述的四大通知类型不能控制目标方法是否执行.一般使用上述的四大通知类型,都是用来记录程序的执行状态.

5.环绕通知: 在目标方法执行前后都要执行的通知方法. 控制目标方法是否执行.并且环绕通知的功能最为强大.

切入点表达式说明

1). bean(bean的id) 类名首字母小写 匹配1个类
2). within(包名.类名) 按包路径匹配类 匹配多个类
上述表达式是粗粒度的控制,按类匹配.
3).execution(返回值类型 包名.类名.方法名(参数列表))
4).@annotation(包名.注解名) 按注解进行拦截.

用户行为日志记录实现(实践)

本小节作为课堂练习,以AOP方式记录项目中的用户行为信息,并将其存储到数据库。参考日志模块的文档。

在SysLogDao接口中添加,用于实现日志信息持久化的方法。关键代码如下:

int insertObject(SysLog entity);

在SysLogMapper.xml中添加insertObject元素,用于向日志表写入用户行为日志。关键代码如下:

   insert into sys_logs

   (username,operation,method,params,time,ip,createdTime)

   values

(#{username},#{operation},#{method},#{params},#{time},#{ip},#{createdTime})

第一步:在SysLogService接口中,添加保存日志信息的方法。关键代码如下:

void saveObject(SysLog entity)

第二步:在SysLogServiceImpl类中添加,保存日志的方法实现。关键代码如下:

    @Override

    public void saveObject(SysLog entity) {

      sysLogDao.insertObject(entity);

}

日志切面Aspect实现

业务描述与设计实现
在日志切面中,抓取用户行为信息,并将其封装到日志对象然后传递到业务,通过业务层对象对日志日志信息做进一步处理。

定义日志切面类对象,通过环绕通知处理日志记录操作。关键代码如下:

//@Order(1)
@Slf4j
@Aspect
@Component
public class SysLogAspect {
// private static final Logger log=LoggerFactory.getLogger(SysLogAspect.class);
/**
* @Pointcut 注解用于描述切入点(在哪些点上执行拓展业务)
* bean(bean对象名字):为一种切入点表达式(这个表达式中定义了哪个或哪些bean对象的方法要进行功能扩展).
* 例如,bean(sysUserServiceImpl)表达式表示名字为sysUserServiceImpl的bean对象中所有方法的集合为切入点,
* 也就是说这个sysUserServiceImpl对象中的任意方法执行时都要进行功能扩展.
*/
@Pointcut(“bean(sysUserServiceImpl)”)
//@Pointcut("@annotation(com.cy.cy.pj.common.annotation.RequiredLog)")
public void doLogPointCut() {
}// 此方法内部不需要写具体实现(方法的方法名也是任意)

/**
 * Around注解描述的方法为一个通知方法(服务增益方法),此方法内部可以做服务增益(拓展业务),@Around注解
 * 内部要指定切入点表达式,在此切入点表达式对应的切入点方法上做功能扩展.
 * 
 * @param jp 表示连接点,连接点是动态确定的,用于封装正在执行的切入点方法(目标方法)信息.
 * @return 目标方法的执行结果
 * @throws Throwable 通知方法执行过程中出现的异常
 */
@Around("doLogPointCut()")
public Object doLogAround(ProceedingJoinPoint jp) throws Throwable {
	// 1.记录方法开始执行时间
	try {
		long t1 = System.currentTimeMillis();
		log.info("start:{}", t1);
		// 2.执行目标方法
		Object result = jp.proceed();// 最终(中间还可以调用本类其它通知或其它切面的通知)会调用目标方法
		// 3.记录方法结束执行时间
		long t2 = System.currentTimeMillis();
		log.info("after:{}", t2);
		String targetClassMethod = getTargetClassMethod(jp);
		log.info("{}目标方法执行耗时:{}", targetClassMethod, (t2 - t1));
		// 4.将正常行为日志信息写入到数据库
		saveUserLog(jp, (t2 - t1));
		// 5.返回目标方法执行结果
		return result;// 目标方法的返回结果
	} catch (Throwable e) {
		log.error("目标方法执行时出现了异常:{}", e.getMessage());
		throw e;
	}
}

@Autowired
private SysLogService sysLogService;

/** 记录用户行为的正常信息 */
private void saveUserLog(ProceedingJoinPoint jp, long time) throws Exception {
	// 1.获取用户行为日志
	String ip = IPUtils.getIpAddr();
	String username =ShiroUtils.getUsername();// 将来是登录用户的用户名
	Date createdTime = new Date();
	// 获取操作名
	String operation = getOperation(jp);
	String method = getTargetClassMethod(jp);
	// String params=Arrays.toString(jp.getArgs());//普通格式字符串
	String params = new ObjectMapper().writeValueAsString(jp.getArgs());// json格式的参数
	// 2.封装用户行为日志(在内容中通过对象去实现)
	SysLog log = new SysLog();
	log.setIp(ip);
	log.setUsername(username);
	log.setCreateDate(createdTime);
	log.setOperation(operation);
	log.setMethod(method);
	log.setParams(params);
	log.setTime(time);
	
	//new Thread() {
		//public void run() {
			sysLogService.saveObject(log);
		//}
	//};
	// 3.将用户行为日志持久化(将内存中对象信息写入到数据库)
	
}

/** 获取目标方法上@ReqiredLog注解中定义的operation名字 */
private String getOperation(ProceedingJoinPoint jp) throws Exception {
	// 1.获取目标对象类型
	Class<?> targetCls = jp.getTarget().getClass();
	// 2.获取目标方法对象
	// 2.1获取方法签名信息
	MethodSignature ms = (MethodSignature) jp.getSignature();
	// 2.2获取方法对象
	// 假如底层配置为jdk代理,则method指向接口中的抽象方法对象.
	// 假如底层配置为CGLIB代理,则这个method指向具体目标对象中的方法对象
	// Method method=ms.getMethod();
	// 假如希望无论是jdk代理还是cglib代理,我们让method变量指向的都是目标对象中的方法对象,那如何实现?
	Method method = targetCls.getDeclaredMethod(ms.getName(), ms.getParameterTypes());
	// 3.获取方法上的reqiredLog注解对象
	RequiredLog requiredLog = method.getAnnotation(RequiredLog.class);
	// 4.获取注解中的operation的值.
	if (requiredLog == null)
		return "operation";
	return requiredLog.operation();
}

/** 获取目标方法的全限定名(目标类全名+方法名) */
private String getTargetClassMethod(ProceedingJoinPoint jp) {
	// 1.获取目标对象类型
	Class<?> targetCls = jp.getTarget().getClass();
	// 2.获取目标对象类型的类全名
	String targetClsName = targetCls.getName();
	// 3.获取目标方法名
	// 3.1获取方法签名(方法签名对象中封装了方法相关信息)
	// Signature s=jp.getSignature();
	MethodSignature ms = (MethodSignature) jp.getSignature();
	// 3.2基于方法签名获取方法名
	// String methodName=s.getName();
	String methodName = ms.getName();
	// 4.构建方法的全限定名并返回
	return targetClsName + "." + methodName;
}

}

利用反射获取注解

/** 获取目标方法上@ReqiredLog注解中定义的operation名字 */
private String getOperation(ProceedingJoinPoint jp) throws Exception {
// 1.获取目标对象类型
Class<?> targetCls = jp.getTarget().getClass();
// 2.获取目标方法对象
// 2.1获取方法签名信息
MethodSignature ms = (MethodSignature) jp.getSignature();
// 2.2获取方法对象
// 假如底层配置为jdk代理,则method指向接口中的抽象方法对象.
// 假如底层配置为CGLIB代理,则这个method指向具体目标对象中的方法对象
// Method method=ms.getMethod();
// 假如希望无论是jdk代理还是cglib代理,我们让method变量指向的都是目标对象中的方法对象,那如何实现?
Method method = targetCls.getDeclaredMethod(ms.getName(), ms.getParameterTypes());
// 3.获取方法上的reqiredLog注解对象
RequiredLog requiredLog = method.getAnnotation(RequiredLog.class);
// 4.获取注解中的operation的值.
if (requiredLog == null)
return “operation”;
return requiredLog.operation();
}

利用反射获取方法

/** 获取目标方法的全限定名(目标类全名+方法名) */
private String getTargetClassMethod(ProceedingJoinPoint jp) {
// 1.获取目标对象类型
Class<?> targetCls = jp.getTarget().getClass();
// 2.获取目标对象类型的类全名
String targetClsName = targetCls.getName();
// 3.获取目标方法名
// 3.1获取方法签名(方法签名对象中封装了方法相关信息)
// Signature s=jp.getSignature();
MethodSignature ms = (MethodSignature) jp.getSignature();
// 3.2基于方法签名获取方法名
// String methodName=s.getName();
String methodName = ms.getName();
// 4.构建方法的全限定名并返回
return targetClsName + “.” + methodName;
}

实现Redis缓存

自定义注解

@Retention(RetentionPolicy.RUNTIME) //该注解什么时候有效
@Target({ElementType.METHOD}) //对方法有效
public @interface CacheFind {
String key(); //该属性为必须添加
int seconds() default 0; //设定超时时间 默认不超时
}

编辑CacheAOP

package com.jt.aop;

import com.jt.anno.CacheFind;
import com.jt.pojo.ItemDesc;
import com.jt.util.ObjectMapperUtil;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import redis.clients.jedis.Jedis;

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

@Component //将对象交给spring容器管理
@Aspect //标识我是一个切面
public class CacheAOP {

//1.注入缓存redis对象
@Autowired
private Jedis jedis;

/**
 * 拦截@CacheFind注解标识的方法.
 * 通知选择: 缓存的实现应该选用环绕通知
 * 步骤:
 *  1.动态生成key  用户填写的key+用户提交的参数
 */
@Around("@annotation(cacheFind)")
public Object around(ProceedingJoinPoint joinPoint, CacheFind cacheFind){

    //1.如何获取用户在注解中填写的内容呢???  如何获取注解对象....
    String key = cacheFind.key();   //前缀  ITEM_CAT_PARENTID
    //2.如何获取目标对象的参数呢???
    Object[] array = joinPoint.getArgs();
    key += "::"+Arrays.toString(array); // "ITEM_CAT_PARENTID::[0]"

    //3.从redis中获取数据
    Object result = null;
    if(jedis.exists(key)){
        //需要获取json数据之后,直接转化为对象返回!!
        String json = jedis.get(key);
        //如何获取返回值类型
        MethodSignature methodSignature =
                        (MethodSignature) joinPoint.getSignature();
        Class targetClass = methodSignature.getReturnType();
        result = ObjectMapperUtil.toObject(json,targetClass);
        System.out.println("AOP实现缓存的查询!!!");
    }else{
        //key不存在,应该查询数据库
        try {
            result = joinPoint.proceed();    //执行目标方法,获取返回值结果
            String json = ObjectMapperUtil.toJSON(result);
            if(cacheFind.seconds()>0){       //判断是否需要超时时间
                jedis.setex(key, cacheFind.seconds(), json);
            }else{
                jedis.set(key,json);
            }
            System.out.println("AOP执行数据库操作!!!");
        } catch (Throwable throwable) {
            throwable.printStackTrace();
            throw new RuntimeException(throwable);
        }
    }
    return result;
}

关于环绕通知参数的说明
问题一:连接点必须位于通知的参数的第一位.

问题二: 其他四大通知了类型是否可以添加ProceedingJoinPoint对象
答案: ProceedingJoinPoint 只能添加到环绕通知中.

关于JoinPoint方法说明

/**
* 要求: 拦截注解方法
* 打印:
* 1.打印目标对象的类型
* 2.打印方法的参数
* 3.获取目标对象的名称及方法的名称
* @param joinPoint
*/
@Before("@annotation(com.jt.anno.CacheFind)")
public void before(JoinPoint joinPoint){

    Object target = joinPoint.getTarget();  //获取目标对象
    Object[] args = joinPoint.getArgs();    //获取方法参数的
    String targetName =
            joinPoint.getSignature().getDeclaringTypeName(); //获取目标对象的名称
    //获取目标对象的类型
    Class targetClass = joinPoint.getSignature().getDeclaringType();
    //获取目标方法的名称
    String methodName = joinPoint.getSignature().getName();
    System.out.println(target);
    System.out.println(args);
    System.out.println(targetName);
    System.out.println(targetClass);
    System.out.println(methodName);

}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值