1 Star 1 Fork 0

DarkFire/JDBCToMybatis

加入 Gitee
与超过 1200万 开发者一起发现、参与优秀开源项目,私有仓库也完全免费 :)
免费加入
克隆/下载
贡献代码
同步代码
取消
提示: 由于 Git 不支持空文件夾,创建文件夹后会生成空的 .keep 文件
Loading...
README
MulanPSL-2.0

手撕JDBC框架

JDBC封装Mybatis

Select 原生SQL语句

封装原生查询,返回ResultSet

    public  ResultSet select(String sql, Object... values){
        try {
            Class.forName(driverPath);
            Connection connection = DriverManager.getConnection(sqlUrl,user,password);
            PreparedStatement pst = connection.prepareStatement(sql);
            for (int i =0;i<values.length;i++){
                //jdbc会自动转换类型
                pst.setObject(i+1,values[i]);
            }
            ResultSet set = pst.executeQuery();
            connection.close();
            pst.close();
            return set;
        }catch (Exception e){
            e.printStackTrace();
            return null;
        }finally {
            System.out.println("执行完毕");
        }
    }

在调用该方法时,只需传递 select(sql,参数);

("select * from `user` where id = ?",1)

返回一个范型对象,将ResultSet在方法内封装成对象:

public <T> T selectOne(String sql,RowMapper rm,Object... values){
        T obj = null; //范型对象
        try {
            Class.forName(driverPath);
            Connection connection = DriverManager.getConnection(sqlUrl,user,password);
            PreparedStatement pst = connection.prepareStatement(sql);
            for (int i =0;i<values.length;i++){
                //jdbc会自动转换类型
                pst.setObject(i+1,values[i]);
            }
            ResultSet set = pst.executeQuery();
            while (set.next()){
                //对执行结果进行对象封装
                obj =  rm.rowMapper(set);
            }
            set.close();
            connection.close();
            pst.close();
            return obj;
        }catch (Exception e){
            e.printStackTrace();
            return null;
        }finally {
            System.out.println("执行完毕");
        }
    }

测试类

 @Test
    public void TestSelectOne()  {
        RowMapper rowMapper = new RowMapper() {
          //RowMapper是一个接口,实例
            @Override
            public <T> T rowMapper(ResultSet set) throws SQLException{
              //策略模式
                int id = set.getInt(1); //获取ID
                String name =  set.getString(2);//获取用户名
                int age = set.getInt(3);
                User user = new User(id,name,age);
                return (T) user;
            }
        };
        User user  =  SqlSession.getInstance().selectOne("select * from `user` where id = ?",rowMapper,0);
        System.out.println(user);
    }

同理,如果查询结果包含多条,那么可以创建 List 接收范型集合。

Select 简化SQL调用

​ 在进行SQL调用时,我们的代码通常会有很多重复的部分。那么我们该怎么简化SQL语句的执行呢。上面我们通过创建使用原生jdbc获取结果集后,通过RowMapper中的接口来将该结果集转化为实现RowMapper.rowMapper()方法中返回的对象。虽然简化了SQL的调用,但是仍然需要自己实现rowMapper匿名类。

​ 于是我们可以在SqlSession,也就是在Dao层调用sql执行方法的那一层里可以实现对查询结果的封装:

ResultSet set = pst.executeQuery();
            while (set.next()){
                //对执行结果进行对象封装
                obj =  handler.mapperClass(set,cls);
            }

我们创建了Handler结果集处理层,将实现对SQL执行结果的自动化封装成对象/Map集合/普通类型。

1.我们可以通过Map集合的方式,获取到ResultSet中的列名以及对应的数据,将该数据放入K-V键值对中

2.我们可以传入Class,通过反射的形式获取到ResultSet中的列名对应的属性字段,然后再对其进行赋值即可。也可以通过反射的形式获取set方法来对属性值进行赋值。

//通过反射的方法来获取cls对象的属性列表,在通过set集合的列名来给类设置属性值。
    private Object toObject(ResultSet set,Class cls) throws SQLException {
        ResultSetMetaData rsm = set.getMetaData();
        Object obj = null;
        try {
            obj = cls.newInstance();
            for(int i = 1;i<=rsm.getColumnCount();i++){
//                ············································
//                方法一  反射直接修改字段
//                //通过反射获取类的字段(属性)信息
//                Field field = obj.getClass().getDeclaredField(rsm.getColumnName(i));
//                //设置私有属性是可修改的
//                field.setAccessible(true);
//                //直接设置属性值
//                field.set(obj,set.getObject(rsm.getColumnName(i)));
//                ·············································
                //通过反射获取类中的set方法
                String methodName = "set"+rsm.getColumnName(i).substring(0,1).toUpperCase()+rsm.getColumnName(i).substring(1);
                System.out.println(methodName);
                Field field = cls.getDeclaredField(rsm.getColumnName(i));//根据属性名称获取单个属性
                //通过属性的类型来调用类中的Setter方法,如setId(Integer.class);
                if (field.getGenericType().toString().equals("int")||field.getGenericType().toString().equals("class java.lang.Integer")){
                    Method method = obj.getClass().getDeclaredMethod(methodName,Integer.class);
                    method.invoke(obj,set.getInt(rsm.getColumnName(i)));
                }else if(field.getGenericType().toString().equals("string")||field.getGenericType().toString().equals("class java.lang.String")){
                    Method method = obj.getClass().getDeclaredMethod(methodName,String.class);
                    method.invoke(obj,set.getString(rsm.getColumnName(i)));
                }

            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        return obj;
    }

Insert 简化SQL调用

​ 一条插入SQL语句会包含需要传入的参数。一般使用PrepareStatement来对该SQL语句预处理后在对问号部位进行赋值。那么就需要自己传入参数而且参数必须和SQL语句问号位置相对应。

//insert user values(?,?,?)
PreparedStatement pst = getPst(sql);
pst.setInt(1,1001);
pst.setString(2,"刘德华");
pst.setInt(3,19);

​ 如果SQL语句中问号数量太多,如果我们手动设置这些参数非常麻烦。那么我们就需要简化SQL的操作,看怎么才能让程序自动给处理在哪里插入参数。

​ 思路:我们可以构造一个SQL语句如:select * from user where id = #{id},然后再传入一个User对象,该User的对象的id属性存在且不为空,那么可以通过传递sql语句和一个user对象即可完成语句的执行。我们可以通过StringBuilder对传入的SQL进行解析,解析完毕之后再使用反射获取到对象对应属性的值,随后使用原生jdbc对其进行预处理赋值执行即可。

public List<Object> parseSQL(String sql){
        List<Object> params = new ArrayList<>();
        StringBuilder sb = new StringBuilder(sql);
        StringBuilder newSQL = new StringBuilder();
        while (true){
            int left = sb.indexOf("#{");
            int right = sb.indexOf("}");
            if(left!=-1 && right!=-1 && left<right){
                //取出在#{左侧的所有
                newSQL.append(sb.substring(0,left));
                //获取参数名 #{name}
                String paramName = sb.substring(left+2,right);
                //将获取的参数名放入List中
                params.add(paramName);
                //用问号填充参数
                newSQL.append("?");
                //从后面的再找
                sb = new StringBuilder(sb.substring(right+1));
            }else{
                //说明本次循环已经找到了,将剩余的字符填充
                newSQL.append(sb.substring(right+1));
                break;
            }
        }
        params.add(newSQL.toString());
        return params;
    }

解析完毕SQL之后,那就可以反射传入的对象预处理了。

public boolean insert(String sql,Object obj) throws Exception {
        //从SQL语句中获得需要插入的参数。参数名和obj的属性相对应
        List<Object> list = handler.parseSQL(sql);
        //获取处理后的SQL语句
        PreparedStatement pst = getPst((String) list.get(list.size()-1));
        //反射对象获取对象的相应属性
        for (int i = 0;i<list.size()-1;i++){
            //通过反射获取相应字段
            Field field = obj.getClass().getDeclaredField((String) list.get(i));
            //设置字段可访问
            field.setAccessible(true);
            //获取字段对应的值
            Object value = field.get(obj);
            //通过jdbc原生对预处理赋值
            pst.setObject(i+1,value);
        }
        try {
            pst.execute();
            return true;
        }catch (Exception e){
            e.printStackTrace();
            return false;
        }
    }

测试代码

  @Test
    public void testInsert() throws Exception {
        String sql = "insert user values (#{id},#{name},#{age})";
        User user = new User(1010,"李三",29);
        if (SqlSession.getInstance().insert(sql,user)){
            System.out.println("insert succeed!");
        }else {
            System.out.println("insert failed!");
        }
    }

但是上面的代码有个Bug,就是只能传入对象而不能传入其他基本类型。那么我们可以对obj的类型进行判断再预处理:

//给预处理的SQL语句自动赋值
    public void setParams(PreparedStatement pst,Object obj,List<Object> params) throws Exception {
        Class<?> cls = obj.getClass();
        if(cls == Integer.class || cls == String.class || cls == Float.class || cls == Double.class){
            //说明传入的是一个基本参数,直接赋值
            pst.setObject(1, obj);
        }else if(obj instanceof Map){
            //说明传入的是Map对象
            Map map = (Map) obj;
            for(int i = 0;i<params.size()-1;i++){
                //通过参数名来查询Map集合中的属性
                Object value = map.get(params.get(i));
                pst.setObject(i+1,value);
            }
        }else {
            //传入的是其他对象
            //反射对象获取对象的相应属性
            for (int i = 0;i<params.size()-1;i++){
                //通过反射获取相应字段
                Field field = obj.getClass().getDeclaredField((String) params.get(i));
                //设置字段可访问
                field.setAccessible(true);
                //获取字段对应的值
                Object value = field.get(obj);
                //通过jdbc原生对预处理赋值
                pst.setObject(i+1,value);
            }

        }
    }

通过注解执行SQL语句

注解写法

package src.ORM.Annotation;


import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.METHOD)
//Retention表示获取注解的段,RunTime表示在java源码、编译、运行都可以调用
@Retention(RetentionPolicy.RUNTIME)
public @interface Delete {
    String value();
}

注解默认的方法为 value(); 访问该访问即可获取到注解中value对应的值

调用该方法的语句为 @Delete(value = "delete from user where id = 12")

注解目的

​ 通过注解 我们可以不用重写Dao接口的具体实现方法,通过注解即可实现简单SQL语句的具体实现,如Dao接口的方法为:

void insert(User user); 那么我们可以通过对其进行注解:@Insert("insert users value(#{id},#{name},#{age}) ")来实现数据的插入。

实现原理

​ 通过代理Proxy类,代理Dao类中的方法来获取被执行的方法返回值和注解类型以及注解值。在通过上面已经封装好的CRUD方法来执行SQL语句。

​ a.获取需要反射的类,重写invoke方法。

​ b.通过反射中的参数获得注解名

​ c.根据注解名来判断执行哪种SQL语句

​ d.在通过反射注解类获得注解的注释值,即注解中的SQL语句

​ e.如果执行的SQL语句为select查询语句,带有返回值的。那么需要区分返回的是集合还是普通对象

​ e.1 通过反射被代理的方法获取该方法的返回值,如果该值为List集合,则需要获取List中的范型类型,再使用封装好的方法执行即可。

​ e.2 如果为基本类型、自定义类型,直接调用封装的方法即可。

Proxy代理类的方法类似于通过逆向Hook得到类的方法并进行调用。

 public <T> T getFuncProxy(Class<?> cls){
        //获得类的加载
        ClassLoader classLoader = cls.getClassLoader();
        Class<?>[] interfaces = new Class[]{cls};
        InvocationHandler invocationHandler = new InvocationHandler() {
            @Override
            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                //获取返回类型的类
              	//method -> 代理类被调用的方法
              	//args -> 代理类被调用方法的参数
                Class<?> returnCLass = method.getReturnType();
                System.out.println("returnType->"+returnCLass.getTypeName());
                System.out.println("MethodName->"+method.getName());
                if(String.class.equals(returnCLass)){
                    return "返回结果已经被代理了";
                }else{
                    return null;
                }
            }
        };
        //创建一个代理
        Object obj =  Proxy.newProxyInstance(classLoader,interfaces,invocationHandler);
        return (T)obj;
    }

测试方法

package Proxy;

public interface MyFunction {
    String  read();
    void write();
}


@Test
    public void test(){
        MyFunction mf = getFuncProxy(MyFunction.class);
        System.out.println(mf.read());
        mf.write();
    }

实现代码

public <T> T getMapper(Class<?> cls){
        //此处提供代理Dao对象的功能,实现Dao层接口无需实现即可执行SQL语句的功能
        ClassLoader classLoader = cls.getClassLoader();
        Class[] interfaces = new Class[]{cls};
        InvocationHandler invocationHandler = new InvocationHandler() {
            /***
             * proxy -> 代理对象
             * method ->代理对象的方法
             * args -> 代理对象的方法的参数
             */
            @Override
            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                Annotation an = method.getAnnotations()[0]; //通过被代理的方法获取该方法的注解名
                Class<? extends Annotation> aClass = an.annotationType();//获取注解类型的类
                Method method1 = aClass.getMethod("value"); //通过反射该类调用value方法获取注解值
                String sql = (String)method1.invoke(an);
                if (Insert.class.equals(aClass)) {
                    SqlSession. getInstance().insert(sql,args[0]);
                } else if (Select.class.equals(aClass)) {
                    //a.第一步要判断是不是list类型
                    System.out.println("log1->"+method.getReturnType());
                    if(method.getReturnType() == List.class){
                        //查询方法select2的参数为基本类型+Map+自定义类型,
                        //所以需要获取被代理方法的返回类型才可以进行实体的映射创建
                        //如果返回类型为List,那么就需要获取List中的范型类用于MapperClass的实体映射。
                        Type wholeReturnType = method.getGenericReturnType(); //获取返回类型的全部->List<User>
                        ParameterizedType realReturnTypes = (ParameterizedType)(wholeReturnType);
                        Type realType = realReturnTypes.getActualTypeArguments()[0];
                        System.out.println("getReturnType->"+method.getGenericReturnType()
                                +",getGenericReturnType->"+wholeReturnType+",getActualTypeArguments->"+realType);
                        if(args == null){
                            return SqlSession.getInstance().select2(sql,(Class<?>) realType);
                        }else{
                            return SqlSession.getInstance().select2(sql,(Class<?>) realType,args[0]);
                        }
                    }else {
                        //此时只需传入方法的返回值类型即可
                        if(args == null){
                            return SqlSession.getInstance().select2(sql,(Class<?>) method.getReturnType()).get(0);
                        }else{
                            return SqlSession.getInstance().select2(sql,(Class<?>) method.getReturnType(),args[0]).get(0);
                        }
                    }
                } else if (Delete.class.equals(aClass)) {
                    SqlSession.getInstance().delete(sql,args[0]);
                } else if (Update.class.equals(aClass)) {
                    SqlSession.getInstance().update(sql,args[0]);
                }else {
                    System.err.println("未被注解定义的类型!");
                }
                return null;
            }
        };
        //开始创建类的代理
        Object obj = Proxy.newProxyInstance(classLoader,interfaces,invocationHandler);
        return (T)obj;
    }

加入Druid数据库连接线程池

通过静态代码块的方式,让该代码在类被加载时就执行来创建一个Properties对象

{
        properties = new Properties();
        try {
            InputStream in = new BufferedInputStream(new FileInputStream("src/main/druid.properties"));
            properties.load(in);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

并由Druid自动创建线程池并获得

 public Connection getConnection() throws Exception {
        //通过druid工厂创建数据源
        dataSource = DruidDataSourceFactory.createDataSource(properties);
        return dataSource.getConnection();
    }	
木兰宽松许可证, 第2版 木兰宽松许可证, 第2版 2020年1月 http://license.coscl.org.cn/MulanPSL2 您对“软件”的复制、使用、修改及分发受木兰宽松许可证,第2版(“本许可证”)的如下条款的约束: 0. 定义 “软件”是指由“贡献”构成的许可在“本许可证”下的程序和相关文档的集合。 “贡献”是指由任一“贡献者”许可在“本许可证”下的受版权法保护的作品。 “贡献者”是指将受版权法保护的作品许可在“本许可证”下的自然人或“法人实体”。 “法人实体”是指提交贡献的机构及其“关联实体”。 “关联实体”是指,对“本许可证”下的行为方而言,控制、受控制或与其共同受控制的机构,此处的控制是指有受控方或共同受控方至少50%直接或间接的投票权、资金或其他有价证券。 1. 授予版权许可 每个“贡献者”根据“本许可证”授予您永久性的、全球性的、免费的、非独占的、不可撤销的版权许可,您可以复制、使用、修改、分发其“贡献”,不论修改与否。 2. 授予专利许可 每个“贡献者”根据“本许可证”授予您永久性的、全球性的、免费的、非独占的、不可撤销的(根据本条规定撤销除外)专利许可,供您制造、委托制造、使用、许诺销售、销售、进口其“贡献”或以其他方式转移其“贡献”。前述专利许可仅限于“贡献者”现在或将来拥有或控制的其“贡献”本身或其“贡献”与许可“贡献”时的“软件”结合而将必然会侵犯的专利权利要求,不包括对“贡献”的修改或包含“贡献”的其他结合。如果您或您的“关联实体”直接或间接地,就“软件”或其中的“贡献”对任何人发起专利侵权诉讼(包括反诉或交叉诉讼)或其他专利维权行动,指控其侵犯专利权,则“本许可证”授予您对“软件”的专利许可自您提起诉讼或发起维权行动之日终止。 3. 无商标许可 “本许可证”不提供对“贡献者”的商品名称、商标、服务标志或产品名称的商标许可,但您为满足第4条规定的声明义务而必须使用除外。 4. 分发限制 您可以在任何媒介中将“软件”以源程序形式或可执行形式重新分发,不论修改与否,但您必须向接收者提供“本许可证”的副本,并保留“软件”中的版权、商标、专利及免责声明。 5. 免责声明与责任限制 “软件”及其中的“贡献”在提供时不带任何明示或默示的担保。在任何情况下,“贡献者”或版权所有者不对任何人因使用“软件”或其中的“贡献”而引发的任何直接或间接损失承担责任,不论因何种原因导致或者基于何种法律理论,即使其曾被建议有此种损失的可能性。 6. 语言 “本许可证”以中英文双语表述,中英文版本具有同等法律效力。如果中英文版本存在任何冲突不一致,以中文版为准。 条款结束 如何将木兰宽松许可证,第2版,应用到您的软件 如果您希望将木兰宽松许可证,第2版,应用到您的新软件,为了方便接收者查阅,建议您完成如下三步: 1, 请您补充如下声明中的空白,包括软件名、软件的首次发表年份以及您作为版权人的名字; 2, 请您在软件包的一级目录下创建以“LICENSE”为名的文件,将整个许可证文本放入该文件中; 3, 请将如下声明文本放入每个源文件的头部注释中。 Copyright (c) [Year] [name of copyright holder] [Software Name] is licensed under Mulan PSL v2. You can use this software according to the terms and conditions of the Mulan PSL v2. You may obtain a copy of Mulan PSL v2 at: http://license.coscl.org.cn/MulanPSL2 THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. See the Mulan PSL v2 for more details. Mulan Permissive Software License,Version 2 Mulan Permissive Software License,Version 2 (Mulan PSL v2) January 2020 http://license.coscl.org.cn/MulanPSL2 Your reproduction, use, modification and distribution of the Software shall be subject to Mulan PSL v2 (this License) with the following terms and conditions: 0. Definition Software means the program and related documents which are licensed under this License and comprise all Contribution(s). Contribution means the copyrightable work licensed by a particular Contributor under this License. Contributor means the Individual or Legal Entity who licenses its copyrightable work under this License. Legal Entity means the entity making a Contribution and all its Affiliates. Affiliates means entities that control, are controlled by, or are under common control with the acting entity under this License, ‘control’ means direct or indirect ownership of at least fifty percent (50%) of the voting power, capital or other securities of controlled or commonly controlled entity. 1. Grant of Copyright License Subject to the terms and conditions of this License, each Contributor hereby grants to you a perpetual, worldwide, royalty-free, non-exclusive, irrevocable copyright license to reproduce, use, modify, or distribute its Contribution, with modification or not. 2. Grant of Patent License Subject to the terms and conditions of this License, each Contributor hereby grants to you a perpetual, worldwide, royalty-free, non-exclusive, irrevocable (except for revocation under this Section) patent license to make, have made, use, offer for sale, sell, import or otherwise transfer its Contribution, where such patent license is only limited to the patent claims owned or controlled by such Contributor now or in future which will be necessarily infringed by its Contribution alone, or by combination of the Contribution with the Software to which the Contribution was contributed. The patent license shall not apply to any modification of the Contribution, and any other combination which includes the Contribution. If you or your Affiliates directly or indirectly institute patent litigation (including a cross claim or counterclaim in a litigation) or other patent enforcement activities against any individual or entity by alleging that the Software or any Contribution in it infringes patents, then any patent license granted to you under this License for the Software shall terminate as of the date such litigation or activity is filed or taken. 3. No Trademark License No trademark license is granted to use the trade names, trademarks, service marks, or product names of Contributor, except as required to fulfill notice requirements in Section 4. 4. Distribution Restriction You may distribute the Software in any medium with or without modification, whether in source or executable forms, provided that you provide recipients with a copy of this License and retain copyright, patent, trademark and disclaimer statements in the Software. 5. Disclaimer of Warranty and Limitation of Liability THE SOFTWARE AND CONTRIBUTION IN IT ARE PROVIDED WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR IMPLIED. IN NO EVENT SHALL ANY CONTRIBUTOR OR COPYRIGHT HOLDER BE LIABLE TO YOU FOR ANY DAMAGES, INCLUDING, BUT NOT LIMITED TO ANY DIRECT, OR INDIRECT, SPECIAL OR CONSEQUENTIAL DAMAGES ARISING FROM YOUR USE OR INABILITY TO USE THE SOFTWARE OR THE CONTRIBUTION IN IT, NO MATTER HOW IT’S CAUSED OR BASED ON WHICH LEGAL THEORY, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. 6. Language THIS LICENSE IS WRITTEN IN BOTH CHINESE AND ENGLISH, AND THE CHINESE VERSION AND ENGLISH VERSION SHALL HAVE THE SAME LEGAL EFFECT. IN THE CASE OF DIVERGENCE BETWEEN THE CHINESE AND ENGLISH VERSIONS, THE CHINESE VERSION SHALL PREVAIL. END OF THE TERMS AND CONDITIONS How to Apply the Mulan Permissive Software License,Version 2 (Mulan PSL v2) to Your Software To apply the Mulan PSL v2 to your work, for easy identification by recipients, you are suggested to complete following three steps: i Fill in the blanks in following statement, including insert your software name, the year of the first publication of your software, and your name identified as the copyright owner; ii Create a file named “LICENSE” which contains the whole context of this License in the first directory of your software package; iii Attach the statement to the appropriate annotated syntax at the beginning of each source file. Copyright (c) [Year] [name of copyright holder] [Software Name] is licensed under Mulan PSL v2. You can use this software according to the terms and conditions of the Mulan PSL v2. You may obtain a copy of Mulan PSL v2 at: http://license.coscl.org.cn/MulanPSL2 THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. See the Mulan PSL v2 for more details.

简介

a tool about devloping mybatis framework 展开 收起
README
MulanPSL-2.0
取消

发行版 (1)

全部

贡献者

全部

语言

近期动态

不能加载更多了
马建仓 AI 助手
尝试更多
代码解读
代码找茬
代码优化
Java
1
https://gitee.com/darkfiregit/JDBCToMybatis.git
git@gitee.com:darkfiregit/JDBCToMybatis.git
darkfiregit
JDBCToMybatis
JDBCToMybatis
master

搜索帮助