JAVA设计模式-结构型-代理模式

一、什么是代理模式

代理模式是通过一个代理对象来访问目标对象,以此实现对目标对象功能的一系列增强或扩展,例如打印日志,记录接口耗时,数据验证等等.

二、为什么要使用代理模式

代理模式可以在满足开闭原则的基础上对目标对象的功能实现进一步的扩展,即不改动原有代码的情况下对目标方法进行扩展

三、代理模式的分类

1.静态代理模式

1.源码示例

需求:在不改动原有代码(UserServiceImpl类)的基础上,针对createUser方法实现入参的打印,queryUserNameByUserId方法实现入参的打印和统计方法的执行耗时
代码实现:

//真实对象接口类
public interface IUserService {
    void createUser(String userName, Integer userAge);
    String queryUserNameByUserId(Long userId);
}
//真实对象具体实现类
public class UserServiceImpl implements IUserService {
    @Override
    public void createUser(String userName, Integer userAge) {
        //保存用户代码...
    }
    @Override
    public String queryUserNameByUserId(Long userId) {
        //查询用户代码...
        return "张三";
    }
}
//代理对象类
@Slf4j
public class ProxyUserServiceImpl implements IUserService {

    private final IUserService userService;

    public ProxyUserServiceImpl(IUserService userService) {
        this.userService = userService;
    }

    @Override
    public void createUser(String userName, Integer userAge) {
        log.info("创建用户,参数:userName:{},userAge:{}", userName, userAge);
        userService.createUser(userName, userAge);
    }

    @Override
    public String queryUserNameByUserId(Long userId) {
        log.info("根据用户ID获取用户名称,参数:userId:{}", userId);
        TimeInterval timer = DateUtil.timer();
        String userName = userService.queryUserNameByUserId(userId);
        log.info("根据用户ID获取用户名称,方法耗时:{}毫秒", timer.interval());
        return userName;
    }

    public static ProxyUserServiceImpl getInstance(IUserService userService) {
        return new ProxyUserServiceImpl(userService);
    }
}
//调用者
public class StaticProxyClient {
    public static void main(String[] args) {
        ProxyUserServiceImpl proxyUserService = ProxyUserServiceImpl.getInstance(new UserServiceImpl());

        //执行方法createUser,并且打印入参
        proxyUserService.createUser("张三", 25);

        //执行方法queryUserNameByUserId,并且打印入参以及方法耗时
        proxyUserService.queryUserNameByUserId(123456789L);
    }
}
//执行结果
[main] INFO com.example.demo.design.proxy.ProxyUserService - 创建用户,参数:userName:张三,userAge:25
[main] INFO com.example.demo.design.proxy.ProxyUserService - 根据用户ID获取用户名称,参数:userId:123456789
[main] INFO com.example.demo.design.proxy.ProxyUserService - 根据用户ID获取用户名称方法耗时:0毫秒

以上为静态代理模式的经典实现

2.静态代理模式总结

静态代理模式是代理模式的一种落地实现,相对比较简单易懂.
我个人的理解:静态代理模式其实就是使用组合的方式去实现的,在代理类中注入目标类,然后调用目标类的方法,同时对该方法进行扩展.
静态代理模式的优点:
1.静态代理模式相对比较直观简单容易理解和实现
2.静态代理模式确实实现了目标对象方法的增强和扩展
静态代理模式的缺点:
1.代码太过于写死,而且容易违背开闭原则,因为在静态代理模式中目标对象的代理对象必须提前写好,假设接口发生变更那么代理类的代码也需要修改.
2.静态代理模式中每一个目标对象都需要手动编写一个对应的代理对象,假设目标对象过多则会导致代码的过渡膨胀,造成项目的臃肿.
以上为静态代理模式的缺点,所以在开源框架中基本不会采用静态代理模式.

2.动态代理模式

动态代理模式就是为了解决静态代理模式中的弊端而产生的.

1.JDK动态代理

1.源码示例
public static Object newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h) throws IllegalArgumentException {
        //返回一个代理类,这个是整个方法的核心
        Class<?> cl = getProxyClass0(loader, intfs);
		//使用反射获取其有参构造器,constructorParams是定义在Proxy类中的字段,值为{InvocationHandler.class}
        final Constructor<?> cons = cl.getConstructor(constructorParams);
		//使用返回创建代理对象
        return cons.newInstance(new Object[]{h});
}
2.框架应用
1.JDK动态代理在Mybatis框架中的应用

类的继承关系
BaseJdbcLogger类继承图
BaseJdbcLogger:
Mybatis框架中用于代理进行日志记录的基类(所有用于记录日志的代理类的基类)

ConnectionLogger:
ConnectionLogger为Connection接口代理类的InvocationHandler具体实现,扩展了prepareStatement(),prepareCall(),createStatement()方法,并且打印了预处理的Sql语句

public final class ConnectionLogger extends BaseJdbcLogger implements InvocationHandler {

  private final Connection connection;

  private ConnectionLogger(Connection conn, Log statementLog, int queryStack) {
    super(statementLog, queryStack);
    this.connection = conn;
  }

  @Override
  public Object invoke(Object proxy, Method method, Object[] params)
      throws Throwable {
    try {
      //针对Object类中的方法不做任何处理,直接返回
      if (Object.class.equals(method.getDeclaringClass())) {
        return method.invoke(this, params);
      }
      //针对以下俩个方法进行扩展,prepareStatement(),prepareCall()
      if ("prepareStatement".equals(method.getName()) || "prepareCall".equals(method.getName())) {
      	//在deBug模式下,打印预处理的Sql语句 例: Preparing: select * from user where id=?
        if (isDebugEnabled()) {
          debug(" Preparing: " + removeExtraWhitespace((String) params[0]), true);
        }
        //方法执行完毕之后会返回PreparedStatement对象,但是这里不直接返回PreparedStatement真实对象,而是返回PreparedStatement代理对象
        PreparedStatement stmt = (PreparedStatement) method.invoke(connection, params);
        //调用PreparedStatementLogger.newInstance()方法获取到PreparedStatement的代理对象
        stmt = PreparedStatementLogger.newInstance(stmt, statementLog, queryStack);
        return stmt;
        //如果是通过代理对象调用createStatement()方法,则返回的Statement对象也是一个代理对象,并非真实的Statement对象
      } else if ("createStatement".equals(method.getName())) {
        Statement stmt = (Statement) method.invoke(connection, params);
        stmt = StatementLogger.newInstance(stmt, statementLog, queryStack);
        return stmt;
      } else {
        return method.invoke(connection, params);
      }
    } catch (Throwable t) {
      throw ExceptionUtil.unwrapThrowable(t);
    }
  }

  //通过JDK的Proxy.newProxyInstance()方法创建Connection对象的代理类,通过构造方法传入真实的Connection对象
  public static Connection newInstance(Connection conn, Log statementLog, int queryStack) {
    InvocationHandler handler = new ConnectionLogger(conn, statementLog, queryStack);
    ClassLoader cl = Connection.class.getClassLoader();
    //创建代理类的核心方法
    return (Connection) Proxy.newProxyInstance(cl, new Class[]{Connection.class}, handler);
  }

  public Connection getConnection() {
    return connection;
  }
}

PreparedStatementLogger:
PreparedStatementLogger为PreparedStatement接口代理类的InvocationHandler具体实现,打印了Sql语句中的参数

public final class PreparedStatementLogger extends BaseJdbcLogger implements InvocationHandler {

  private final PreparedStatement statement;

  private PreparedStatementLogger(PreparedStatement stmt, Log statementLog, int queryStack) {
    super(statementLog, queryStack);
    this.statement = stmt;
  }

  @Override
  public Object invoke(Object proxy, Method method, Object[] params) throws Throwable {
    try {
      if (Object.class.equals(method.getDeclaringClass())) {
        return method.invoke(this, params);
      }
      /*
      *针对如下方法进行了扩展
      execute(),
      executeUpdate(),
      executeQuery(),
      addBatch()
      */
      if (EXECUTE_METHODS.contains(method.getName())) {
      	//在deBug模式下,打印了执行的Sql语句中的所有参数以及参数类型
        if (isDebugEnabled()) {
          debug("Parameters: " + getParameterValueString(), true);
        }
        clearColumnInfo();
        //如果是通过代理对象调用executeQuery()方法,则返回的ResultSet对象也是代理对象,并非真实的ResultSet对象
        if ("executeQuery".equals(method.getName())) {
          ResultSet rs = (ResultSet) method.invoke(statement, params);
          return rs == null ? null : ResultSetLogger.newInstance(rs, statementLog, queryStack);
        } else {
          return method.invoke(statement, params);
        }
      } else if (SET_METHODS.contains(method.getName())) {
        if ("setNull".equals(method.getName())) {
          setColumn(params[0], null);
        } else {
          setColumn(params[0], params[1]);
        }
        return method.invoke(statement, params);
        //通过代理对象调用getResultSet()方法,返回的也是ResultSet代理对象
      } else if ("getResultSet".equals(method.getName())) {
        ResultSet rs = (ResultSet) method.invoke(statement, params);
        return rs == null ? null : ResultSetLogger.newInstance(rs, statementLog, queryStack);
      } else if ("getUpdateCount".equals(method.getName())) {
        int updateCount = (Integer) method.invoke(statement, params);
        if (updateCount != -1) {
          debug("   Updates: " + updateCount, false);
        }
        return updateCount;
      } else {
        return method.invoke(statement, params);
      }
    } catch (Throwable t) {
      throw ExceptionUtil.unwrapThrowable(t);
    }
  }

  //调用JDK的Proxy.newProxyInstance()方法,生成PreparedStatement的代理对象
  public static PreparedStatement newInstance(PreparedStatement stmt, Log statementLog, int queryStack) {
    InvocationHandler handler = new PreparedStatementLogger(stmt, statementLog, queryStack);
    ClassLoader cl = PreparedStatement.class.getClassLoader();
    return (PreparedStatement) Proxy.newProxyInstance(cl, new Class[]{PreparedStatement.class, CallableStatement.class}, handler);
  }

  public PreparedStatement getPreparedStatement() {
    return statement;
  }

ResultSetLogger:
ResultSetLogger为ResultSet接口代理类的InvocationHandler具体实现类,针对next()方法进行了扩展,打印了Sql的执行结果

public final class ResultSetLogger extends BaseJdbcLogger implements InvocationHandler {

  private static final Set<Integer> BLOB_TYPES = new HashSet<>();
  private boolean first = true;
  private int rows;
  private final ResultSet rs;
  private final Set<Integer> blobColumns = new HashSet<>();

  static {
    BLOB_TYPES.add(Types.BINARY);
    BLOB_TYPES.add(Types.BLOB);
    BLOB_TYPES.add(Types.CLOB);
    BLOB_TYPES.add(Types.LONGNVARCHAR);
    BLOB_TYPES.add(Types.LONGVARBINARY);
    BLOB_TYPES.add(Types.LONGVARCHAR);
    BLOB_TYPES.add(Types.NCLOB);
    BLOB_TYPES.add(Types.VARBINARY);
  }

  private ResultSetLogger(ResultSet rs, Log statementLog, int queryStack) {
    super(statementLog, queryStack);
    this.rs = rs;
  }

  @Override
  public Object invoke(Object proxy, Method method, Object[] params) throws Throwable {
    try {
      //如果是Object类的方法,不做任何处理,直接返回.
      if (Object.class.equals(method.getDeclaringClass())) {
        return method.invoke(this, params);
      }
      //先执行完方法,获取到结果
      Object o = method.invoke(rs, params);
      //针对next()进行扩展,
      if ("next".equals(method.getName())) {
      	//ResultSet的next()方法返回的是一个Boolean值 为false的时候只打印执行结果的Total数量 为true的时候才打印执行结果
        if ((Boolean) o) {
          rows++;
          //在deBug模式下打印Sql的执行结果
          if (isTraceEnabled()) {
            ResultSetMetaData rsmd = rs.getMetaData();
            final int columnCount = rsmd.getColumnCount();
            if (first) {
              first = false;
              //打印列名 Columns
              printColumnHeaders(rsmd, columnCount);
            }
            //打印结果 Row
            printColumnValues(columnCount);
          }
        } else {
          debug("     Total: " + rows, false);
        }
      }
      clearColumnInfo();
      return o;
    } catch (Throwable t) {
      throw ExceptionUtil.unwrapThrowable(t);
    }
  }

  //打印Sql执行结果的列名 例:Columns: id, asset_package_id, dict_name, owner_type, removable, parent, files, update_time, create_time
  private void printColumnHeaders(ResultSetMetaData rsmd, int columnCount) throws SQLException {
    StringJoiner row = new StringJoiner(", ", "   Columns: ", "");
    for (int i = 1; i <= columnCount; i++) {
      if (BLOB_TYPES.contains(rsmd.getColumnType(i))) {
        blobColumns.add(i);
      }
      row.add(rsmd.getColumnLabel(i));
    }
    trace(row.toString(), false);
  }

//打印每一条记录 例:Row: 1, ABS20220505000000, 测试文件夹(计划管理人), PLAN_MANAGER, Y, null, <<BLOB>>, 2022-05-06 15:33:14, 2022-05-06 15:28:24
  private void printColumnValues(int columnCount) {
    StringJoiner row = new StringJoiner(", ", "       Row: ", "");
    for (int i = 1; i <= columnCount; i++) {
      try {
        if (blobColumns.contains(i)) {
          row.add("<<BLOB>>");
        } else {
          row.add(rs.getString(i));
        }
      } catch (SQLException e) {
        // generally can't call getString() on a BLOB column
        row.add("<<Cannot Display>>");
      }
    }
    trace(row.toString(), false);
  }
  /**
  	这俩个方法结合起来打印效果如下:
    Columns: id, asset_package_id, dict_name, owner_type, removable, parent, files, update_time, create_time
        Row: 1, ABS20220505000000, 测试文件夹(计划管理人), PLAN_MANAGER, Y, null, <<BLOB>>, 2022-05-06 15:33:14, 2022-05-06 15:28:24
        Row: 2, ABS20220505000000, 电池.txt, PLAN_MANAGER, Y, 1, <<BLOB>>, 2022-05-24 10:21:35, 2022-05-06 15:28:59
        Row: 3, ABS20220505000000, 名字.txt, PLAN_MANAGER, Y, 1, <<BLOB>>, 2022-05-06 15:29:54, 2022-05-06 15:29:49
     Total: 3
  **/

  //调用JDK的 Proxy.newProxyInstance()方法生成ResultSet的代理对象
  public static ResultSet newInstance(ResultSet rs, Log statementLog, int queryStack) {
    InvocationHandler handler = new ResultSetLogger(rs, statementLog, queryStack);
    ClassLoader cl = ResultSet.class.getClassLoader();
    return (ResultSet) Proxy.newProxyInstance(cl, new Class[]{ResultSet.class}, handler);
  }

  public ResultSet getRs() {
    return rs;
  }
}

下面将结合代码调用链来梳理动态代理模式是如何在代码中使用的
注意:
1.以下代码只截取跟动态代理相关的核心代码
2.本次只针对Select相关操作的代码进行举例

首先是MybatisPlusInterceptor

/**
 com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor 
 com.baomidou.mybatisplus.core.executor.MybatisCachingExecutor
 
 
 1.在调用Mybatis-plus的相关Api时(例如:select(),insert()方法),会进入该拦截器中
 2.在该拦截器中获取Executor接口具体的实现类,这里获取到的实现类是MybatisCachingExecutor类
 3.最终会调用MybatisCachingExecutor类中的query()方法返回最终的结果
**/
public class MybatisPlusInterceptor implements Interceptor {

    @Setter
    private List<InnerInterceptor> interceptors = new ArrayList<>();

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        Object target = invocation.getTarget();
        Object[] args = invocation.getArgs();
        //判断目标对象是否为Executor接口的子类
        if (target instanceof Executor) {
            final Executor executor = (Executor) target;
            Object parameter = args[1];
            boolean isUpdate = args.length == 2;
            MappedStatement ms = (MappedStatement) args[0];
            //这里判断是Select操作还是Update操作
            if (!isUpdate && ms.getSqlCommandType() == SqlCommandType.SELECT) {
                RowBounds rowBounds = (RowBounds) args[2];
                ResultHandler resultHandler = (ResultHandler) args[3];
                BoundSql boundSql;
                if (args.length == 4) {
                    boundSql = ms.getBoundSql(parameter);
                } else {
                    boundSql = (BoundSql) args[5];
                }
                for (InnerInterceptor query : interceptors) {
                    if (!query.willDoQuery(executor, ms, parameter, rowBounds, resultHandler, boundSql)) {
                        return Collections.emptyList();
                    }
                    query.beforeQuery(executor, ms, parameter, rowBounds, resultHandler, boundSql);
                }
                CacheKey cacheKey = executor.createCacheKey(ms, parameter, rowBounds, boundSql);
				/**				
				在断点调试中发现,当操作为Select时,将使用Executor接口的实现类来执行query()方法
				这里具体的实现类为MybatisCachingExecutor
				**/
                return executor.query(ms, parameter, rowBounds, resultHandler, cacheKey, boundSql);
            } else if (isUpdate) {
                for (InnerInterceptor update : interceptors) {
                    if (!update.willDoUpdate(executor, ms, parameter)) {
                        return -1;
                    }
                    update.beforeUpdate(executor, ms, parameter);
                }
            }
        } else {
            final StatementHandler sh = (StatementHandler) target;
            Connection connections = (Connection) args[0];
            Integer transactionTimeout = (Integer) args[1];
            for (InnerInterceptor innerInterceptor : interceptors) {
                innerInterceptor.beforePrepare(sh, connections, transactionTimeout);
            }
        }
        return invocation.proceed();
    }
}

接下来进入到MybatisCachingExecutor

/**
com.baomidou.mybatisplus.core.executor.MybatisCachingExecutor
com.baomidou.mybatisplus.core.executor.MybatisSimpleExecutor

此类逻辑比较简单
1.获取缓存,判断缓存不为空的时候执行缓存相关的逻辑代码(不是本次关注的重点)
2.缓存为空的时候,使用调用成员变量delegate的query()方法
**/
public class MybatisCachingExecutor implements Executor {

	/**
	成员变量为Executor接口
	在断点调试时发现,该成员变量的具体实现子类是MybatisSimpleExecutor
	**/
    private final Executor delegate;
    private final TransactionalCacheManager tcm = new TransactionalCacheManager();

    @Override
    public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
        throws SQLException {
        //获取缓存
        Cache cache = ms.getCache();
        Optional<IPage> pageOptional = ParameterUtils.findPage(parameterObject);
        //如果缓存不为空,则进入到此段逻辑,但本次关注的是代理模式,所以对此段逻辑并不过多说明
        if (cache != null) {
            flushCacheIfRequired(ms);
            if (ms.isUseCache() && resultHandler == null) {
                ensureNoOutParams(ms, boundSql);
                Object result = tcm.getObject(cache, key);
                if (result == null) {
                    if (pageOptional.isPresent()) {
                        IPage page = pageOptional.get();
                        CacheKey countCacheKey = null;
                        if (page.isSearchCount()) {        
                            countCacheKey = getCountCacheKey(ms, boundSql, parameterObject, RowBounds.DEFAULT);
                            Number count = (Number) tcm.getObject(cache, countCacheKey);
                            if (count != null) {
                                page.hitCount(true);
                                page.setTotal(count.longValue());
                            }
                        }
                        result = delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
                        List<E> records = (List<E>) result;
                        page.setRecords(records);
                        tcm.putObject(cache, key, records);
                        if (countCacheKey != null && !page.isHitCount()) {
                            tcm.putObject(cache, countCacheKey, page.getTotal());
                        }
                        return new PageList(records, page.getTotal());
                    } else {
                        result = delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
                        tcm.putObject(cache, key, result);
                        return (List<E>) result;
                    }
                } else {
                    if (pageOptional.isPresent()) {
                        IPage page = pageOptional.get();
                        if (page.isSearchCount()) {
                            CacheKey cacheKey = getCountCacheKey(ms, boundSql, parameterObject, RowBounds.DEFAULT);
                            Number count = (Number) tcm.getObject(cache, cacheKey);
                            if (count != null) {
                                page.hitCount(true);
                                return new PageList((List) result, count.longValue());
                            } else {
                                result = delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
                                List<E> records = (List<E>) result;
                                tcm.putObject(cache, cacheKey, page.getTotal());
                                return records;
                            }
                        }
                        return new PageList((List) result, 0L);
                    } else {
                        return (List<E>) result;
                    }
                }
            }
        }
        /**
		在断点调试时发现真正使用的具体实现对象为MybatisSimpleExecutor
		所以这里调用的是MybatisSimpleExecutor类中的query()方法
		**/
        return delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
    }
}

然后进入到MybatisSimpleExecutor
具体逻辑见代码注释
2.第一截代码段为子类MybatisSimpleExecutor的相关核心代码
1.第二截代码段为父类BaseExecutor的相关核心代码

/**
com.baomidou.mybatisplus.core.executor.MybatisSimpleExecutor 
org.apache.ibatis.executor.BaseExecutor
org.apache.ibatis.executor.statement.RoutingStatementHandler

1.首先因为此类最终继承至BaseExecutor类,但是并未重写query()方法,所以会先调用父类的query()方法
2.在父类的query()方法中最终会调用其子类的doQuery()方法,也就是此类的doQuery()方法
**/
public class MybatisSimpleExecutor extends AbstractBaseExecutor {
	//最终会进入此方法
    @Override
    public <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
        Statement stmt = null;
        try {
        	//获取Mybatis的核心配置类
            Configuration configuration = ms.getConfiguration();
            //在断点调试时发现获取到的StatementHandler接口的具体实现类为RoutingStatementHandler
            StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
            //获取Statement对象
            stmt = prepareStatement(handler, ms.getStatementLog(), false);
            /**
			获取到Statement对象之后
			如果为空则直接返回一个空的结果集
			如果不为空则调用RoutingStatementHandler类的query()方法去获取结果集
			**/
            return stmt == null ? Collections.emptyList() : handler.query(stmt, resultHandler);
        } finally {
            closeStatement(stmt);
        }
    }

    private Statement prepareStatement(StatementHandler handler, Log statementLog, boolean isCursor) throws SQLException {
        Statement stmt;
     	//这里调用的是父类的getConnection()方法,已在父类的代码中进行了标注
        Connection connection = getConnection(statementLog);
        /**
		在断点调试时发现,这里的handler接口的具体实现对象为RoutingStatementHandler
		所以这里会去调用RoutingStatementHandler类中的prepare()方法获取到Statement对象
		**/
        stmt = handler.prepare(connection, transaction.getTimeout());
        if (stmt == null && !isCursor) {
            return null;
        } else {
            handler.parameterize(stmt);
            return stmt;
        }
    }
}

BaseExecutor

/**
org.apache.ibatis.executor.BaseExecutor
**/
public abstract class BaseExecutor implements Executor {

  @Override
  public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
    ErrorContext.instance().resource(ms.getResource()).activity("executing a query").object(ms.getId());
    if (closed) {
      throw new ExecutorException("Executor was closed.");
    }
    if (queryStack == 0 && ms.isFlushCacheRequired()) {
      clearLocalCache();
    }
    List<E> list;
    try {
      queryStack++;
      list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
      if (list != null) {
        handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
      } else {
      	//最终会调用此方法
        list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
      }
    } finally {
      queryStack--;
    }
    if (queryStack == 0) {
      for (DeferredLoad deferredLoad : deferredLoads) {
        deferredLoad.load();
      }
      deferredLoads.clear();
      if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
        clearLocalCache();
      }
    }
    return list;
  }

  private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
    List<E> list;
    localCache.putObject(key, EXECUTION_PLACEHOLDER);
    try {
      //这里调用的是子类的doQuery()方法
      list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
    } finally {
      localCache.removeObject(key);
    }
    localCache.putObject(key, list);
    if (ms.getStatementType() == StatementType.CALLABLE) {
      localOutputParameterCache.putObject(key, parameter);
    }
    return list;
  }
}

  /**
	由以下代码可知,当日志级别为DEBUG时,会去调用ConnectionLogger.newInstance()方法
	这个方法在上述ConnectionLogger类中可知返回的是Connection接口的代理对象,所以此时获取到的Connection对象就已经是代理对象了
  **/
  protected Connection getConnection(Log statementLog) throws SQLException {
    //获取Connection接口的具体实现类
    Connection connection = transaction.getConnection();
    if (statementLog.isDebugEnabled()) {
   	  //获取代理对象并且返回
      return ConnectionLogger.newInstance(connection, statementLog, queryStack);
    } else {
      return connection;
    }
  }

RoutingStatementHandler

/**
org.apache.ibatis.executor.statement.RoutingStatementHandler
org.apache.ibatis.executor.statement.PreparedStatementHandler

此类为StatementHandler接口的实现类
此类的逻辑也比较简单,直接调用成员变量delegate的prepare()方法返回Statement对象
**/
public class RoutingStatementHandler implements StatementHandler {
 /**
 在断点调试时发现,成员变量StatementHandler接口的实现类为PreparedStatementHandler
 **/
 private final StatementHandler delegate;
 
  @Override
  public Statement prepare(Connection connection, Integer transactionTimeout) throws SQLException {
  	//所以这里调用的是PreparedStatementHandler类的prepare()方法
    return delegate.prepare(connection, transactionTimeout);
  }

  @Override
  public <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException {
  	//所以这里调用的是PreparedStatementHandler类中的query()方法
    return delegate.query(statement, resultHandler);
  }
}

PreparedStatementHandler

/**
org.apache.ibatis.executor.statement.PreparedStatementHandler
org.apache.ibatis.executor.statemen.BaseStatementHandler 
org.apache.ibatis.executor.resultset.DefaultResultSetHandler

1.首先此类继承BaseStatementHandler类,但是未重写prepare()方法,所以会先去调用父类的prepare()方法
2.在父类的prepare()方法中调用其子类的instantiateStatement()方法,也就是此类的instantiateStatement()方法

**/
public class PreparedStatementHandler extends BaseStatementHandler {
  /**
  此方法最终会返回Statement对象
  如果之前获取到的Connection接口的具体实现对象是代理对象,所以这里调用connection.prepareStatement()方法会进入到
  ConnectionLogger中的invoke()方法
  由之前梳理的ConnectionLogger类中可知,执行prepareStatement()方法时,返回的是Statement接口的代理对象
  **/
  @Override
  protected Statement instantiateStatement(Connection connection) throws SQLException {
    String sql = boundSql.getSql();
    if (mappedStatement.getKeyGenerator() instanceof Jdbc3KeyGenerator) {
      String[] keyColumnNames = mappedStatement.getKeyColumns();
      if (keyColumnNames == null) {
        return connection.prepareStatement(sql, PreparedStatement.RETURN_GENERATED_KEYS);
      } else {
        return connection.prepareStatement(sql, keyColumnNames);
      }
    } else if (mappedStatement.getResultSetType() == ResultSetType.DEFAULT) {
      return connection.prepareStatement(sql);
    } else {
      return connection.prepareStatement(sql, mappedStatement.getResultSetType().getValue(), ResultSet.CONCUR_READ_ONLY);
    }
  }
  
  @Override
  public <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException {
    PreparedStatement ps = (PreparedStatement) statement;
    //去执行Sql
    ps.execute();
    /**
	在断点调试时,发现resultSetHandler的具体实现类为DefaultResultSetHandler
	所以这里会去调用DefaultResultSetHandler类的handleResultSets()方法
	**/
    return resultSetHandler.handleResultSets(ps);
  }
}

BaseStatementHandler

/**
org.apache.ibatis.executor.statemen.BaseStatementHandler
**/
public abstract class BaseStatementHandler implements StatementHandler {
  @Override
  public Statement prepare(Connection connection, Integer transactionTimeout) throws SQLException {
    ErrorContext.instance().sql(boundSql.getSql());
    Statement statement = null;
    try {
      //这里的instantiateStatement()方法调用的是子类中的方法
      statement = instantiateStatement(connection);
      setStatementTimeout(statement, transactionTimeout);
      setFetchSize(statement);
      return statement;
    } catch (SQLException e) {
      closeStatement(statement);
      throw e;
    } catch (Exception e) {
      closeStatement(statement);
      throw new ExecutorException("Error preparing statement.  Cause: " + e, e);
    }
  }
}

至此我们已经获取到了Statement对象(在DEBUG级别下获取到的是代理对象),下面我们将去获取Sql的执行结果集,各位小伙伴还记得走到了哪段代码嘛?
对,就是MybatisSimpleExecutor类

public class MybatisSimpleExecutor extends AbstractBaseExecutor {
    @Override
    public <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
        Statement stmt = null;
        try {
            Configuration configuration = ms.getConfiguration();
            StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
            /**
			这里经过上述一大段逻辑最终返回了Statement对象
			当对象不为空时我们将调用RoutingStatementHandler类中的query()方法去获取Sql的执行结果集
			RoutingStatementHandler类的query()方法已经在上面的代码中做了相关注释,各位小伙伴可以划上去看看
			**/
            stmt = prepareStatement(handler, ms.getStatementLog(), false);
            return stmt == null ? Collections.emptyList() : handler.query(stmt, resultHandler);
        } finally {
            closeStatement(stmt);
        }
    }

DefaultResultSetHandler

/**
 org.apache.ibatis.executor.resultset.DefaultResultSetHandler 
**/
public class DefaultResultSetHandler implements ResultSetHandler {

  //重点是调用handleResultSet()方法, getFirstResultSet()方法
  @Override
  public List<Object> handleResultSets(Statement stmt) throws SQLException {
    ErrorContext.instance().activity("handling results").object(mappedStatement.getId());

    final List<Object> multipleResults = new ArrayList<>();

    int resultSetCount = 0;
    //调用getFirstResultSet()方法
    ResultSetWrapper rsw = getFirstResultSet(stmt);

    List<ResultMap> resultMaps = mappedStatement.getResultMaps();
    int resultMapCount = resultMaps.size();
    validateResultMapsCount(rsw, resultMapCount);
    while (rsw != null && resultMapCount > resultSetCount) {
      ResultMap resultMap = resultMaps.get(resultSetCount);
      //调用handleResultSet()方法
      handleResultSet(rsw, resultMap, multipleResults, null);
      rsw = getNextResultSet(stmt);
      cleanUpAfterHandlingResultSet();
      resultSetCount++;
    }

    String[] resultSets = mappedStatement.getResultSets();
    if (resultSets != null) {
      while (rsw != null && resultSetCount < resultSets.length) {
        ResultMapping parentMapping = nextResultMaps.get(resultSets[resultSetCount]);
        if (parentMapping != null) {
          String nestedResultMapId = parentMapping.getNestedResultMapId();
          ResultMap resultMap = configuration.getResultMap(nestedResultMapId);
          handleResultSet(rsw, resultMap, null, parentMapping);
        }
        rsw = getNextResultSet(stmt);
        cleanUpAfterHandlingResultSet();
        resultSetCount++;
      }
    }

    return collapseSingleResultList(multipleResults);
  }

  private ResultSetWrapper getFirstResultSet(Statement stmt) throws SQLException {
  	/**
	这里调用Statement接口的getResultSet()方法,返回ResultSet对象
	如果是DEBUG级别下,那么调用的则是Statement接口的代理对象的getResultSet()方法,那么就会进入StatementLogger类的invoke()方法,
	由之前梳理的StatementLogger类可知,在执行getResultSet()方法,返回的ResultSet对象实际上为代理对象
	**/
    ResultSet rs = stmt.getResultSet();
    while (rs == null) {
      // move forward to get the first resultset in case the driver
      // doesn't return the resultset as the first result (HSQLDB 2.1)
      if (stmt.getMoreResults()) {
        rs = stmt.getResultSet();
      } else {
        if (stmt.getUpdateCount() == -1) {
          // no more results. Must be no resultset
          break;
        }
      }
    }
    return rs != null ? new ResultSetWrapper(rs, configuration) : null;
  }

  //重点是调用handleRowValues()方法
  private void handleResultSet(ResultSetWrapper rsw, ResultMap resultMap, List<Object> multipleResults, ResultMapping parentMapping) throws SQLException {
    try {
      if (parentMapping != null) {
        //调用handleRowValues()方法
        handleRowValues(rsw, resultMap, null, RowBounds.DEFAULT, parentMapping);
      } else {
        if (resultHandler == null) {
          DefaultResultHandler defaultResultHandler = new DefaultResultHandler(objectFactory);
          //调用handleRowValues()方法
          handleRowValues(rsw, resultMap, defaultResultHandler, rowBounds, null);
          multipleResults.add(defaultResultHandler.getResultList());
        } else {
        //调用handleRowValues()方法
          handleRowValues(rsw, resultMap, resultHandler, rowBounds, null);
        }
      }
    } finally {
      // issue #228 (close resultsets)
      closeResultSet(rsw.getResultSet());
    }
  }

 //重点是调用handleRowValuesForNestedResultMap()方法
  public void handleRowValues(ResultSetWrapper rsw, ResultMap resultMap, ResultHandler<?> resultHandler, RowBounds rowBounds, ResultMapping parentMapping) throws SQLException {
    if (resultMap.hasNestedResultMaps()) {
      ensureNoRowBounds();
      checkResultHandler();
      handleRowValuesForNestedResultMap(rsw, resultMap, resultHandler, rowBounds, parentMapping);
    } else {
      handleRowValuesForSimpleResultMap(rsw, resultMap, resultHandler, rowBounds, parentMapping);
    }
  }

 //重点需要关注的方法
  private void handleRowValuesForNestedResultMap(ResultSetWrapper rsw, ResultMap resultMap, ResultHandler<?> resultHandler, RowBounds rowBounds, ResultMapping parentMapping) throws SQLException {
    final DefaultResultContext<Object> resultContext = new DefaultResultContext<>();
    ResultSet resultSet = rsw.getResultSet();
    skipRows(resultSet, rowBounds);
    Object rowValue = previousRowValue;

	/**
	这里使用while循环 ,重点关注resultSet.next()方法
	这里调用ResultSet接口的next()方法,返回执行的结果(Boolean值)
	如果是DEBUG级别,那么获取到的ResultSet对象也就是代理对象,调用next()方法时,会进入到ResultSetLogger的invoke()方法中
	由于上述梳理的ResultSetLogger类可知,执行invoke()时会返回执行结果,并且打印查询结果
	**/
    while (shouldProcessMoreRows(resultContext, rowBounds) && !resultSet.isClosed() && resultSet.next()) {
      final ResultMap discriminatedResultMap = resolveDiscriminatedResultMap(resultSet, resultMap, null);
      final CacheKey rowKey = createRowKey(discriminatedResultMap, rsw, null);
      Object partialObject = nestedResultObjects.get(rowKey);
      if (mappedStatement.isResultOrdered()) {
        if (partialObject == null && rowValue != null) {
          nestedResultObjects.clear();
          storeObject(resultHandler, resultContext, rowValue, parentMapping, resultSet);
        }
        rowValue = getRowValue(rsw, discriminatedResultMap, rowKey, null, partialObject);
      } else {
        rowValue = getRowValue(rsw, discriminatedResultMap, rowKey, null, partialObject);
        if (partialObject == null) {
          storeObject(resultHandler, resultContext, rowValue, parentMapping, resultSet);
        }
      }
    }
    if (rowValue != null && mappedStatement.isResultOrdered() && shouldProcessMoreRows(resultContext, rowBounds)) {
      storeObject(resultHandler, resultContext, rowValue, parentMapping, resultSet);
      previousRowValue = null;
    } else if (rowValue != null) {
      previousRowValue = rowValue;
    }
  }
}

至此,整个代码调用链梳理完毕
这里我们来梳理几个问题:
1.为什么要在ConnectionLogger中执行prepareStatement()时返回PreparedStatement的代理对象
2.为什么要在StatementLogger中执行getResultSet()时返回ResultSet的代理对象
3.为什么静态代理模式在这里不适用

答:
1.首先我们的需求是针对不同阶段的Sql执行过程去打印对应的日志(Sql语句,Sql执行参数,Sql执行结果)
2.但是他们都是在不同的对象中实现的,所以我们就需要针对每个对象去实现一个对应的代理对象
3.但是这些对象之间又是互相关联的
4.根据Connection对象调用prepareStatement()方法获取Statement对象
5.根据Statement对象调用getResultSet()方法获取ResultSet对象
6.所以我们需要在ConnectionLogger中对prepareStatement()实现扩展,让他返回Statement的代理对象
7.这样在Statement调用getResultSet()方法时,实际上是Statement对象的代理对象在调用getResultSet()方法,那么获取到的ResultSet对象
也就是代理对象了
8.这样在执行ResultSet对象的next()方法时,实际上也就是ResultSet对象的代理对象在调用next()方法
9.这样就能在不改动原有代码的情况下对执行的每一步进行代理,针对对应的方法实现扩展,打印日志
10.这种明显是静态代理无法做到的事情,因为静态代理需要事先知道真实对象才行,但是往往我们需要在不知道真实对象的情况下对目标对象进行代理

2.CGLIB动态代理

1.源码示例
2.框架应用
1.CGLIB动态代理在SpringBootAop中的应用
2.面试题
1.Spring中默认使用的是JDK的动态代理还是CGLIB动态代理?

先说结论:
1.在Spring中默认使用的是JDK的动态代理
2.在Spirng中如果代理对象有接口则使用JDK动态代理,否则使用CGLIB动态代理

2.SpringBoot中默认使用的是JDK的动态代理还是CGLIB动态代理?

先说结论:
1.在SpringBoot2.0版本之前默认使用的是JDK动态代理
2.在SpinrgBoot2.0版本之后默认使用的是CGLIB动态代理,同时也继承了Spring的逻辑(代理对象有接口则使用JDK动态代理,否则使用CGLIB动态代理)
3.支持在配置文件中进行修改(spring.aop.proxy-target-class)

先看SpringBootAop的配置类

2.0版本之前(包含2.0), 转载文章路径点击跳转至目标文章

@Configuration
@ConditionalOnClass({ EnableAspectJAutoProxy.class, Aspect.class, Advice.class })
@ConditionalOnProperty(prefix = "spring.aop", name = "auto", havingValue = "true", matchIfMissing = true)
public class AopAutoConfiguration {

	@Configuration
	@EnableAspectJAutoProxy(proxyTargetClass = false)
	@ConditionalOnProperty(prefix = "spring.aop", name = "proxy-target-class", havingValue = "false",
			matchIfMissing = true)
	public static class JdkDynamicAutoProxyConfiguration {

	}

	@Configuration
	@EnableAspectJAutoProxy(proxyTargetClass = true)
	@ConditionalOnProperty(prefix = "spring.aop", name = "proxy-target-class", havingValue = "true",
			matchIfMissing = false)
	public static class CglibAutoProxyConfiguration {

	}
}

2.0版本之后(在源码中截取的)

@Configuration(proxyBeanMethods = false)
@ConditionalOnProperty(prefix = "spring.aop", name = "auto", havingValue = "true", matchIfMissing = true)
public class AopAutoConfiguration {

	@Configuration(proxyBeanMethods = false)
	@ConditionalOnClass(Advice.class)
	static class AspectJAutoProxyingConfiguration {

		@Configuration(proxyBeanMethods = false)
		@EnableAspectJAutoProxy(proxyTargetClass = false)
		@ConditionalOnProperty(prefix = "spring.aop", name = "proxy-target-class", havingValue = "false")
		static class JdkDynamicAutoProxyConfiguration {

		}

		@Configuration(proxyBeanMethods = false)
		@EnableAspectJAutoProxy(proxyTargetClass = true)
		@ConditionalOnProperty(prefix = "spring.aop", name = "proxy-target-class", havingValue = "true",
				matchIfMissing = true)
		static class CglibAutoProxyConfiguration {

		}
	}
}

根据以上俩段不同版本的SpringBootAop的配置文件可知,对于JDK动态代理跟CGLIB动态代理的默认优先级是不同的.

下面重点研究SpringBoot2.0版本之后的AOP中对于动态代理的使用

/**
org.springframework.aop.framework.DefaultAopProxyFactory
此类为AOP创建代理类的核心工厂类
**/
public class DefaultAopProxyFactory implements AopProxyFactory, Serializable {

	@Override
	public AopProxy createAopProxy(AdvisedSupport config) throws AopConfigException {
		/**
		这个if判断决定是否可以直接使用JDK动态代理
		!NativeDetector.inNativeImage():
			读取系统配置:System.getProperty("org.graalvm.nativeimage.imagecode"),没仔细研究过,但是返回的一直是fasle,取非的话这个条件就是true
		config.isOptimize():
			源码的注释为是否需要优化,默认值为fasle
		config.isProxyTargetClass():(重点关注)
			核心条件,表示是否使用目标对象生成代理类,也就是是否使用CGLIB动态代理,默认为true
		hasNoUserSuppliedProxyInterfaces():
			判断目标对象是否实现了接口或者是SpringProxy的子类
			如果目标对象实现了接口并且不为SpringProxy的子类则使用JDK的动态代理,否则使用CGLIB动态代理
		**/
		if (!NativeDetector.inNativeImage() &&
				(config.isOptimize() || config.isProxyTargetClass() || hasNoUserSuppliedProxyInterfaces(config))) {
			Class<?> targetClass = config.getTargetClass();
			if (targetClass == null) {
				throw new AopConfigException("TargetSource cannot determine target class: " +
						"Either an interface or a target is required for proxy creation.");
			}
			/**
			这个if判断最终决定使用JDK或者CGILB动态代理
			targetClass.isInterface():
				目标对象是否为接口,如果是则使用JDK动态代理
			Proxy.isProxyClass():
				目标对象是否本身就是代理类,如果是则使用JDK动态代理	
			**/
			if (targetClass.isInterface() || Proxy.isProxyClass(targetClass)) {
				return new JdkDynamicAopProxy(config);
			}
			return new ObjenesisCglibAopProxy(config);
		}
		else {
			return new JdkDynamicAopProxy(config);
		}
	}

	private boolean hasNoUserSuppliedProxyInterfaces(AdvisedSupport config) {
		Class<?>[] ifcs = config.getProxiedInterfaces();
		return (ifcs.length == 0 || (ifcs.length == 1 && SpringProxy.class.isAssignableFrom(ifcs[0])));
	}
}

由以上创建代理对象的代码可知
1.SpringBootAop中虽然默认使用的是CGLIB动态代理,但是最终还是需要根据逻辑判断来决定
2.虽然可以在配置文件中设置(spring.aop.proxy-target-class:fasle)来强制使用JDK动态代理,但是对于没有实现接口的目标对象依然会使用CGLIB动态代理(防止代码报错)

下面看一下当你设置了spring.aop.proxy-target-class:fasle时的代码调用流程

/**
org.springframework.aop.framework.autoproxy.AbstractAutoProxyCreator 

**/
public abstract class AbstractAutoProxyCreator extends ProxyProcessorSupport
		implements SmartInstantiationAwareBeanPostProcessor, BeanFactoryAware {
	
	protected Object createProxy(Class<?> beanClass, @Nullable String beanName,
			@Nullable Object[] specificInterceptors, TargetSource targetSource) {

		if (this.beanFactory instanceof ConfigurableListableBeanFactory) {
			AutoProxyUtils.exposeTargetClass((ConfigurableListableBeanFactory) this.beanFactory, beanName, beanClass);
		}

		ProxyFactory proxyFactory = new ProxyFactory();
		proxyFactory.copyFrom(this);

		//当设置了spring.aop.proxy-target-class:fasle时,会进入这个if分支
		if (!proxyFactory.isProxyTargetClass()) {
			/**
			确定是否应使用给定bean的目标类而不是接口来代理该bean.(注释上的解释)
			其实就是返回是否使用CGLIB动态代理
			**/
			if (shouldProxyTargetClass(beanClass, beanName)) {
				proxyFactory.setProxyTargetClass(true);
			}
			else {
				/**
				校验目标类是否实现了接口,否则将proxyTargetClass设置为true
				**/
				evaluateProxyInterfaces(beanClass, proxyFactory);
			}
		}

		Advisor[] advisors = buildAdvisors(beanName, specificInterceptors);
		proxyFactory.addAdvisors(advisors);
		proxyFactory.setTargetSource(targetSource);
		customizeProxyFactory(proxyFactory);

		proxyFactory.setFrozen(this.freezeProxy);
		if (advisorsPreFiltered()) {
			proxyFactory.setPreFiltered(true);
		}

		ClassLoader classLoader = getProxyClassLoader();
		if (classLoader instanceof SmartClassLoader && classLoader != beanClass.getClassLoader()) {
			classLoader = ((SmartClassLoader) classLoader).getOriginalClassLoader();
		}
		return proxyFactory.getProxy(classLoader);
	}
}
//org.springframework.aop.framework.ProxyProcessorSupport
public class ProxyProcessorSupport extends ProxyConfig implements Ordered, BeanClassLoaderAware, AopInfrastructureBean {
	
	protected void evaluateProxyInterfaces(Class<?> beanClass, ProxyFactory proxyFactory) {
		//获取目标类上的所有接口
		Class<?>[] targetInterfaces = ClassUtils.getAllInterfacesForClass(beanClass, getProxyClassLoader());
		//定义变量,默认为fasle
		boolean hasReasonableProxyInterface = false;
		//循环目标类实现的接口数组,只要有一个接口满足以下条件直接结束循环,并修改变量为true
		for (Class<?> ifc : targetInterfaces) {
			/**	
			isConfigurationCallbackInterface():
				判断给定接口是否只是容器回调,其实就是说目标类实现的接口不能是如下定义的相关接口
			isInternalLanguageInterface():
				判断目标类实现的接口不能是如下定义的相关接口
			ifc.getMethods().length > 0:
				接口中的方法数量要>0
			**/
			if (!isConfigurationCallbackInterface(ifc) && !isInternalLanguageInterface(ifc) &&
					ifc.getMethods().length > 0) {
				hasReasonableProxyInterface = true;
				break;
			}
		}
		/**
		如果上述判断条件一个都没满足,则直接设置proxyTargetClass为true,表示使用CGLIB动态代理
		否则将实现的接口都添加至proxyFactory中的成员变量中
		**/
		if (hasReasonableProxyInterface) {
			for (Class<?> ifc : targetInterfaces) {
				proxyFactory.addInterface(ifc);
			}
		}
		else {
			proxyFactory.setProxyTargetClass(true);
		}
	}

	protected boolean isConfigurationCallbackInterface(Class<?> ifc) {
		return (InitializingBean.class == ifc || DisposableBean.class == ifc || Closeable.class == ifc ||
				AutoCloseable.class == ifc || ObjectUtils.containsElement(ifc.getInterfaces(), Aware.class));
	}

	protected boolean isInternalLanguageInterface(Class<?> ifc) {
		return (ifc.getName().equals("groovy.lang.GroovyObject") ||
				ifc.getName().endsWith(".cglib.proxy.Factory") ||
				ifc.getName().endsWith(".bytebuddy.MockAccess"));
	}
	
}
public abstract class AutoProxyUtils {

	public static boolean shouldProxyTargetClass(
			ConfigurableListableBeanFactory beanFactory, @Nullable String beanName) {

		if (beanName != null && beanFactory.containsBeanDefinition(beanName)) {
			BeanDefinition bd = beanFactory.getBeanDefinition(beanName);
			return Boolean.TRUE.equals(bd.getAttribute(PRESERVE_TARGET_CLASS_ATTRIBUTE));
		}
		return false;
	}
}

四、总结

提示:这里对文章进行总结:

还没想好呢!!!

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值