三、有关Spring AOP的理解

一、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/>

说明:

  1. 上方是配置完成之后直接生效,直接对某些包下的类或者方法进行拦截处理;
  2. 下方在使用时还需要添加注解,自定义的切面注解,要在某个类或者方法上加上此注解来对其进行拦截;

二、示例一:使用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。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Spring AOP(面向切面编程)是Spring框架中的一个重要模块,用于实现横切关注点的模块化。它通过在程序运行期间动态地将代码织入到应用程序的特定位置,从而实现对横切关注点的处理。 在Spring AOP中,通过定义切面(Aspect)和连接点(Join Point)来实现对横切关注点的处理。切面定义了在何处以及如何进行横切关注点的处理,而连接点则表示在应用程序中可以插入切面的位置。 Spring AOP的核心概念是切面、连接点、通知(Advice)、切点(Pointcut)和引入(Introduction): 1. 切面(Aspect):切面是一个模块化的单元,它包含了一组通知和切点。通知定义了在何时、何地以及如何进行横切关注点的处理,而切点定义了在应用程序中哪些连接点可以插入切面。 2. 连接点(Join Point):连接点是在应用程序中可以插入切面的位置。例如,方法调用、方法执行、异常抛出等都可以作为连接点。 3. 通知(Advice):通知定义了在连接点上执行的操作。常见的通知类型有前置通知(Before)、后置通知(After)、返回通知(After Returning)和异常通知(After Throwing)等。 4. 切点(Pointcut):切点是一个表达式,用于定义哪些连接点可以插入切面。通过切点表达式,可以选择性地将切面应用于特定的连接点。 5. 引入(Introduction):引入允许在现有的类中添加新的方法和属性。 Spring AOP的优点在于它能够将横切关注点与业务逻辑分离,提高了代码的可维护性和可重用性。同时,它也提供了一种非侵入式的方式来实现横切关注点的处理,不需要修改原有的代码。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值