一、Spring AOP:
详情参考一:狂神说Spring07:AOP就这么简单
详情参考一:Spring(2)之 (2.2 使用 AspectJ实现 AOP)
1. 什么是AOP?
AOP:面向切面编程,即通过预编译方式和运行期动态代理实现程序功能的统一维护的一种技术。Spring的Aop就是将公共的业务 (日志 , 安全等) 和领域业务结合起来 , 当执行领域业务时 , 将会把公共业务加进来 . 实现公共业务的重复利用 , 其本质还是动态代理(即将公共代码与业务代码分离,将其独立出来(即切面类),在执行业务代码时动态织入进来;使开发人员只关注业务代码的编写,提升开发效率。)
涉及名词:
切面(Aspect):切面类;
切入点(PointCut):切面通知 执行的 “地点”的定义,要在哪个类或者哪个方法进行织入;
通知(Advice):它是切面类中的一个方法;
…
2. 通知类型?
通知: Spring中支持5种类型的通知:
@Before:前置通知,业务代码之前执行;
@After: 后置通知,业务代码之后执行,无论是否出现异常都执行;
@Around: 环绕通知,环绕业务代码,有参数joinPoint:joinPoint.proceed();方法proceed()相当于目标对象业务代码中的save();
@AfterThrowing: 异常抛出通知,业务代码后,出现异常执行;
@AfterReturning:业务代码后执行,若出现异常不执行(即@AfterReturning与@AfterThrowing 两者不会同时存在)。
@PointCut(" execution( * com. asd. service. UserServiceImpl. * (… ))" ) :表示对哪些类的哪些方法进行拦截切入;(execution后是包名、类名或方法名字)
@Pointcut("@annotation(com.asd.utils.log.Log4Controller)") :表示对所有 @Log4Controller 注解注释的类或方法进行拦截切入;(annotation后是注解)
注:@PointCut()可换成任一类型的其他通知(如 @Before(…)、@After(…)…);
3. Spring中AOP的实现?
一、使用XML 方式,通过自定义类来实现AOP:
目标业务类是userServiceImpl:
第一步:自定义一个切入类:
public class MyAop{
public void beginTrans(){
System.out.println("方法执行前---开始事务");
}
public void commitTrans(){
System.out.println("方法执行后---提交事务");
}
public void afterReturning(){
System.out.println("afterReturning");
}
public void afterThrow(){
System.out.println("afterThrow");
}
public void around(ProceedingJoinPoint joinPoint) throws Throwable{
System.out.println("begin");
joinPoint.proceed();
System.out.println("end");
}
}
第二步:在Spring配置文件中,注册bean,并通过使用AOP的标签来进行AOP的配置:
<!--第一种方式使用XML 方式,通过自定义类来实现-->
<!--注册bean-->
<bean id="diy" class="com.asd.spring.MyAop"/>
<!--aop的配置-->
<aop:config>
<!--第一种方式:使用AOP的标签实现-->
<aop:aspect ref="diy">
<aop:pointcut id="diyPonitcut" expression="execution(* com.asd.service.UserServiceImpl.*(..))"/>
<aop:before pointcut-ref="diyPonitcut" method="beginTrans"/>
<aop:after pointcut-ref="diyPonitcut" method="commitTrans"/>
</aop:aspect>
</aop:config>
二、使用注解实现:
第一步:编写一个注解实现的增强类:
package com.asd.config;
@Aspect
public class AnnotationPointcut {
//法一:单个注解自己定义
@Before("execution(* com.asd.service.UserServiceImpl.*(..))")
public void before(){
System.out.println("方法执行前----开始事务");
}
@After("execution(* com.asd.service.UserServiceImpl.*(..))")
public void after(){
System.out.println("方法执行后----提交事务");
}
@Around("execution(* com.asd.service.UserServiceImpl.*(..))")
public void around(ProceedingJoinPoint joinPoint) throws Throwable {
System.out.println("begin环绕前");
joinPoint.proceed();//执行目标方法proceed
System.out.println("end环绕后");
}
//法二:使用 @PointCut 注解进行统一定义
@PointCut("execution(* com.asd.service.UserServiceImpl.*(..))")
public void pointCut(){
}
@Before("pointCut()")
public void beginTrans(){
System.out.println("方法执行前----开始事务");
}
@After("pointCut()")
public void commitTrans(){
System.out.println("方法执行后----提交事务");
}
@Around("pointCut()")
public void around(ProceedingJoinPoint joinPoint) throws Throwable {
System.out.println("begin环绕前");
joinPoint.proceed();//执行目标方法proceed
System.out.println("end环绕后");
}
}
第二步:在Spring配置文件中,注册bean,并增加支持注解的配置:
<!--第二种方式:注解方式实现-->
<!--注册bean-->
<bean id="annotationPointcut" class="com.asd.config.AnnotationPointcut"/>
<!-- 开启代理模式 -->
<aop:aspectj-autoproxy/>
说明:
- 上方是配置完成之后直接生效,直接对某些包下的类或者方法进行拦截处理;
- 下方在使用时还需要添加注解,自定义的切面注解,要在某个类或者方法上加上此注解来对其进行拦截;
二、示例一:使用AOP实现自定义日志
第一步:自定义切面类:
import lombok.extern.slf4j.Slf4j;
@Component
@Aspect
@Slf4j //下方log.info()使用,需添加lombok依赖
public class LogAspect {
@Autowired
private User user;
@Pointcut("@annotation(com.asd.utils.log.Log4Controller)")
public void logPointcut(){
}
@Pointcut("@annotation(com.asd.utils.log.Log4Service)")
public void logPointcut2(){
}
@Pointcut("@annotation(com.asd.utils.log.Log4System)")
public void logPointcut3(){
}
/**
* 记录Controller层方法的日志
* @param joinPoint
* @return 给前端的提示信息
* @throws LogException 自定义异常,用于避免重复记录日志
*/
@Around("logPointcut()")
public Object around(ProceedingJoinPoint joinPoint){
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
String className = joinPoint.getTarget().getClass().getName();
String methodName = method.getName();
Object[] args = joinPoint.getArgs();
//将参数名称和参数值拼接
String[] parameterNames = signature.getParameterNames();
String[] a = new String[parameterNames.length];
for (int i = 0; i <parameterNames.length-1; i++) {
a[i]=parameterNames[i]+"="+args[i].toString();
}
long t1 = System.currentTimeMillis();
log.info("操作用户:{}|进入方法:{}|请求参数:{}",user.getUsername(),className+"."+methodName,a);
try {
Object proceed = joinPoint.proceed();
log.info("操作用户:{}|{}方法结束|共耗时:{}",user.getUsername(),methodName,System.currentTimeMillis()-t1);
return proceed;
} catch (LogException e) {
return ResponseEntity.ok("服务器内部出错,请稍后重试!");
} catch (Throwable e) {
log.error("操作用户:{}|请求方法:{}|请求参数:{}|异常信息:{}",user.getUsername(),className+"."+methodName,a,e.getMessage(),e);
return ResponseEntity.ok("服务器内部出错,请稍后重试!");
}
}
/**
* 记录Service层方法的日志
* @param joinPoint
* @return 业务方法的返回值
* @throws LogException 自定义异常,用于避免重复记录日志
*/
@Around("logPointcut2()")
public Object around2(ProceedingJoinPoint joinPoint) throws LogException {
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
String className = joinPoint.getTarget().getClass().getName();
String methodName = method.getName();
Object[] args = joinPoint.getArgs();
//将参数名称和参数值拼接
String[] parameterNames = signature.getParameterNames();
String[] a = new String[parameterNames.length];
for (int i = 0; i < parameterNames.length - 1; i++) {
a[i] = parameterNames[i] + "=" + args[i].toString();
}
long t1 = System.currentTimeMillis();
log.info("操作用户:{}|进入方法:{}|请求参数:{}", user.getUsername(), className + "." + methodName, a);
try {
Object proceed = joinPoint.proceed();
log.info("操作用户:{}|{}方法结束|共耗时:{}", user.getUsername(), methodName, System.currentTimeMillis() - t1);
return proceed;
} catch (Throwable e) {
log.error("操作用户:{}|请求方法:{}|请求参数:{}|异常信息:{}", user.getUsername(), className + "." + methodName, a, e.getMessage(), e);
throw new LogException("已经记录过日志的异常!");
}
}
}
第二步:自定义切面注解:
如:@Log4Controller、@Log4Service、@Log4System
@Pointcut("@annotation(com.asd.utils.log.Log4Controller)")
@Pointcut("@annotation(com.asd.utils.log.Log4Service)")
@Pointcut("@annotation(com.asd.utils.log.Log4System)"
/**
* 自定义注解@Log4Controller,用于记录Controller层日志
* (@Log4Service、@Log4System类似)
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Log4Controller {
String value() default "";
}
第三步:业务类中使用切面注解:
//Controller层的类
public class UserController {
@Log4Controller
@GetMapping("/listUser")
public ResponseEntity<?> getUser(String name){
List<User> list=userService.getUser(name);
return ResponseEntity.ok("查询成功!");
}
}
//Service层的类
public class UserService {
@Log4Service
@Override
public List<User> getUser(String name){
List<User> list=userDao.getUser(name);
return list;
}
}
二、示例二:使用AOP实现Redis分布式锁
第一步:自定义切面类:
@Component
@Aspect
@Slf4j
public class RedisLockAspect {
@Autowired
private RedissonClient redissonClient;
@Autowired
private RedisProperties redisProperties;
@Around("@annotation(com.asd.core.annotation.RedisLock)")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable{
Method proxyMethod = ((MethodSignature)joinPoint.getSignature()).getMethod();
Method realMethod = joinPoint.getTarget().getClass().getDeclaredMethod(proxyMethod.getName(), proxyMethod.getParameterTypes());
RedisLock redisLock = realMethod.getAnnotation(RedisLock.class);
if (joinPoint.getArgs().length>0&&joinPoint.getArgs()[0] instanceof UserDTO){
String certNo=((UserDTO)joinPoint.getArgs()[0]).getCertNo();
//Redis分布式锁,同一用户同一时间只能单一查询
RLock lock=null;
try{
lock = redissonClient.getLock(redisLock.prefix()+realMethod.getName()+":"+certNo);
log.debug("lock:{}",lock.getName());
if (!lock.tryLock(-1, redisProperties.getLockExpireInSeconds(), TimeUnit.SECONDS)){
throw new SametimeQueryException();
}
}catch (RedisException e){
log.error("Redis异常,加锁失败",e);
}
try {
return joinPoint.proceed();
}finally {
try{
if (lock!=null){
lock.unlock();
}
}catch (RedisException e){
log.error("Redis异常,解锁失败",e);
}
}
}
//一般不会走到这步,除非方法参数异常
throw new UnknownException();
}
}
第二步:自定义切面注解:
/**
* Redis分布式锁
* (该注解用于方法上可以实现分布式同步锁)
*/
@Inherited
@Documented
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RedisLock {
String prefix() default "shop:Lock:";
long waitSecond() default -1;
long expireTime() default 10;
TimeUnit unit() default TimeUnit.SECONDS;
}
第三步:业务类中使用切面注解:
查询时:同一用户同一时间只能单一查询;
public class QueryServiceImpl implements QueryService{
@RedisLock
@Override
public User queryUser(String name){
...
}
}
注意一: tryLock()方法介绍::
详细参考一:Lock的tryLock()方法
详细参考二:redisson锁 tryLock的使用及正确用法
tryLock()方法是有返回值的,它表示用来尝试获取锁,如果获取成功,则返回true,如果获取失败(即锁已被其他线程获取),则返回false,这个方法无论如何都会立即返回。在拿不到锁时不会一直在那等待;具有三种构造方法:①无参;②包含 失效时间、单位 两个参数的构造方法;③包含 等待时间,失效时间,单位三个参数的构造方法; 示例二的自定义切面类中用的是第三种。
public class TestReentrantLock {
public void testReentrantLock(RedissonClient redisson){
RLock lock = redisson.getLock("anyLock");
try{
//1.无参 (最常见的使用方法)
//lock.lock();
//2.两个参数:失效时间,单位 (支持过期解锁功能,10秒钟以后自动解锁, 无需调用unlock方法手动解锁)
//lock.lock(10, TimeUnit.SECONDS);
//3.三个参数:等待时间,失效时间,单位 (尝试加锁,最多等待3秒,上锁以后10秒自动解锁)
boolean res = lock.tryLock(3, 10, TimeUnit.SECONDS);
if(res){ //成功
// do your business
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
注意二:1. RedisProperties.java:
@Component
@Data
@ConfigurationProperties("asd.core.redis")
public class RedisProperties {
private Integer lockExpireInSeconds;
private Integer rateLimiterTimeout;
}
2. application.yml:
pboc:
core:
redis:
lock-expire-in-seconds: 30
rate-limiter-timeout: 10
3. 对于@ConfigurationProperties(“asd.core.redis”)的使用:
详细参考:@ConfigurationProperties注解的基本使用
在SpringBoot使用@ConfigurationProperties注解读取yml/properties配置文件参数:在配置文件中所配置的参数都正确的注入到Java类对象中,而类中存在,但配置文件中没有配置的属性值默认为null。