AOP的概念和实现原理—Spring系列介绍

来源:互联网 发布:linux cat 多个文件 编辑:程序博客网 时间:2024/06/04 19:28

  上一篇我们了解了DI,下面我们来看一下AOP。AOP,面向切面编程。那么什么是面向切面编程呢?下面我们先来看一段代码。  

public User findByUserName(String userName) {        Instant begin = Instant.now();        User user = null;        try(Connection conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/edu-demo?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Chongqing", "root", "123123")){            try(PreparedStatement ps = conn.prepareStatement("select `id`, `account`, `password` from t_user where `account`=?")){                ps.setString(1, userName);                try(ResultSet rs = ps.executeQuery()) {                    if (rs.next()) {                        user = new User();                        user.setId(rs.getInt(1));                        user.setUserName(rs.getString(2));                        user.setPassword(rs.getString(3));                    }                }            }        }catch(Exception e){            e.printStackTrace();        }        Instant end = Instant.now();        if(logger.isLoggable(Level.INFO)){            logger.info("UserRepository.findByUserName(): " + (end.getNano() - begin.getNano()) / 1000000);        }        return user;    }

  这是一段常见的JDBC查询代码,在方法中加入性能日志。在系统遇到性能问题时,经常需要在一些方法中添加性能统计代码,但是像上面那做样的后果就是性能统计的代码会遍布项目的第一个角落,当我们不在需要性能统计的时候又需要去项目中找出性能统计代码,然后逐一删除。
  我们希望将项目中遍布各处的功能相同的代码抽取出来,然后进行统一的处理。这样当我们不在需要该功能或者需要修改该功能的实现的时候,只需要修改一个地方就可以了。
  那如何实现呢?我们就需要使用到一个技术,代理。代理好比生活中的代办公司,我们需要做某件比较复杂事情的时候,不是自己去办,而是找一个代办公司,代办公司办好后直接通知我们就可以了。
  代理分为动态代理和静态代理,静态代理需要为每一个目标类创建一个代理类,这样会造成类的爆炸式增长。在项目中使用更多的是动态代理。使用动态代理,我们只需要为一个功能生成一个代理类就可以了。下面来看一下代理的实现原理。
  这里写图片描述
  我们可以看到,动态代理在调用者和目标对象中间引入了一个代理对象,而代理对象和目标对象之间需要有相同的接口。也叫做基于接口的代理。  

//接口public interface UserRepository {    /**     * 根据用户名查询用户     * @param userName 用户名     * @return 如果该用户存在,则返回用户对象,否则返回null     */    User findByUserName(String userName);}
//实现类public class UserRepositoryImpl implements UserRepository {    static {        try {            Class.forName("com.mysql.cj.jdbc.Driver");        } catch (ClassNotFoundException e) {            e.printStackTrace();        }    }    @Override    public User findByUserName(String userName) {        User user = null;        try(Connection conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/edu-demo?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Chongqing", "root", "123123")){            try(PreparedStatement ps = conn.prepareStatement("select `id`, `account`, `password` from t_user where `account`=?")){                ps.setString(1, userName);                try(ResultSet rs = ps.executeQuery()) {                    if (rs.next()) {                        user = new User();                        user.setId(rs.getInt(1));                        user.setUserName(rs.getString(2));                        user.setPassword(rs.getString(3));                    }                }            }        }catch(Exception e){            e.printStackTrace();        }        return user;    }}
//调用者public class UserServiceImpl implements UserService {    private UserRepository userRepository = new UserRepositoryImpl();    @Override    public User login(String userName) {        return userRepository.findByUserName(userName);    }}

  这是没有加入性能统计代码的例子,下面我们创建代理对象,来为我们加入统一的性能统计。  

public class PerformanceAnalysisProxy implements InvocationHandler{    private static final org.apache.logging.log4j.Logger logger = org.apache.logging.log4j.LogManager.getLogger("cn.cqbdqn.ems.proxy.PerformanceAnalysisProxy");    private Object target;//目标对象引用    public PerformanceAnalysisProxy(Object target){        this.target = target;    }    public static <T> T createProxy(Object target) {        if(Objects.isNull(target)) {            throw new IllegalArgumentException("target is null!");        }        Class<?> type = target.getClass();        return (T)Proxy.newProxyInstance(type.getClassLoader(), //类加载器                                            type.getInterfaces(), //目标对象实现的接口                                            new PerformanceAnalysisProxy(target));//调用处理器    }    @Override    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {        Instant begin = Instant.now();        //调用目标对象的方法        Object obj = method.invoke(target, args);        Instant end = Instant.now();        if(logger.isDebugEnabled()){            logger.debug("{}.{}:{}", target.getClass().getName(), method.getName(), (end.getNano() - begin.getNano()) / 1000000);        }        return obj;    }}

  在代理类中,需要使用目标对象作为参数,也就是要代理哪一个对象(不明白Java动态代理实现的朋友可以参考相关资料,这里不作解释)。该代理类可以为任何的对象生成代理对象(目标对象需要有接口),由于代理对象和目标对象之间有相同的接口,我们可以认为目标对象和代理对象的功能是相同的。  

public class UserServiceImpl implements UserService {    //创建代理对象    private UserRepository userRepository = PerformanceAnalysisProxy.createProxy(new UserRepositoryImpl());    @Override    public User login(String userName) {        return userRepository.findByUserName(userName);    }}

  当我们需要进行性能统计的时候,就使用代理对象,不需要的时候,就直接使用目标对象。而且性能统计的代码都被我们移动到了代理对象中,当需要修改该功能的时候,只需要修改代理对象就可以了。
  面向切面编程,将遍布应用各处的功能分享出来形成可重用的组件。
  在Spring中,由于对象已经交给Spring管理,当我们需要动态为对象添加功能的时候,Spring会自动为我们生成代理对象,Spring支持动态代理和CGLIB生成代理对象。
  AOP中涉及到以下术语:   

  • 通知(Advice)
  • 连接点(Join Point)
  • 切点(Pointcut)
  • 切面(Aspect)
  • 引入(Introduction)
  • 织入(Weaving)
      
      通知定义了切面是什么以及何时使用。Spring切面可以应用5种类型的通知。前置通知,后置通知,返回通知,异常通知,异常通知,环绕通知。
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {        try {            //前置通知功能插入点            Object obj = method.invoke(target, args);            //返回通知功能插入点            //后置通知功能插入点            return obj;        }catch(Exception e){            //异常通知功能插入点            throw e;        }    }

上面的代码标示出了各个功能点的执行时机,而循环绕通知的执行时机在前置通知之后,返回通知之前执行。
  连接点就是应用通知的时机。连接点是在应用执行过程中能够插入切面的一个点,比如方法调用时,抛出异常时,甚至是修改字段的时候。
  切点定义了通知在何处执行。切点的定义会匹配通知所要织入的一个或多个连接点。
 切面是通知和切点的结合。通知和切点定义了切面的全部内容,它是什么,在何时何处完成功能。
 最后,我们来看一个Spring配置切面的例子:

<!-- 定义通知bean -->    <bean id="performanceAnalysisAspect" class="cn.cqbdqn.ems.proxy.PerformanceAnalysisAspect"></bean>    <aop:config>        <!-- 定义切点 该切点表示cn.cqbdqn.ems.repositories包及其子包下所有的类的所有方法,方法参数不限 -->        <aop:pointcut id="performancePointcut" expression="execution(* cn.cqbdqn.ems.repositories..*(..))"/>        <!-- 定义切面 -->        <aop:aspect id="performanceAspect" ref="performanceAnalysisAspect">            <!-- 定义通知 method指定当拦截到指定切点时调用通知对象的哪个方法 pointcut-ref指定引用的切点 -->            <aop:before method="before" pointcut-ref="performancePointcut"/>            <aop:after method="after" pointcut-ref="performancePointcut"/>            <aop:after-returning method="afterReturning" pointcut-ref="performancePointcut"/>            <aop:after-throwing method="afterThrowing" pointcut-ref="performancePointcut"/>            <aop:around method="around" pointcut-ref="performancePointcut"/>        </aop:aspect>    </aop:config>