MyBatis拦截器(插件)分页

1、MyBatis核心配置文件:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE configuration
PUBLIC "-//ibatis.apache.org//DTD Config 3.0//EN"
"http://ibatis.apache.org/dtd/ibatis-3-config.dtd">
<configuration>
    <!-- 设置别名 -->
    <typeAliases>
        <!-- 2. 指定扫描包,会把包内所有的类都设置别名,别名的名称就是类名,大小写不敏感 -->
        <package name="com.sgl.entity" />
    </typeAliases>
    
    <plugins>
        <plugin interceptor="com.sgl.interceptor.PageInterceptor">
            <property name="limit" value="10"/>
            <property name="dbType" value="mysql"/>
        </plugin>
    </plugins>
</configuration>

PageInfo.java

package com.sgl.interceptor;

import java.util.List;

import com.alibaba.fastjson.JSONObject;

public class PageInfo {
    
    private int pageIndex;// 当前页
    private int pageSize = 10;// 每页记录数
    private int totalCount;// 总记录数
    private int pageCount;// 总页数
    private boolean hasPre;// 是否首页,true表示不是首页
    private boolean hasNext;// 是否尾页,true表示不是尾页
    private JSONObject filter;// 查询过滤条件
    private List<?> results;// 结果集

    public PageInfo() {
        super();
    }

    public PageInfo(int pageIndex, int pageSize) {
        super();
        this.pageIndex = pageIndex;
        this.pageSize = pageSize;
    }

    public int getPageIndex() {
        return pageIndex;
    }

    public void setPageIndex(int pageIndex) {
        this.pageIndex = pageIndex;
    }

    public int getPageSize() {
        return pageSize;
    }

    public void setPageSize(int pageSize) {
        this.pageSize = pageSize;
    }

    public int getTotalCount() {
        return totalCount;
    }

    public void setTotalCount(int totalCount) {
        this.totalCount = totalCount;
    }

    public int getPageCount() {
        return totalCount%pageSize==0?(totalCount/pageSize):(totalCount/pageSize+1);
    }


    public boolean getHasPre(){
        return (pageIndex>1);
    }
    
    public boolean getHasNext(){
        return (pageIndex<getPageCount());
    }

    public JSONObject getFilter() {
        return filter;
    }

    public void setFilter(JSONObject filter) {
        this.filter = filter;
    }

    public List<?> getResults() {
        return results;
    }

    public void setResults(List<?> results) {
        this.results = results;
    }

    @Override
    public String toString() {
        return "PageInfo [pageIndex=" + pageIndex + ", pageSize=" + pageSize + ", totalCount=" + totalCount
                + ", pageCount=" + pageCount + ", hasPre=" + hasPre + ", hasNext=" + hasNext + ", filter=" + filter
                + ", results=" + results + "]";
    }

}

2、简单例子:

package com.sgl.interceptor;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.text.DateFormat;
import java.util.Date;
import java.util.List;
import java.util.Locale;
import java.util.Properties;

import org.apache.ibatis.executor.parameter.ParameterHandler;
import org.apache.ibatis.executor.statement.StatementHandler;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.ParameterMapping;
import org.apache.ibatis.plugin.Interceptor;
import org.apache.ibatis.plugin.Intercepts;
import org.apache.ibatis.plugin.Invocation;
import org.apache.ibatis.plugin.Plugin;
import org.apache.ibatis.plugin.Signature;
import org.apache.ibatis.reflection.MetaObject;
import org.apache.ibatis.reflection.SystemMetaObject;
import org.apache.ibatis.session.Configuration;
import org.apache.ibatis.type.TypeHandlerRegistry;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

@Intercepts({
        @Signature(type = StatementHandler.class, method = "prepare", args = { Connection.class, Integer.class }) })
public class PageInterceptor implements Interceptor {

    private static Logger logger = LoggerFactory.getLogger(PageInterceptor.class);

    private static final String SELECT_ID = "selectpage";

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        StatementHandler statementHandler = (StatementHandler) invocation.getTarget();
        Connection connection = (Connection) invocation.getArgs()[0];
        // 获取statementHandler包装类
        MetaObject statmentHandlerObject = SystemMetaObject.forObject(statementHandler);
        // 获取操作数据库接口映射的相关信息
        MappedStatement mappedStatement = (MappedStatement) statmentHandlerObject.getValue("delegate.mappedStatement");
        ParameterHandler parameterHandler = (ParameterHandler) statmentHandlerObject
                .getValue("delegate.parameterHandler");

        String id = mappedStatement.getId();
        String sql = statementHandler.getBoundSql().getSql();// 获取执行的sql语句
        Object param = statementHandler.getBoundSql().getParameterObject();// 获取sql中的参数

        // 对id为selectPage的查询进行分页处理
        if (SELECT_ID.equals(id.substring(id.lastIndexOf(".") + 1).toLowerCase())) {
            // 获取分页参数
            PageInfo pageInfo = (PageInfo) parameterHandler.getParameterObject();
            int pageIndex = pageInfo.getPageIndex();
            int pageSize = pageInfo.getPageSize();

            // 获取查询总记录数
            int totalCount = getTotalCount(mappedStatement, statementHandler, connection);
            pageInfo.setTotalCount(totalCount);
            // 拼接分页查询sql语句
            String limitSql = sql.trim() + " limit " + (pageIndex - 1) * pageSize + " , " + pageSize;

            logger.info("执行的sql[方法]:=====》{}", id);
            logger.info("执行的sql[语句]:=====》{}", limitSql.replaceAll("[\s]+", " "));
            logger.info("执行的sql[参数]:=====》{}", param);

            // 将构建完成的分页sql语句赋值个体'delegate.boundSql.sql',替换原来的sql语句
            statmentHandlerObject.setValue("delegate.boundSql.sql", limitSql);
        } else {
            logger.info("执行的sql[方法]:=====》{}", id);
            logger.info("执行的sql[语句]:=====》{}", sql.replaceAll("[\s]+", " "));
            logger.info("执行的sql[参数]:=====》{}", param);
        }

        return invocation.proceed();
    }

    // 拦截类型StatementHandler
    @Override
    public Object plugin(Object object) {
        if (object instanceof StatementHandler) {
            return Plugin.wrap(object, this);
        } else {
            return object;
        }
    }

    @Override
    public void setProperties(Properties properties) {

    }

    // 获取查询的总记录数
    private int getTotalCount(MappedStatement mappedStatement, StatementHandler statementHandler,
            Connection connection) {
        StringBuffer sb = new StringBuffer();
        sb.append("SELECT COUNT(1) as count FROM (" + showSql(mappedStatement, statementHandler) + ") t ");

        PreparedStatement preparedStatement = null;
        ResultSet resultSet = null;
        int totalCount = 0;
        try {
            preparedStatement = connection.prepareStatement(sb.toString());
            resultSet = preparedStatement.executeQuery();
            if (resultSet.next()) {
                totalCount = resultSet.getInt(1);
            }
        } catch (Exception e) {
            logger.info("分页查询获取总记录数发生异常====》{}", e.getMessage());
        } finally {
            try {
                if (resultSet != null) {
                    resultSet.close();
                }
                if (preparedStatement != null) {
                    preparedStatement.close();
                }
            } catch (Exception e2) {
                logger.info("关闭资源异常====>{}" + e2.getMessage());
            }
        }
        return totalCount;
    }

    /**
     * 把sql语句中的?替换成实际参数值,并返回sql语句
     * 
     * @return
     */
    private String showSql(MappedStatement mappedStatement, StatementHandler statementHandler) {
        Configuration configuration = mappedStatement.getConfiguration();
        BoundSql boundSql = statementHandler.getBoundSql();
        String sql = boundSql.getSql().replaceAll("[\s]+", " ");
        List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
        Object parameterObject = boundSql.getParameterObject();
        if (parameterMappings.size() > 0 && parameterObject != null) {
            // 获取基础类型处理器注册器
            TypeHandlerRegistry typeHandlerRegistry = configuration.getTypeHandlerRegistry();
            if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {// 判断参数类型是否已注册(判断是否是基本参数类型)
                sql = sql.replaceFirst("\?", getParameterValue(parameterObject));
            } else {
                MetaObject metaObject = configuration.newMetaObject(parameterObject);
                for (ParameterMapping parameterMapping : parameterMappings) {
                    String propertyName = parameterMapping.getProperty();
                    if (metaObject.hasGetter(propertyName)) {
                        Object value = metaObject.getValue(propertyName);
                        sql = sql.replaceFirst("\?", getParameterValue(value));
                    } else if (boundSql.hasAdditionalParameter(propertyName)) {
                        Object additionalParameter = boundSql.getAdditionalParameter(propertyName);
                        sql = sql.replaceFirst("\?", getParameterValue(additionalParameter));
                    }
                }
            }
        }
        return sql;
    }

    /**
     * 对参数值进行分类处理
     * 
     * @param object
     * @return
     */
    private String getParameterValue(Object object) {
        String value = null;
        if (object instanceof String) {
            value = "'" + object.toString() + "'";
        } else if (object instanceof Date) {
            DateFormat dateFormat = DateFormat.getDateTimeInstance(DateFormat.DEFAULT, DateFormat.DEFAULT,
                    Locale.CHINA);
            value = "'" + dateFormat.format(new Date()) + "'";
        } else {
            if (object != null) {
                value = object.toString();
            } else {
                value = "";
            }
        }
        return value;
    }

}

3、原理解析:

//注解拦截器并签名
@Intercepts({@Signature(type=StatementHandler.class,method="prepare",args={Connection.class,Integer.class})})
method="prepare"和StatementHandler服务类中prepare方法相对应。
Statement prepare(Connection connection, Integer transactionTimeout) throws SQLException;

  自定义的插件类,都需要使用@Intercepts注解,@Signature是对插件需要拦截的对象进行签名,type表示要拦截的类型,method表示拦截类中的方法,args是需要的参数,这里的参数在后面也可以获取到。

  StatementHandler:数据库会话器,专门用于处理数据库会话,statement的执行操作,是一个接口。

  MetaObject:mybatis工具类,可以有效的读取或修改一些重要对象的属性,基本思想是通过反射去获取和设置对象的属性值,只是MetaObject类不需要我们自己去实现具体反射的方法,已经封装好了。

  通过MetaObject.getValue()和MetaObject.setValue(name,value)方法去获取对象属性值和设置对象属性值。

String sql = (String) metaObjectHandler.getValue("delegate.boundSql.sql");
metaObjectHandler.setValue("delegate.boundSql.sql", limitSql);

通过MetaObject属性的获取流程:

MappedStatement mappedStatement = (MappedStatement) metaObjectHandler.getValue("delegate.mappedStatement");

  metaObjectHandler是一个MetaObject对象。

  首先通过metaStatementHandler.getValue(“delegate”)拿到真正实现StatementHandler接口的服务对象。

public class RoutingStatementHandler implements StatementHandler {
//delegate属性来自这里,是一个实现了StatementHandler接口的类
    private final StatementHandler delegate;
//通过这里给delegate属性赋值
    public RoutingStatementHandler(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {
        switch(RoutingStatementHandler.SyntheticClass_1.$SwitchMap$org$apache$ibatis$mapping$StatementType[ms.getStatementType().ordinal()]) {
        case 1:
            this.delegate = new SimpleStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql);
            break;
        case 2:
            this.delegate = new PreparedStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql);
            break;
        case 3:
            this.delegate = new CallableStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql);
            break;
        default:
            throw new ExecutorException("Unknown statement type: " + ms.getStatementType());
        }

    }
}

  拿到具体的服务对象(处理逻辑的StatementHandler实现类)后,再获取mappedStatement属性,我们再来看mappedStatement属性的定义:

public abstract class BaseStatementHandler implements StatementHandler {
    protected final Configuration configuration;
    protected final ObjectFactory objectFactory;
    protected final TypeHandlerRegistry typeHandlerRegistry;
    protected final ResultSetHandler resultSetHandler;
    protected final ParameterHandler parameterHandler;
    protected final Executor executor;
    //定义在这里
    protected final MappedStatement mappedStatement;
    protected final RowBounds rowBounds;
    protected BoundSql boundSql;
}

  可以看出是定义在BaseStatementHandler中的属性,三个具体的服务对象都会继承BaseStatementHandler。这里有很多和执行数据库操作相关的属性,如果我们需要的话,都可以通过上述方式获取,如果相获取下层对象的属性,按照这个写法一次获取也可以拿到。

  RoutingStatementHandler:不是真正的服务对象,它通过适配器模式找到正确的StatementHandler去执行操作。通过invocation.getTarget()获取到的是一个RoutingStatementHandler代理对象,再通过MappedStatement中不同的类型,找到具体的处理类。

  真正实现StatementHandler接口的服务对象有:SimpleStatementHandler,PreparedStatementHandler,CallableStatementHandler都继承BaseStatementHandler,它们分别对应是三种不同的执行器:SIMPLE:默认的简单执行器;REUSE:重用预处理语句的执行期;BATCH:重用语句和批量更新处理器。

  BoundSql: 用于组装SQL和参数,使用插件时需要通过它拿到当前运行的SQL和参数及参数规则。它有如下几个重要属性:

public class BoundSql {
    private String sql;
    private List<ParameterMapping> parameterMappings;
    private Object parameterObject;
    private Map<String, Object> additionalParameters;
    private MetaObject metaParameters;
}

  parameterObject:是参数本身,调用方法时传递进来的参数。可以是pojo,map或@param注解的参数等。

  parameterMappings:它是一个List,存储了许多ParameterMapping对象。这个对象会描述我们的参数,参数包括属性、名称、表达式、javaType、jdbcType等。

  sql:我们书写在mapper.xml文件中的一条sql语句。

  MappedStatement:存储mapper.xml文件中一条sql语句配置的所有信息。

  Connection:连接对象,在插件中会依赖它去进行一些数据库操作。

  Configuration:包含mybatis所有的配置信息。

  ParameterHandler:接口,对预编译语句进行参数设置。即将参数设置到sql语句中。它有两个重要方法:getParameterObject()用于获取参数对象和 
  setParameters(PreparedStatement var1)用于设置参数对象。

在插件中我们使用了一个辅助类,来封装分页时会用到的一些参数,定义如下:

/**
 * description:实现分页的辅助类,用于封装用于分页的一些参数
 */
public class PageParam {
    private Integer defaultPage;
    //    默认每页显示条数
    private Integer defaultPageSize;
    //    是否启用分页功能
    private Boolean defaultUseFlag;
    //    是否检测当前页码的合法性(大于最大页码或小于最小页码都不合法)
    private Boolean defaultCheckFlag;
    //当前sql查询的总记录数,回填
    private Integer totle;
    //    当前sql查询实现分页后的总页数,回填
    private Integer totlePage;

    ...(省略get和set方法)

当需要使用到分页功能时,我们只需要将分页参数封装到PageParam对象中,并且作为参数传递到查询方法中,插件中就会自动获取到这些参数,并且动态组分页的Sql查询语句。下面就是我们自定义的分页插件类实现:

import org.apache.ibatis.executor.parameter.ParameterHandler;
import org.apache.ibatis.executor.statement.RoutingStatementHandler;
import org.apache.ibatis.executor.statement.StatementHandler;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.plugin.*;
import org.apache.ibatis.reflection.MetaObject;
import org.apache.ibatis.reflection.SystemMetaObject;
import org.apache.ibatis.scripting.defaults.DefaultParameterHandler;
import javax.security.auth.login.Configuration;
import java.lang.reflect.InvocationTargetException;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.Map;
import java.util.Properties;

/**
 * description:插件分页
 */
@Intercepts(@Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class}))
public class PageInterceptor implements Interceptor {
    //    默认页码
    private Integer defaultPage;
    //    默认每页显示条数
    private Integer defaultPageSize;
    //    是否启用分页功能
    private boolean defaultUseFlag;
    //    检测当前页码的合法性(大于最大页码或小于最小页码都不合法)
    private boolean defaultCheckFlag;

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        StatementHandler statementHandler = getActuralHandlerObject(invocation);

        MetaObject metaStatementHandler = SystemMetaObject.forObject(statementHandler);

        String sql = statementHandler.getBoundSql().getSql();

//        检测未通过,不是select语句
        if (!checkIsSelectFalg(sql)) {
            return invocation.proceed();
        }

        BoundSql boundSql = statementHandler.getBoundSql();

        Object paramObject = boundSql.getParameterObject();

        PageParam pageParam = getPageParam(paramObject);

        if (pageParam == null)
            return invocation.proceed();

        Integer pageNum = pageParam.getDefaultPage() == null ? defaultPage : pageParam.getDefaultPage();
        Integer pageSize = pageParam.getDefaultPageSize() == null ? defaultPageSize : pageParam.getDefaultPageSize();

        Boolean useFlag = pageParam.isDefaultUseFlag() == null ? defaultUseFlag : pageParam.isDefaultUseFlag();
        Boolean checkFlag = pageParam.isDefaultCheckFlag() == null ? defaultCheckFlag : pageParam.isDefaultCheckFlag();

        //不使用分页功能
        if (!useFlag) {
            return invocation.proceed();
        }

        int totle = getTotle(invocation, metaStatementHandler, boundSql);

        //将动态获取到的分页参数回填到pageParam中
        setTotltToParam(pageParam, totle, pageSize);

        //检查当前页码的有效性
        checkPage(checkFlag, pageNum, pageParam.getTotlePage());

        //修改sql
        return updateSql2Limit(invocation, metaStatementHandler, boundSql, pageNum, pageSize);
    }

    @Override
    public Object plugin(Object o) {
        return Plugin.wrap(o, this);
    }

    //    在配置插件的时候配置默认参数
    @Override
    public void setProperties(Properties properties) {
        String strDefaultPage = properties.getProperty("default.page");
        String strDefaultPageSize = properties.getProperty("default.pageSize");
        String strDefaultUseFlag = properties.getProperty("default.useFlag");
        String strDefaultCheckFlag = properties.getProperty("default.checkFlag");
        defaultPage = Integer.valueOf(strDefaultPage);
        defaultPageSize = Integer.valueOf(strDefaultPageSize);
        defaultUseFlag = Boolean.valueOf(strDefaultUseFlag);
        defaultCheckFlag = Boolean.valueOf(strDefaultCheckFlag);
    }


    //    从代理对象中分离出真实statementHandler对象,非代理对象
    private StatementHandler getActuralHandlerObject(Invocation invocation) {
        StatementHandler statementHandler = (StatementHandler) invocation.getTarget();
        MetaObject metaStatementHandler = SystemMetaObject.forObject(statementHandler);
        Object object = null;
//        分离代理对象链,目标可能被多个拦截器拦截,分离出最原始的目标类
        while (metaStatementHandler.hasGetter("h")) {
            object = metaStatementHandler.getValue("h");
            metaStatementHandler = SystemMetaObject.forObject(object);
        }

        if (object == null) {
            return statementHandler;
        }
        return (StatementHandler) object;
    }

    //    判断是否是select语句,只有select语句,才会用到分页
    private boolean checkIsSelectFalg(String sql) {
        String trimSql = sql.trim();
        int index = trimSql.toLowerCase().indexOf("select");
        return index == 0;
    }

    /*
    获取分页的参数

    参数可以通过map,@param注解进行参数传递。或者请求pojo继承自PageParam  将PageParam中的分页数据放进去
     */
    private PageParam getPageParam(Object paramerObject) {
        if (paramerObject == null) {
            return null;
        }

        PageParam pageParam = null;
        //通过map和@param注解将PageParam参数传递进来,pojo继承自PageParam不推荐使用  这里从参数中提取出传递进来的pojo继承自PageParam

//        首先处理传递进来的是map对象和通过注解方式传值的情况,从中提取出PageParam,循环获取map中的键值对,取出PageParam对象
        if (paramerObject instanceof Map) {
            Map<String, Object> params = (Map<String, Object>) paramerObject;
            for (Map.Entry<String, Object> entry : params.entrySet()) {
                if (entry.getValue() instanceof PageParam) {
                    return (PageParam) entry.getValue();
                }
            }
        } else if (paramerObject instanceof PageParam) {
//            继承方式 pojo继承自PageParam 只取出我们希望得到的分页参数
            pageParam = (PageParam) paramerObject;

        }
        return pageParam;
    }

    //    获取当前sql查询的记录总数
    private int getTotle(Invocation invocation, MetaObject metaStatementHandler, BoundSql boundSql) {
//        获取mapper文件中当前查询语句的配置信息
        MappedStatement mappedStatement = (MappedStatement) metaStatementHandler.getValue("delegate.mappedStatement");

        //获取所有配置Configuration
        org.apache.ibatis.session.Configuration configuration = mappedStatement.getConfiguration();

//        获取当前查询语句的sql
        String sql = (String) metaStatementHandler.getValue("delegate.boundSql.sql");

//        将sql改写成统计记录数的sql语句,这里是mysql的改写语句,将第一次查询结果作为第二次查询的表
        String countSql = "select count(*) as totle from (" + sql + ") $_paging";

//        获取connection连接对象,用于执行countsql语句
        Connection conn = (Connection) invocation.getArgs()[0];

        PreparedStatement ps = null;

        int totle = 0;

        try {
//            预编译统计总记录数的sql
            ps = conn.prepareStatement(countSql);
            //构建统计总记录数的BoundSql
            BoundSql countBoundSql = new BoundSql(configuration, countSql, boundSql.getParameterMappings(), boundSql.getParameterObject());
            //构建ParameterHandler,用于设置统计sql的参数
            ParameterHandler parameterHandler = new DefaultParameterHandler(mappedStatement, boundSql.getParameterObject(), countBoundSql);
            //设置总数sql的参数
            parameterHandler.setParameters(ps);
            //执行查询语句
            ResultSet rs = ps.executeQuery();
            while (rs.next()) {
//                与countSql中设置的别名对应
                totle = rs.getInt("totle");
            }
        } catch (SQLException e) {
            e.printStackTrace();
        } finally {
            if (ps != null)
                try {
                    ps.close();
                } catch (SQLException e) {
                    e.printStackTrace();
                }
        }
        return totle;
    }

    //    设置条数参数到pageparam对象
    private void setTotltToParam(PageParam param, int totle, int pageSize) {
        param.setTotle(totle);
        param.setTotlePage(totle % pageSize == 0 ? totle / pageSize : (totle / pageSize) + 1);
    }

    //    修改原始sql语句为分页sql语句
    private Object updateSql2Limit(Invocation invocation, MetaObject metaStatementHandler, BoundSql boundSql, int page, int pageSize) throws InvocationTargetException, IllegalAccessException, SQLException {
        String sql = (String) metaStatementHandler.getValue("delegate.boundSql.sql");
        //构建新的分页sql语句
        String limitSql = "select * from (" + sql + ") $_paging_table limit ?,?";
        //修改当前要执行的sql语句
        metaStatementHandler.setValue("delegate.boundSql.sql", limitSql);
        //相当于调用prepare方法,预编译sql并且加入参数,但是少了分页的两个参数,它返回一个PreparedStatement对象
        PreparedStatement ps = (PreparedStatement) invocation.proceed();
        //获取sql总的参数总数
        int count = ps.getParameterMetaData().getParameterCount();
        //设置与分页相关的两个参数
        ps.setInt(count - 1, (page - 1) * pageSize);
        ps.setInt(count, pageSize);
        return ps;
    }

    //    验证当前页码的有效性
    private void checkPage(boolean checkFlag, Integer pageNumber, Integer pageTotle) throws Exception {
        if (checkFlag) {
            if (pageNumber > pageTotle) {
                throw new Exception("查询失败,查询页码" + pageNumber + "大于总页数" + pageTotle);
            }
        }
    }
}
原文地址:https://www.cnblogs.com/a591378955/p/8483554.html