1 Star 0 Fork 165

ElonChung / Java-Review

forked from flatfish / Java-Review 
加入 Gitee
与超过 1200万 开发者一起发现、参与优秀开源项目,私有仓库也完全免费 :)
免费加入
克隆/下载
SSM-Mybatis3-源码解读-第13篇-Binding模块.md 27.91 KB
一键复制 编辑 原始数据 按行查看 历史
icanci 提交于 2020-09-07 10:22 . :zap:更新文件名称

SSM - Mybatis3 - 源码解读 - 第13篇 - Binding模块

概述

  • Mybatis的 Binding模块,对应 binding

1599439357907

  • 在调用 SqlSession 相应方法执行数据库操作时,需要指定映射文件中定义的 SQL 节点,如果出现拼写错误,我们只能在运行时才能发现相应的异常。为了尽早发现这种错误,MyBatis 通过 Binding 模块,将用户自定义的 Mapper 接口与映射配置文件关联起来,系统可以通过调用自定义 Mapper 接口中的方法执行相应的 SQL 语句完成数据库操作,从而避免上述问题。
  • 值得读者注意的是,开发人员无须编写自定义 Mapper 接口的实现,MyBatis 会自动为其创建动态代理对象。在有些场景中,自定义 Mapper 接口可以完全代替映射配置文件,但有的映射规则和 SQL 语句的定义还是写在映射配置文件中比较方便,例如动态 SQL 语句的定义。

MapperRegistry

  • org.apache.ibatis.binding.MapperRegistry ,Mapper 注册表
public class MapperRegistry {

    // Mybatis Configuration 对象
    private final Configuration config;
    // MapperProxyFactory 的映射
    // KEY:Mapper接口
    private final Map<Class<?>, MapperProxyFactory<?>> knownMappers = new HashMap<>();

    // 构造函数初始化
    public MapperRegistry(Configuration config) {
        this.config = config;
    }

    // 获得 Mapper Proxy 对象
    @SuppressWarnings("unchecked")
    public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
        final MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory<T>) knownMappers.get(type);
        if (mapperProxyFactory == null) {
            throw new BindingException("Type " + type + " is not known to the MapperRegistry.");
        }
        try {
            return mapperProxyFactory.newInstance(sqlSession);
        } catch (Exception e) {
            throw new BindingException("Error getting mapper instance. Cause: " + e, e);
        }
    }

    // 是否有此类型的 class
    public <T> boolean hasMapper(Class<T> type) {
        return knownMappers.containsKey(type);
    }

    // 添加到 knownMappers 中
    public <T> void addMapper(Class<T> type) {
        // 判断是不是接口
        if (type.isInterface()) {
            // 判断是不是已经有了此类型,有了就抛出异常
            if (hasMapper(type)) {
                throw new BindingException("Type " + type + " is already known to the MapperRegistry.");
            }
            boolean loadCompleted = false;
            try {
                // 添加到 knowMappers中
                knownMappers.put(type, new MapperProxyFactory<>(type));
                // It's important that the type is added before the parser is run
                // otherwise the binding may automatically be attempted by the
                // mapper parser. If the type is already known, it won't try.
                // 注解配置
                MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type);
                // 解析
                parser.parse();
                // 标记加载完成
                loadCompleted = true;
            } finally {
                // 如果加载没完成。就将其移除
                if (!loadCompleted) {
                    knownMappers.remove(type);
                }
            }
        }
    }

    /**
   * Gets the mappers.
   *
   * @return the mappers
   * @since 3.2.2
   */
    public Collection<Class<?>> getMappers() {
        return Collections.unmodifiableCollection(knownMappers.keySet());
    }

    /**
   * Adds the mappers.
   *
   * @param packageName
   *          the package name
   * @param superType
   *          the super type
   * @since 3.2.2
   */
    // 扫描指定包,并将符合的类,放到knownMappers
    public void addMappers(String packageName, Class<?> superType) {
        // 创建 ResolverUtil 对象
        ResolverUtil<Class<?>> resolverUtil = new ResolverUtil<>();
		// 扫描指定的类
        resolverUtil.find(new ResolverUtil.IsA(superType), packageName);
        Set<Class<? extends Class<?>>> mapperSet = resolverUtil.getClasses();
        // 遍历,添加到 knowMappers中
        for (Class<?> mapperClass : mapperSet) {
            addMapper(mapperClass);
        }
    }

    /**
   * Adds the mappers.
   *
   * @param packageName
   *          the package name
   * @since 3.2.2
   */
    
    // 扫描包
    public void addMappers(String packageName) {
        addMappers(packageName, Object.class);
    }

}

MapperProxyFactory

  • org.apache.ibatis.binding.MapperProxyFactory ,Mapper Proxy 工厂类
public class MapperProxyFactory<T> {

    // Mapper 接口
    private final Class<T> mapperInterface;
    // 方法与MapperMethod的映射
    private final Map<Method, MapperMethodInvoker> methodCache = new ConcurrentHashMap<>();

    public MapperProxyFactory(Class<T> mapperInterface) {
        this.mapperInterface = mapperInterface;
    }

    // 获取 mapperInterface
    public Class<T> getMapperInterface() {
        return mapperInterface;
    }

    public Map<Method, MapperMethodInvoker> getMethodCache() {
        return methodCache;
    }

    @SuppressWarnings("unchecked")
    protected T newInstance(MapperProxy<T> mapperProxy) {
        return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);
    }

    // 根据SqlSession创建对象
    public T newInstance(SqlSession sqlSession) {
        final MapperProxy<T> mapperProxy = new MapperProxy<>(sqlSession, mapperInterface, methodCache);
        return newInstance(mapperProxy);
    }

}

MapperProxy

  • org.apache.ibatis.binding.MapperProxy ,实现 InvocationHandler、Serializable 接口,Mapper Proxy 。关键是 java.lang.reflect.InvocationHandler 接口
public class MapperProxy<T> implements InvocationHandler, Serializable {

    private static final long serialVersionUID = -4724728412955527868L;
    // 允许的类型
    private static final int ALLOWED_MODES = MethodHandles.Lookup.PRIVATE | MethodHandles.Lookup.PROTECTED
        | MethodHandles.Lookup.PACKAGE | MethodHandles.Lookup.PUBLIC;
    // 构造器方法
    private static final Constructor<Lookup> lookupConstructor;
    // Method对象
    private static final Method privateLookupInMethod;
    // SqlSession对象
    private final SqlSession sqlSession;
    // Mapper接口
    private final Class<T> mapperInterface;
    // 从 {@link MapperProxyFactory#methodCache} 传递过来
    private final Map<Method, MapperMethodInvoker> methodCache;

    public MapperProxy(SqlSession sqlSession, Class<T> mapperInterface, Map<Method, MapperMethodInvoker> methodCache) {
        this.sqlSession = sqlSession;
        this.mapperInterface = mapperInterface;
        this.methodCache = methodCache;
    }

    static {
        Method privateLookupIn;
        try {
            privateLookupIn = MethodHandles.class.getMethod("privateLookupIn", Class.class, MethodHandles.Lookup.class);
        } catch (NoSuchMethodException e) {
            privateLookupIn = null;
        }
        privateLookupInMethod = privateLookupIn;

        Constructor<Lookup> lookup = null;
        if (privateLookupInMethod == null) {
            // JDK 1.8
            try {
                lookup = MethodHandles.Lookup.class.getDeclaredConstructor(Class.class, int.class);
                lookup.setAccessible(true);
            } catch (NoSuchMethodException e) {
                throw new IllegalStateException(
                    "There is neither 'privateLookupIn(Class, Lookup)' nor 'Lookup(Class, int)' method in java.lang.invoke.MethodHandles.",
                    e);
            } catch (Exception e) {
                lookup = null;
            }
        }
        lookupConstructor = lookup;
    }

    // 调用方法
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        try {
            // 如果是Object定义的方法,直接调用
            if (Object.class.equals(method.getDeclaringClass())) {
                // 方法调用
                return method.invoke(this, args);
            } else {
                
                return cachedInvoker(method).invoke(proxy, method, args, sqlSession);
            }
        } catch (Throwable t) {
            throw ExceptionUtil.unwrapThrowable(t);
        }
    }

    private MapperMethodInvoker cachedInvoker(Method method) throws Throwable {
        try {
            // A workaround for https://bugs.openjdk.java.net/browse/JDK-8161372
            // It should be removed once the fix is backported to Java 8 or
            // MyBatis drops Java 8 support. See gh-1929
            MapperMethodInvoker invoker = methodCache.get(method);
            if (invoker != null) {
                return invoker;
            }

            return methodCache.computeIfAbsent(method, m -> {
                // 默认方法,在JDK1.8之后接口中有这个方法
                if (m.isDefault()) {
                    try {
                        if (privateLookupInMethod == null) {
                            return new DefaultMethodInvoker(getMethodHandleJava8(method));
                        } else {
                            return new DefaultMethodInvoker(getMethodHandleJava9(method));
                        }
                    } catch (IllegalAccessException | InstantiationException | InvocationTargetException
                             | NoSuchMethodException e) {
                        throw new RuntimeException(e);
                    }
                } else {
                    return new PlainMethodInvoker(new MapperMethod(mapperInterface, method, sqlSession.getConfiguration()));
                }
            });
        } catch (RuntimeException re) {
            Throwable cause = re.getCause();
            throw cause == null ? re : cause;
        }
    }

    private MethodHandle getMethodHandleJava9(Method method)
        throws NoSuchMethodException, IllegalAccessException, InvocationTargetException {
        final Class<?> declaringClass = method.getDeclaringClass();
        return ((Lookup) privateLookupInMethod.invoke(null, declaringClass, MethodHandles.lookup())).findSpecial(
            declaringClass, method.getName(), MethodType.methodType(method.getReturnType(), method.getParameterTypes()),
            declaringClass);
    }

    private MethodHandle getMethodHandleJava8(Method method)
        throws IllegalAccessException, InstantiationException, InvocationTargetException {
        final Class<?> declaringClass = method.getDeclaringClass();
        return lookupConstructor.newInstance(declaringClass, ALLOWED_MODES).unreflectSpecial(method, declaringClass);
    }

    interface MapperMethodInvoker {
        Object invoke(Object proxy, Method method, Object[] args, SqlSession sqlSession) throws Throwable;
    }

    private static class PlainMethodInvoker implements MapperMethodInvoker {
        private final MapperMethod mapperMethod;

        public PlainMethodInvoker(MapperMethod mapperMethod) {
            super();
            this.mapperMethod = mapperMethod;
        }

        @Override
        public Object invoke(Object proxy, Method method, Object[] args, SqlSession sqlSession) throws Throwable {
            return mapperMethod.execute(sqlSession, args);
        }
    }

    private static class DefaultMethodInvoker implements MapperMethodInvoker {
        private final MethodHandle methodHandle;

        public DefaultMethodInvoker(MethodHandle methodHandle) {
            super();
            this.methodHandle = methodHandle;
        }

        @Override
        public Object invoke(Object proxy, Method method, Object[] args, SqlSession sqlSession) throws Throwable {
            return methodHandle.bindTo(proxy).invokeWithArguments(args);
        }
    }
}

MapperMethod

  • org.apache.ibatis.binding.MapperMethod ,Mapper 方法。在 Mapper 接口中,每个定义的方法,对应一个 MapperMethod 对象
public class MapperMethod {

    // SqlCommand 对象
    private final SqlCommand command;
    // MethodSignature 对象
    private final MethodSignature method;

    // 构造方法
    public MapperMethod(Class<?> mapperInterface, Method method, Configuration config) {
        this.command = new SqlCommand(config, mapperInterface, method);
        this.method = new MethodSignature(config, mapperInterface, method);
    }

    public Object execute(SqlSession sqlSession, Object[] args) {
        Object result;
        // 判断类型
        switch (command.getType()) {
            case INSERT: {
                Object param = method.convertArgsToSqlCommandParam(args);
                result = rowCountResult(sqlSession.insert(command.getName(), param));
                break;
            }
            case UPDATE: {
                Object param = method.convertArgsToSqlCommandParam(args);
                result = rowCountResult(sqlSession.update(command.getName(), param));
                break;
            }
            case DELETE: {
                Object param = method.convertArgsToSqlCommandParam(args);
                result = rowCountResult(sqlSession.delete(command.getName(), param));
                break;
            }
            case SELECT:
                if (method.returnsVoid() && method.hasResultHandler()) {
                    executeWithResultHandler(sqlSession, args);
                    result = null;
                } else if (method.returnsMany()) {
                    result = executeForMany(sqlSession, args);
                } else if (method.returnsMap()) {
                    result = executeForMap(sqlSession, args);
                } else if (method.returnsCursor()) {
                    result = executeForCursor(sqlSession, args);
                } else {
                    Object param = method.convertArgsToSqlCommandParam(args);
                    result = sqlSession.selectOne(command.getName(), param);
                    if (method.returnsOptional()
                        && (result == null || !method.getReturnType().equals(result.getClass()))) {
                        result = Optional.ofNullable(result);
                    }
                }
                break;
            case FLUSH:
                result = sqlSession.flushStatements();
                break;
            default:
                throw new BindingException("Unknown execution method for: " + command.getName());
        }
        if (result == null && method.getReturnType().isPrimitive() && !method.returnsVoid()) {
            throw new BindingException("Mapper method '" + command.getName()
                                       + " attempted to return null from a method with a primitive return type (" + method.getReturnType() + ").");
        }
        return result;
    }

    // 
    private Object rowCountResult(int rowCount) {
        final Object result;
        if (method.returnsVoid()) {
            result = null;
        } else if (Integer.class.equals(method.getReturnType()) || Integer.TYPE.equals(method.getReturnType())) {
            result = rowCount;
        } else if (Long.class.equals(method.getReturnType()) || Long.TYPE.equals(method.getReturnType())) {
            result = (long) rowCount;
        } else if (Boolean.class.equals(method.getReturnType()) || Boolean.TYPE.equals(method.getReturnType())) {
            result = rowCount > 0;
        } else {
            throw new BindingException("Mapper method '" + command.getName() + "' has an unsupported return type: " + method.getReturnType());
        }
        return result;
    }

    private void executeWithResultHandler(SqlSession sqlSession, Object[] args) {
        MappedStatement ms = sqlSession.getConfiguration().getMappedStatement(command.getName());
        if (!StatementType.CALLABLE.equals(ms.getStatementType())
            && void.class.equals(ms.getResultMaps().get(0).getType())) {
            throw new BindingException("method " + command.getName()
                                       + " needs either a @ResultMap annotation, a @ResultType annotation,"
                                       + " or a resultType attribute in XML so a ResultHandler can be used as a parameter.");
        }
        Object param = method.convertArgsToSqlCommandParam(args);
        if (method.hasRowBounds()) {
            RowBounds rowBounds = method.extractRowBounds(args);
            sqlSession.select(command.getName(), param, rowBounds, method.extractResultHandler(args));
        } else {
            sqlSession.select(command.getName(), param, method.extractResultHandler(args));
        }
    }

    private <E> Object executeForMany(SqlSession sqlSession, Object[] args) {
        List<E> result;
        Object param = method.convertArgsToSqlCommandParam(args);
        if (method.hasRowBounds()) {
            RowBounds rowBounds = method.extractRowBounds(args);
            result = sqlSession.selectList(command.getName(), param, rowBounds);
        } else {
            result = sqlSession.selectList(command.getName(), param);
        }
        // issue #510 Collections & arrays support
        if (!method.getReturnType().isAssignableFrom(result.getClass())) {
            if (method.getReturnType().isArray()) {
                return convertToArray(result);
            } else {
                return convertToDeclaredCollection(sqlSession.getConfiguration(), result);
            }
        }
        return result;
    }

    private <T> Cursor<T> executeForCursor(SqlSession sqlSession, Object[] args) {
        Cursor<T> result;
        Object param = method.convertArgsToSqlCommandParam(args);
        if (method.hasRowBounds()) {
            RowBounds rowBounds = method.extractRowBounds(args);
            result = sqlSession.selectCursor(command.getName(), param, rowBounds);
        } else {
            result = sqlSession.selectCursor(command.getName(), param);
        }
        return result;
    }

    private <E> Object convertToDeclaredCollection(Configuration config, List<E> list) {
        Object collection = config.getObjectFactory().create(method.getReturnType());
        MetaObject metaObject = config.newMetaObject(collection);
        metaObject.addAll(list);
        return collection;
    }

    @SuppressWarnings("unchecked")
    private <E> Object convertToArray(List<E> list) {
        Class<?> arrayComponentType = method.getReturnType().getComponentType();
        Object array = Array.newInstance(arrayComponentType, list.size());
        if (arrayComponentType.isPrimitive()) {
            for (int i = 0; i < list.size(); i++) {
                Array.set(array, i, list.get(i));
            }
            return array;
        } else {
            return list.toArray((E[]) array);
        }
    }

    private <K, V> Map<K, V> executeForMap(SqlSession sqlSession, Object[] args) {
        Map<K, V> result;
        Object param = method.convertArgsToSqlCommandParam(args);
        if (method.hasRowBounds()) {
            RowBounds rowBounds = method.extractRowBounds(args);
            result = sqlSession.selectMap(command.getName(), param, method.getMapKey(), rowBounds);
        } else {
            result = sqlSession.selectMap(command.getName(), param, method.getMapKey());
        }
        return result;
    }

    public static class ParamMap<V> extends HashMap<String, V> {

        private static final long serialVersionUID = -2212268410512043556L;

        @Override
        public V get(Object key) {
            if (!super.containsKey(key)) {
                throw new BindingException("Parameter '" + key + "' not found. Available parameters are " + keySet());
            }
            return super.get(key);
        }

    }

    // 静态内部类
    public static class SqlCommand {

        private final String name;
        private final SqlCommandType type;

        // 构造方法
        public SqlCommand(Configuration configuration, Class<?> mapperInterface, Method method) {
            // 方法名称
            final String methodName = method.getName();
            final Class<?> declaringClass = method.getDeclaringClass();
            // MappedStatement 对象 ms
            MappedStatement ms = resolveMappedStatement(mapperInterface, methodName, declaringClass,
                                                        configuration);
            // 如果找不到 MappedStatement 
            if (ms == null) {
                // 判断是否有 Flush 注解
                if (method.getAnnotation(Flush.class) != null) {
                    name = null;
                    type = SqlCommandType.FLUSH;
                } else {
                    // 抛出绑定异常
                    throw new BindingException("Invalid bound statement (not found): "
                                               + mapperInterface.getName() + "." + methodName);
                }
                // 找到了
            } else {
                // 获得名字
                name = ms.getId();
                // 获得type
                type = ms.getSqlCommandType();
                // 如果是未知类型,就抛出绑定异常
                if (type == SqlCommandType.UNKNOWN) {
                    throw new BindingException("Unknown execution method for: " + name);
                }
            }
        }

        public String getName() {
            return name;
        }

        public SqlCommandType getType() {
            return type;
        }

        // 获得方法的 MappedStatement 对象
        private MappedStatement resolveMappedStatement(Class<?> mapperInterface, String methodName,
                                                       Class<?> declaringClass, Configuration configuration) {
            // 获得编号
            String statementId = mapperInterface.getName() + "." + methodName;
            // 如果有,获得 MappedStatement对象,并返回
            if (configuration.hasStatement(statementId)) {
                return configuration.getMappedStatement(statementId);
                // 如果没有,并且当前方法就是 declaringClass 声明的,则说明找不到
            } else if (mapperInterface.equals(declaringClass)) {
                // 返回 null
                return null;
            }
            // 遍历父接口,继续获得 MappedStatement 对象
            for (Class<?> superInterface : mapperInterface.getInterfaces()) {
                if (declaringClass.isAssignableFrom(superInterface)) {
                    MappedStatement ms = resolveMappedStatement(superInterface, methodName,
                                                                declaringClass, configuration);
                    if (ms != null) {
                        return ms;
                    }
                }
            }
            // 真的找不到
            return null;
        }
    }

    // MethodSignature 方法签名
    public static class MethodSignature {

        // 返回类型是否为集合
        private final boolean returnsMany;
        // 返回结果是否为map
        private final boolean returnsMap;
        // 返回值类型为 void
        private final boolean returnsVoid;
        // 返回值类型为 {@link org.apache.ibatis.cursor.Cursor}
        private final boolean returnsCursor;
        // 返回值类型为 {@link java.util.Optional}
        private final boolean returnsOptional;
        // 返回类型
        private final Class<?> returnType;
        // 返回方法上的 {@link MapKey#value()} ,前提是返回类型为 Map
        private final String mapKey;
        // 获得 {@link ResultHandler} 在方法参数中的位置。
        // 如果为 null ,说明不存在这个类型
        private final Integer resultHandlerIndex;
        
        // 获得 {@link RowBounds} 在方法参数中的位置
        // 如果为 null ,说明不存在这个类型
        private final Integer rowBoundsIndex;
        // ParamNameResolver 对象
        private final ParamNameResolver paramNameResolver;

        // 构造函数,初始化参数
        public MethodSignature(Configuration configuration, Class<?> mapperInterface, Method method) {
            Type resolvedReturnType = TypeParameterResolver.resolveReturnType(method, mapperInterface);
            if (resolvedReturnType instanceof Class<?>) {
                this.returnType = (Class<?>) resolvedReturnType;
            } else if (resolvedReturnType instanceof ParameterizedType) {
                this.returnType = (Class<?>) ((ParameterizedType) resolvedReturnType).getRawType();
            } else {
                this.returnType = method.getReturnType();
            }
            this.returnsVoid = void.class.equals(this.returnType);
            this.returnsMany = configuration.getObjectFactory().isCollection(this.returnType) || this.returnType.isArray();
            this.returnsCursor = Cursor.class.equals(this.returnType);
            this.returnsOptional = Optional.class.equals(this.returnType);
            this.mapKey = getMapKey(method);
            this.returnsMap = this.mapKey != null;
            this.rowBoundsIndex = getUniqueParamIndex(method, RowBounds.class);
            this.resultHandlerIndex = getUniqueParamIndex(method, ResultHandler.class);
            this.paramNameResolver = new ParamNameResolver(configuration, method);
        }

        public Object convertArgsToSqlCommandParam(Object[] args) {
            return paramNameResolver.getNamedParams(args);
        }

        public boolean hasRowBounds() {
            return rowBoundsIndex != null;
        }

        public RowBounds extractRowBounds(Object[] args) {
            return hasRowBounds() ? (RowBounds) args[rowBoundsIndex] : null;
        }

        public boolean hasResultHandler() {
            return resultHandlerIndex != null;
        }

        public ResultHandler extractResultHandler(Object[] args) {
            return hasResultHandler() ? (ResultHandler) args[resultHandlerIndex] : null;
        }

        public Class<?> getReturnType() {
            return returnType;
        }

        public boolean returnsMany() {
            return returnsMany;
        }

        public boolean returnsMap() {
            return returnsMap;
        }

        public boolean returnsVoid() {
            return returnsVoid;
        }

        public boolean returnsCursor() {
            return returnsCursor;
        }

        /**
     * return whether return type is {@code java.util.Optional}.
     *
     * @return return {@code true}, if return type is {@code java.util.Optional}
     * @since 3.5.0
     */
        public boolean returnsOptional() {
            return returnsOptional;
        }

        // 返回参数类型在方法参数中的文职
        private Integer getUniqueParamIndex(Method method, Class<?> paramType) {
            Integer index = null;
            // 遍历方法参数
            final Class<?>[] argTypes = method.getParameterTypes();
            for (int i = 0; i < argTypes.length; i++) {
                // 类型符合
                if (paramType.isAssignableFrom(argTypes[i])) {
                    // 第一次出现的位置
                    if (index == null) {
                        index = i;
                        // 如果类型重复,抛出绑定异常
                    } else {
                        throw new BindingException(method.getName() + " cannot have multiple " + paramType.getSimpleName() + " parameters");
                    }
                }
            }
            return index;
        }

        public String getMapKey() {
            return mapKey;
        }

        // 获得 mapKey
        private String getMapKey(Method method) {
            String mapKey = null;
            // 返回值类型为 Map
            if (Map.class.isAssignableFrom(method.getReturnType())) {
                // 使用 MapKey 注解
                final MapKey mapKeyAnnotation = method.getAnnotation(MapKey.class);
                // 获得 MapKey 的键
                if (mapKeyAnnotation != null) {
                    mapKey = mapKeyAnnotation.value();
                }
            }
            return mapKey;
        }
    }

}
1
https://gitee.com/elonchung/Java-Review.git
git@gitee.com:elonchung/Java-Review.git
elonchung
Java-Review
Java-Review
master

搜索帮助