springAOP及一些简单的应用(二)
利用 AOP 实现日志处理
这是AOP最常用的用法,基本已经普及
日志注解
为了获取操作描述和是否存数据库,同时方便定义统一的切入点,先定义一个注解
package com.aop.annotation;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface ActionLog {
// 操作名称
String name();
// 是否保存数据库
boolean intoDb() default false;
}
其中name是对操作的描述,intoDb表示是否持久日志信息到数据库
实现切面
首先定义一个实体类来存日志信息(字段可以根据需求增减)
public class ActionLogDTO implements Serializable {
private static final long serialVersionUID = 1L;
private Long id;
/**
* 方法名称
*/
private String mothodName;
/**
* 描述
*/
private String Operation;
/**
* 参数
*/
private String args;
/**
* 运行时长
*/
private Long time;
/**
* 异常信息
*/
private String errorInfo;
.......省略set/get方法
}
然后写一个切面类
package com.aop.aspect;
import java.util.Arrays;
import java.util.Optional;
import org.aspectj.lang.JoinPoint;
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.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import com.aop.annotation.ActionLog;
import com.aop.dto.ActionLogDTO;
@Aspect
@Component
public class ActionLogAspect {
private static final Logger logger = LoggerFactory.getLogger(ActionLogAspect.class);
/**
* 切入点
*/
@Pointcut("@annotation(com.aop.annotation.ActionLog)")
public void actionLogCut() {
}
@Around(value = "actionLogCut()&&@annotation(actionLogAnnoAction)", argNames = "actionLogAnnoAction")
public Object doAround(ProceedingJoinPoint joinPoint, ActionLog actionLogAnnoAction) {
logger.info("方法开始调用");
long beginTime = System.currentTimeMillis();
String errorinfo = "";
Object result = null;
// 执行方法
try {
result = joinPoint.proceed();
} catch (Throwable e) {
errorinfo = e.getMessage();
}
// 执行时长(毫秒)
long time = System.currentTimeMillis() - beginTime;
logger.info("方法结束调用");
saveActionLog(joinPoint, actionLogAnnoAction, time, errorinfo);
return result;
}
private void saveActionLog(JoinPoint joinPoint, ActionLog actionLogAnnoAction, Long time, String errorMessage) {
// 获取切入点所在的类的类名
String className = joinPoint.getTarget().getClass().getName();
// 获取切入点的方法签名
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
// 获取切入点的方法名
String methodName = signature.getName();
ActionLogDTO actionLog = new ActionLogDTO();
actionLog.setId(System.currentTimeMillis());
actionLog.setMothodName(className + "." + methodName);
actionLog.setTime(time);
actionLog.setErrorInfo(errorMessage);
// 获取参数
String args = "";
Object[] argArr = joinPoint.getArgs();
if (argArr != null && argArr.length > 0) {
Optional<Object> reduce = Arrays.stream(argArr).reduce((s1, s2) -> s1.toString() + "," + s2.toString());
args = reduce.orElse("").toString();
}
actionLog.setArgs(args);
String operation = actionLogAnnoAction.name();
actionLog.setOperation(operation);
boolean intoDb = actionLogAnnoAction.intoDb();
if (intoDb) {
logger.info("日志" + actionLog.toString());
}
}
}
写一个环绕通知即可
测试
@Component
public class TestService {
@ActionLog(name = "加法运算", intoDb = true)
public int getSum(int arg1, int arg2) {
for (int i = 0; i < 10000; i++) {
}
return arg1 + arg2;
}
}
@RunWith(SpringRunner.class)
@SpringBootTest(classes = SpringaopApplication.class)
public class SpringaopApplicationTests {
@Autowired
private TestService testService;
@Test
public void contextLoads() {
int sum = testService.getSum(2, 3);
System.out.println(sum);
}
}
2019-09-23 03:25:29.840 INFO 5240 --- [ main] com.aop.aspect.ActionLogAspect : 方法开始调用
2019-09-23 03:25:29.855 INFO 5240 --- [ main] com.aop.aspect.ActionLogAspect : 方法结束调用
2019-09-23 03:25:29.855 INFO 5240 --- [ main] com.aop.aspect.ActionLogAspect : 日志ActionLog [id=1569180329855, mothodName=com.aop.test.TestService.getSum, Operation=加法运算, args=2,3, time=15, errorInfo=]
5
2019-09-23 03:25:29.855 INFO 5240 --- [ Thread-2] o.s.s.concurrent.ThreadPoolTaskExecutor : Shutting down ExecutorService 'applicationTaskExecutor'
利用AOP+redis实现分布式锁
lock注解
定义了锁key,超时时间,超时时间单位
package com.aop.annotation;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.util.concurrent.TimeUnit;
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Lock {
/**
* 锁key
*/
String key();
/**
* 超时时间
*/
long timeout() default 10;
/**
* 时间单位
*/
TimeUnit timeUnit() default TimeUnit.SECONDS;
}
lock切面
package com.aop.aspect;
import java.nio.charset.Charset;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
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.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.LocalVariableTableParameterNameDiscoverer;
import org.springframework.core.annotation.Order;
import org.springframework.data.redis.connection.RedisStringCommands;
import org.springframework.data.redis.connection.ReturnType;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.types.Expiration;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.Expression;
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import org.springframework.stereotype.Component;
import com.aop.annotation.Lock;
import io.netty.util.internal.StringUtil;
@Aspect
@Component
@Order(1)
public class LockAspect {
private static final Logger logger = LoggerFactory.getLogger(ActionLogAspect.class);
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Pointcut("@annotation(com.aop.annotation.Lock)")
public void lockAspectCut() {
}
@Around(value = "lockAspectCut()&&@annotation(lockAnnoAction)", argNames = "lockAnnoAction")
public Object doAround(ProceedingJoinPoint joinPoint, Lock lockAnnoAction) {
// 获取锁key
String key = getkey(joinPoint, lockAnnoAction);
// 获取锁value
String value = getLock(key, lockAnnoAction.timeout(), lockAnnoAction.timeUnit());
if (StringUtil.isNullOrEmpty(value)) {
// 获取失败
logger.error("不要频繁操作");
// 这里可以抛自定义异常
return "不要频繁操作";
}
// 获取成功
try {
return joinPoint.proceed();
} catch (Throwable e) {
logger.error("系统炸了");
return "系统炸了";
} finally {
// 释放锁
delLock(key, value);
}
}
/**
* 获取key 获取注解参数上得spel表达式对应得值
*
* @param joinPoint
* @param actionLogAnnoAction
* @return
*/
private String getkey(ProceedingJoinPoint joinPoint, Lock lockAnnoAction) {
// 用于获取方法的参数名得对象
LocalVariableTableParameterNameDiscoverer discoverer = new LocalVariableTableParameterNameDiscoverer();
// 用于解析spel表达式得两个对象
ExpressionParser parser = new SpelExpressionParser();
EvaluationContext context = new StandardEvaluationContext();
// 获取切入点参数值数组
Object[] parameters = joinPoint.getArgs();
// 获取切入点参数名数组
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
String[] parameterNames = discoverer.getParameterNames(signature.getMethod());
if (parameters == null || parameters.length <= 0) {
return lockAnnoAction.key();
}
for (int i = 0; i < parameterNames.length; i++) {
context.setVariable(parameterNames[i], parameters[i]);
}
// 解析注解参数key表达式
Expression expression = parser.parseExpression(lockAnnoAction.key());
String key = expression.getValue(context, String.class);
return key;
}
/**
* 获取锁
*
* @param key
* @param timeout
* @param timeUnit
* @return
*/
private String getLock(String key, long timeout, TimeUnit timeUnit) {
try {
String value = UUID.randomUUID().toString();
// RedisConnectio得set操作采用SET_IF_ABSENT策略,即若key存在则不进行操作返回失败,若key不存在则保存key和value,并设置超时时间
// 返回成功
Boolean getLock = stringRedisTemplate
// 匿名内部类lambda写法
.execute((RedisCallback<Boolean>) connection -> connection.set(
key.getBytes(Charset.forName("UTF-8")), value.getBytes(Charset.forName("UTF-8")),
Expiration.from(timeout, timeUnit), RedisStringCommands.SetOption.SET_IF_ABSENT));
// 获取成功
if (getLock) {
return value;
}
logger.error("获取锁失败"+"-"+"其他线程已经获取锁");
return null;
} catch (Exception e) {
logger.error("获取锁失败" + key, e);
return null;
}
}
/**
* 释放锁
*
* @param key
* @param value
*/
private void delLock(String key, String value) {
try {
// 通过Redis的eval指令去执行lua脚本实现释放锁的操作
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
// 释放锁
boolean delLock = stringRedisTemplate.execute(
(RedisCallback<Boolean>) connection -> connection.eval(script.getBytes(), ReturnType.BOOLEAN, 1,
key.getBytes(Charset.forName("UTF-8")), value.getBytes(Charset.forName("UTF-8"))));
if (!delLock) {
logger.error("释放锁失败,key超时,锁已被别的线程获取");
}
} catch (Exception e) {
logger.error("释放锁失败" + key, e);
}
}
}
测试
设置不同的端口 启动两次项目,用postman测试,测试接口如下
package com.aop.test;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.aop.annotation.Lock;
import com.aop.dto.Person;
@RestController
@RequestMapping("/test")
public class TestController {
@PostMapping("/lock")
@Lock(key = "#person.idCardNum",timeout = 5)
public Object lockTest(@RequestBody Person person) {
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "操作成功";
}
}
先向8080端口发送请求,然后马上向8081端口发送请求,结果如下
2019-09-24 00:03:47.076 ERROR 16176 --- [nio-8081-exec-3] com.aop.aspect.ActionLogAspect : 获取锁失败-其他线程已经获取锁
2019-09-24 00:03:47.076 ERROR 16176 --- [nio-8081-exec-3] com.aop.aspect.ActionLogAspect : 不要频繁操作
2019-09-24 00:03:47.310 ERROR 16176 --- [nio-8081-exec-4] com.aop.aspect.ActionLogAspect : 获取锁失败-其他线程已经获取锁
2019-09-24 00:03:47.310 ERROR 16176 --- [nio-8081-exec-4] com.aop.aspect.ActionLogAspect : 不要频繁操作
2019-09-24 00:03:47.569 ERROR 16176 --- [nio-8081-exec-5] com.aop.aspect.ActionLogAspect : 获取锁失败-其他线程已经获取锁
2019-09-24 00:03:47.569 ERROR 16176 --- [nio-8081-exec-5] com.aop.aspect.ActionLogAspect : 不要频繁操作
结果说明分布式锁已经生效。
未解决的问题
超时时间问题,如果一个线程在锁已经超时的情况下业务还没执行完,那么别的线程就会获取锁然后执行业务,严重的情况下会导致多个线程持有一个锁。
参考资料
AOP:Chapter 6. 使用Spring进行面向切面编程(AOP)
反射:Spring LocalVariableTableParameterNameDiscoverer获取方法的参数名
Redis:RedisTemplate使用PipeLine的总结
Spring SpEL:Spring表达式语言 之 5.3 SpEL语法 ——跟我学spring3
Spring SpEL 中的EvaluationContext 及Root