美文网首页程序员
Spring AOP 详解和实例

Spring AOP 详解和实例

作者: 七弦桐语 | 来源:发表于2018-09-04 16:07 被阅读84次

    概念

    面向切面编程(也叫面向方面编程):Aspect Oriented Programming(AOP),是软件开发中的一个热点,也是Spring框架中的一个重要内容。利用AOP可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率。

    主要的功能是:日志记录,性能统计,安全控制,事务处理,异常处理等等。

    主要的意图是:将日志记录,性能统计,安全控制,事务处理,异常处理等代码从业务逻辑代码中划分出来,通过对这些行为的分离,我们希望可以将它们独立到非指导业务逻辑的方法中,进而改变这些行为的时候不影响业务逻辑的代码。

    image

    我们要做的,是定义一个切面,在切面的纵向定义处理方法,处理完成后,回到横向业务流(静态代理模式实现 proxy)。

    因为Java 是一门静态的强类型语言, 代码一旦写好, 编译成 java class 以后 ,可以在运行时通过反射(Reflection)来查看类的信息, 但是对类进行修改的话很困难。有如下方式来实现:

    1. 在编译的时候, 根据AOP的配置信息,悄悄的把日志,安全,事务等“切面”代码 和业务类编译到一起去。【预编译】
    2. 在运行期,业务类加载以后, 通过Java动态代理技术为业务类生产一个代理类, 把“切面”代码放到代理类中, Java 动态代理要求业务类需要实现接口才行。【运行期动态代理】
    3. 在运行期, 业务类加载以后, 动态的使用字节码构建一个业务类的子类,将“切面”逻辑加入到子类当中去, CGLIB就是这么做的。

    spring 采用(1)+(2)方式

    实现方式

    1. 实现 AOP 接口

    2. 通过.xml配置方式

    <bean id="personDao" class="com.itheima12.spring.aop.xml.transaction.PersonDaoImpl"></bean>
    <bean id="transaction" class="com.itheima12.spring.aop.xml.transaction.Transaction"></bean>
      <aop:config>
          <!-- 切入点表达式  确定目标类 -->
          <expression="execution(* com.itheima12.spring.aop.xml.transaction.PersonDaoImpl.*(..))"  id="perform"/>
          <!-- ref指向的对象就是切面 -->
          <aop:aspect ref="transaction">
              <!-- 
                  前置通知
                     1、在目标方法执行之前
                     2、获取不到目标方法的返回值
               -->
              <!-- 
                  <aop:before method="beginTransaction" pointcut-ref="perform"/>
               -->
              <!-- 
                  后置通知
                     1、后置通知可以获取到目标方法的返回值
                     2、当目标方法抛出异常,后置通知将不再执行
               -->
               <!-- 
                  <aop:after-returning method="commit" pointcut-ref="perform" returning="val"/>
               -->
              <!-- 
                  最终通知
                     无论目标方法是否抛出异常都将执行
               -->
                  <aop:after method="finallyMethod" pointcut-ref="perform"/>
              <!-- 
                  异常通知(多个异常的处理)这个异常处理是完全独立于系统之外的,脱离业务逻辑
               -->
                  <aop:after-throwing method="throwingMethod" throwing="ex" pointcut-ref="perform"/>
              <!-- 
                  环绕通知(可以进行权限管理,比如 shiro 底层)
                          1. 能控制目标方法的执行
                          2. 前置通知和后置通知能在目标方法的前面和后面加一些代码,但是不能控制目标方法的执行
               -->
                  <aop:around method="aroundMethod" pointcut-ref="perform"/>
          </aop:aspect>
      </aop:config>
    

    一些概念

    • 切面(aspect):用来切插业务方法的类。
    • 连接点(joinpoint):是切面类和业务类的连接点,其实就是封装了业务方法的一些基本属性,作为通知的参数来解析。
    • 通知(advice):在切面类中,声明对业务方法做额外处理的方法。
    • 切入点(pointcut):业务类中指定的方法,作为切面切入的点。其实就是指定某个方法作为切面切的地方。
    • 目标对象(target object):被代理对象。
    • AOP代理(aop proxy):代理对象。
    • 通知
      • 前置通知(before advice):在切入点之前执行。
      • 后置通知(after returning advice):在切入点执行完成后,执行通知。
      • 环绕通知(around advice):包围切入点,调用方法前后完成自定义行为。
      • 异常通知(after throwing advice):在切入点抛出异常后,执行通知。

    切入点表达式

    切入点表达式

    Spring AOP 的原理:

    1. 当spring启动的时候,加载两个bean,对两个bean进行实例化
    2. 当spring容器对配置文件解析到<aop:config>的时候,把切入点表达式解析出来,按照切入点表达式匹配spring容器内的bean。
    3. 如果匹配成功,则为该bean创建对象
    4. 当客户端利用context.getBean获取一个对象时,如果该对象有代理对象,则返回代理对象。如果没有,则返回本身

    应用实例

    1. AOP 权限管理(环绕通知)

    自定义注解

    @Target(ElementType.METHOD) //范围:在方法上
    @Retention(RetentionPolicy.RUNTIME)
    public @interface PrivlegeInfo {
        String name() default "";
    }
    

    编写注解解析器

    public class AnnotationParse {
        /*
         * targetClass  目标类的class形式
         * methodName  在客户端调用哪个方法,methodName就代表哪个方法
         */
        public static String parse(Class targetClass,String methodName) throws Exception{
            String methodAccess = "";
            /**
             * 该方法没有参数
             */
            Method method = targetClass.getMethod(methodName);
            //判断方法上面是否存在PrivilegeInfo注解
            if(method.isAnnotationPresent(PrivlegeInfo.class)){
                //得到方法上面的注解
                PrivlegeInfo privlegeInfo = method.getAnnotation(PrivlegeInfo.class);
                methodAccess = privlegeInfo.name();
            }
            return methodAccess;
        }
    }
    

    service方法

    public class PersonServiceImpl implements PersonService{
        @PrivlegeInfo(name="savePerson")
        public void savePerson() {
            System.out.println("save person");
        }
    
        @PrivlegeInfo(name="updatePerson")
        public void updatePerson() {
            System.out.println("update person");
        }
    }
    

    用户权限的切面类

    public class PrivilegeAspect {
        /**
         * 用户拥有的权限
         */
        private List<Privilege> privileges = new ArrayList<Privilege>();
    
        public List<Privilege> getPrivileges() {
            return privileges;
        }
    
        public void setPrivileges(List<Privilege> privileges) {
            this.privileges = privileges;
        }
    
        public void isAccessMethod(ProceedingJoinPoint joinPoint) throws Throwable{
            /**
             * 1、获取访问目标方法应该具备的权限
             *     得到
             *        1、目标类的class形式
             *        2、方法的名称
             */
            Class targetClass = joinPoint.getTarget().getClass();
            String methodName = joinPoint.getSignature().getName();
            //得到访问该方法的权限
            String methodAccess = AnnotationParse.parse(targetClass, methodName);
            boolean flag = false;
            //遍历用户所有的权限,查看是否用访问该方法的权限
            for (Privilege privilege : privileges) {
                //该用户能够访问目标方法
                if(methodAccess.equals(privilege.getName())){
                    flag = true;
                }
            }        
            if(flag){//访问目标方法
                joinPoint.proceed();
            }else{
                System.out.println("对不起,您没有权限访问");
            }
        }
    }
    

    application 配置文件

    <aop:config>
          <aop:pointcut 
              expression="execution(* com.itheima12.spring.aop.xml.privilege.service.impl.*.*(..))" id="perform"/>
          <aop:aspect ref="privilegeAspect">
              <aop:around method="isAccessMethod" pointcut-ref="perform"/>
          </aop:aspect>
      </aop:config>
    
    

    测试

    public class PrivilegeTest {
        @Test
        public void testPrivilege(){
            ApplicationContext context = 
                new ClassPathXmlApplicationContext("applicationContext.xml");
            /**
             * 初始化用户的权限
             */
            PrivilegeAspect privilegeAspect = (PrivilegeAspect)context.getBean("privilegeAspect");
            Privilege privilege1 = new Privilege();
            privilege1.setName("savePerson");
    
            Privilege privilege2 = new Privilege();
            privilege2.setName("updatePerson");
    
            privilegeAspect.getPrivileges().add(privilege2);
            privilegeAspect.getPrivileges().add(privilege1);
    
            PersonService personService = (PersonService)context.getBean("personService");
            personService.savePerson();
            personService.updatePerson();
        }
    }
    

    2. AOP 缓存

    application

    <context:component-scan base-package="com.itheima12.spring.aop.xml.transaction">
          </context:component-scan>
          <aop:aspectj-autoproxy></aop:aspectj-autoproxy>
    

    切面类

    /**
     * @Aspect
     * @Pointcut("execution(* com.itheima12.spring.aop.xml.transaction.PersonDaoImpl.*(..))")
     * private void aa(){}
           就相当于
     * <aop:config>
              <aop:pointcut 
                  expression="execution(* com.itheima12.spring.aop.xml.transaction.PersonDaoImpl.*(..))" 
                  id="aa()"/>
       </aop:config>
     * @author zd
     *
     */
    @Component("transaction")  // 加入到spring容器中
    @Aspect    // 证明这个注解所在类是切面类
    public class Transaction {
        @Pointcut("execution(* com.itheima12.spring.aop.xml.transaction.PersonDaoImpl.*(..))")
        private void aa(){}  //方法签名
    
        @Before("aa()")  // 前置通知
        public void beginTransaction(){
            System.out.println("begin transaction");
        }
    
        @AfterReturning("aa()")  // 后置通知
        public void commit(){
            System.out.println("commit");
        }
    }
    

    3. AOP 日志管理

    此项目是在 spring boot 环境下实现。

    1、 添加maven依赖注解

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

    2、添加数据库表

    DROP TABLE IF EXISTS `journal`;
    CREATE TABLE `journal` (
      `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '日志id',
      `uid` int(11) NOT NULL COMMENT '用户id',
      `modularType` int(2) NOT NULL COMMENT '模块类型',
      `operationType` int(2) NOT NULL COMMENT '操作类型:0:增/1:删/2:改/3:关闭/4:移动',
      `operationTime` datetime NOT NULL COMMENT '操作时间',
      PRIMARY KEY (`id`)
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
    

    3、增加对应的实体类

    4、日志添加 Mapper

     /**
     * 日志管理
     * Created by 陈梓平 on 2017/8/12.
     */
    public interface JournalMapper {
        /**日志添加*/
        int addJournalInfo(JournalInfo journalInfo);
    }
    
    <?xml version="1.0" encoding="UTF-8"?>
    <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
            "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
    <mapper namespace="com.chen.mapper.JournalMapper">
        <!--添加日志信息-->
        <insert id="addJournalInfo">
          INSERT INTO journal  (uid,modularType,operationType,operationTime)
          VALUES (10086,#{modularType},#{operationType},NOW())
        </insert>
    </mapper>
    

    5、日志工具类

    @Component
    @Transactional
    public class JournalUtils {
    
        @Autowired
        private JournalMapper jouUtilsJournalMapper;
        @Autowired
        private JournalInfo journalInfo;
    
        /**
         * 添加日志
         * @param modeularType
         * @param operationType
         */
        public void addJournalInfo(int modeularType,int operationType,int uid) {
            journalInfo.setModularType(modeularType);
            journalInfo.setOperationType(operationType);
            journalInfo.setUid(uid);
            jouUtilsJournalMapper.addJournalInfo(journalInfo);
        }
    }
    

    6、静态类(包括模块和操作)

    /**
     * 静态信息
     * Created by Administrator on 2017/8/12.
     */
    public class StaticInfo {
        /**--------------------  模块类型  ----------------*/
        //模块1
        public static final int MODEULARTTYPE_FIRST= 1;
    
        /**--------------------  操作类别  ---------------*/
        //增加
        public static final int OPERATIONTYPE_ADD = 0;
        //删除
        public static final int OPERATIONTYPE_UPDATE = 1;
        //修改
        public static final int OPERATIONTYPE_DELETE = 2;
        //开启
        public static final int OPERATIONTYPE_OPEN = 3;
        //关闭
        public static final int OPERATIONTYPE_CLOSE = 4;
        //移动
        public static final int OPERATIONTYPE_MOVER = 5;
    
        /**---------------   AOP代理  --------------------*/
        public static final String AOP_OPERATION_TYPE_ADD =  "add";
        public static final String AOP_OPERATION_TYPE_EDIT =  "edit";
        public static final String AOP_OPERATION_TYPE_MOVE =  "move";
        public static final String AOP_OPERATION_TYPE_DELETE =  "delete";
        public static final String AOP_OPERATION_TYPE_OPENORCLOSE =  "openOrClose";
    
        public static final String AOP_MODULAR_TYPE_FIRST = "Journal";
    
        public static final String AOP_SPIT_CLASSNAME = "impl.";
        public static final String AOP_SPIT_MODULAR_TYPE= "ServiceImpl";
    }
    

    7、日志切面AOP

    @Component
    @Aspect
    public class JournalAspect {
        /**日志输出*/
        private static final Logger logger = LoggerFactory.getLogger(JournalAspect.class);
    
        /**日志工具类*/
        @Autowired
        private JournalUtils aspectJournalUtils;
    
        /**service层切面*/
        private final String POINT_CUT = "execution(* com.chen.service..*(..))";
    
        @Pointcut(POINT_CUT)
        private void pointcut(){}
    
        /**
         * 后置最终通知(目标方法只要执行完了就会执行后置通知方法)
         * 日志管理
         * @param joinPoint
         */
        @After(value = "pointcut()")
        @Transactional
        public void doAfterAdvice(JoinPoint joinPoint) throws CustomException {
            //用的最多 通知的签名
            Signature signature = joinPoint.getSignature();
            //1.获取模块类型
            //AOP代理类的名字(包括包名)
            String declaringTypeName = signature.getDeclaringTypeName();
            logger.info("AOP代理类的名字"+declaringTypeName);
            //获取代理类的类名
            String[] split = declaringTypeName.split(StaticInfo.AOP_SPIT_CLASSNAME);
            String className = split[1];
            //获取模块名
            String[] modularTypeNames = className.split(StaticInfo.AOP_SPIT_MODULAR_TYPE);
            String modularTypeName = modularTypeNames[0];
            int modulerType = -1;
            //模块类型筛选
            modulerType = this.getModularType(modularTypeName, modulerType);
    
            //2.获取操作类型
            //代理的是哪一个方法
            String  methodName = signature.getName();
            logger.info("AOP代理方法的名字"+signature.getName());
            int opreationType = -1;
            opreationType = getOpreationType(joinPoint, signature, opreationType,methodName);
    
            if (modulerType==-1&&opreationType==-1)
                if (!StringUtils.isBlank(methodName)||!StringUtils.isBlank(modularTypeName))
                    throw new CustomException(ResultEnum.JOURNAL_LOG_ERROR);
    
            //3.添加日志
            if (modulerType!=-1&&opreationType!=-1)
                //TODO 3.1 从请求获取用户id
                aspectJournalUtils.addJournalInfo(modulerType,opreationType, 10086);
    
    
        }
        /**
         * 模块类型筛选
         * @param modularTypeName
         * @param type
         * @return
         */
        private int getModularType(String modularTypeName, int type) {
            //模块类型筛选
            switch (modularTypeName){
                case StaticInfo.AOP_MODULAR_TYPE_FIRST:
                    type = StaticInfo.MODEULARTTYPE_FIRST;
                    break;
                    //多模块添加
            }
            return type;
        }
        /**
         * 获取操作类型
         * @param joinPoint
         * @param signature
         * @param opreationType
         * @return
         */
        private int getOpreationType(JoinPoint joinPoint, Signature signature, int opreationType,String  methodName ) {
            switch (methodName){
                case StaticInfo.AOP_OPERATION_TYPE_ADD:
                    opreationType = StaticInfo.OPERATIONTYPE_ADD;
                    break;
                case StaticInfo.AOP_OPERATION_TYPE_EDIT:
                    opreationType = StaticInfo.OPERATIONTYPE_UPDATE;
                    break;
                case StaticInfo.AOP_OPERATION_TYPE_MOVE:
                    opreationType = StaticInfo.OPERATIONTYPE_MOVER;
                    break;
                case StaticInfo.AOP_OPERATION_TYPE_DELETE:
                    opreationType = StaticInfo.OPERATIONTYPE_DELETE;
                    break;
                case StaticInfo.AOP_OPERATION_TYPE_OPENORCLOSE:
                    Object[] obj = joinPoint.getArgs();
                    int arg = (int) obj[1];
                    if (arg==1)
                        opreationType = StaticInfo.OPERATIONTYPE_OPEN;
                    else
                        opreationType = StaticInfo.OPERATIONTYPE_CLOSE;
                    break;
            }
            return opreationType;
        }
    }
    

    8、添加Controller测试

    @RestController
    @RequestMapping
    public class JournalController {
        @Autowired
        private JournalService journalService;
    
        @PostMapping(value = "journalAdd")
        public Result add(){
            return journalService.add();
        }
    }
    

    9、添加Service测试

    @Service
    public class JournalServiceImpl implements JournalService {
        @Override
        public Result add() {
            return ResultUtils.success(ResultEnum.OK);
        }
    }
    

    4. AOP 实现分布式锁

    改造前:
    所有应用分布式锁的地方都需要如下代码:

    RLock redissonLock = redissonUtil.getRedisson().getLock("saveCourseApplyResource"+courseApplyResource.getUserId());
        boolean res = false;
        try {
            //等待3秒,有效期5秒
            res = redissonLock.tryLock(3, 5, TimeUnit.SECONDS);
            if(res){
                //执行业务操作
            }
        }catch (RuntimeException e){
            throw e;
        } catch (InterruptedException e) {
            e.printStackTrace();
            throw new RuntimeException("网络错误,请重试");
        } finally {
            if(res){
                redissonLock.unlock();
            }
        }
    

    改造后:

    1. 首先需要一个注解:
    /**
     * Description: 分布式锁应用注解<br>
     *
     * @author: name:yuxin <br>email: yuruixin@ixincheng.com <br>
     * Create Time:  2018/3/4 0004-下午 8:48<br>
     */
    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;
    
    @Target({ ElementType.METHOD, ElementType.TYPE })
    @Retention(RetentionPolicy.RUNTIME)
    @Documented
    public @interface Lock {
        //分布式锁的key前缀
        String lockName() default "";
        //等待时长
        int waitTime() default 3;
        //有效期时长
        int effectiveTime() default 5;
    }
    
    1. 然后,需要一个切面服务类
    /**
     * Description: 分布式锁切面服务<br>
     *
     * @author: name:yuxin <br>email: yuruixin@ixincheng.com <br>
     * Create Time:  2018/3/4 0004-下午 8:46<br>
     */
    
    import com.xczhihui.bxg.online.common.utils.RedissonUtil;
    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.redisson.api.RLock;
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Component;
    
    import java.lang.reflect.Method;
    import java.util.HashMap;
    import java.util.Map;
    import java.util.concurrent.TimeUnit;
    
    @Component
    @Aspect
    public class LockService {
    
        public static String LOCK_NAME = "lockName";
        public static String WAIT_TIME = "waitTime";
        public static String EFFECTIVE_TIME = "effectiveTime";
    
        @Autowired
        private RedissonUtil redissonUtil;
        protected Logger logger = LoggerFactory.getLogger(this.getClass());
    
        @Pointcut("@annotation(com.xczhihui.common.Lock)")
        public void lockPointcut() {
    
        }
    
        @Around("lockPointcut()")
        public Object around(ProceedingJoinPoint point) throws Throwable {
    
            Map<String,Object> map = getLockParams(point);
            String lockName = (String) map.get(LOCK_NAME);
            int waitTime = (int) map.get(WAIT_TIME);
            int effectiveTime = (int) map.get(EFFECTIVE_TIME);
            Object[] methodParam = null;
            Object object=null;
            boolean resl = false;
            //获取方法参数
            methodParam = point.getArgs();
            String lockKey = (String) methodParam[0];
            // 获得锁对象实例
            RLock redissonLock = redissonUtil.getRedisson().getLock(lockName+lockKey);
            try {
                //等待3秒 有效期8秒
                resl = redissonLock.tryLock(waitTime, effectiveTime, TimeUnit.SECONDS);
                if(resl){
                   object = point.proceed(point.getArgs());
                }
            }catch (RuntimeException e){
                throw e;
            }catch (Exception e){
                e.printStackTrace();
                throw new RuntimeException("网络错误,请重试");
            }finally {
                if(resl){
                    logger.info("开锁,{}",lockName+lockKey);
                    redissonLock.unlock();
                }else{
                    logger.error("未获得锁,{}",lockName+lockKey);
                    throw new RuntimeException("网络错误,请重试");
                }
            }
            return object;
        }
    
        /**
         * Description:获取方法的中锁参数
         * creed: Talk is cheap,show me the code
         * @author name:yuxin <br>email: yuruixin@ixincheng.com
         * @Date: 2018/3/4 0004 下午 8:59
         **/
        public  Map<String,Object> getLockParams(ProceedingJoinPoint joinPoint) throws Exception {
            String targetName = joinPoint.getTarget().getClass().getName();
            String methodName = joinPoint.getSignature().getName();
            Object[] arguments = joinPoint.getArgs();
    
            Class targetClass = Class.forName(targetName);
            Method[] method = targetClass.getMethods();
            Map<String,Object> map = new HashMap<>();
            for (Method m : method) {
                if (m.getName().equals(methodName)) {
                    Class[] tmpCs = m.getParameterTypes();
                    if (tmpCs.length == arguments.length) {
                        Lock lock = m.getAnnotation(Lock.class);
                        if (lock != null) {
                            String lockName = lock.lockName();
                            int waitTime = lock.waitTime();
                            int effectiveTime = lock.effectiveTime();
                            map.put(LOCK_NAME,lockName);
                            map.put(WAIT_TIME,waitTime);
                            map.put(EFFECTIVE_TIME,effectiveTime);
                        }
                        break;
                    }
                }
            }
            return map;
        }
    }
    
    1. 在需要加锁的方法上添加注解:
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    @Lock(lockName = "addCollectionApply",waitTime = 2,effectiveTime = 3)
    public void saveCollectionApply4Lock(String lockKey,CourseApplyInfo courseApplyInfo){
        //业务逻辑处理
    }
    

    相关文章

      网友评论

        本文标题:Spring AOP 详解和实例

        本文链接:https://www.haomeiwen.com/subject/kpzgwftx.html