学习来源 通过自动回复机器人学Mybatis—加强版
使用使用接口来规范MyBatis配置文件与调用,减少出错概率
对dao层的改进 MyBatis的配置文件要求命名空间必须唯一,同时命名空间中的所有sql标签的id也必须唯一,但是项目一大并不能保证这点,这里就需要引入接口和动态代理机制,来规范代码书写
表结构使用前一篇文章的 content 和 tag,使用面向接口的思想对前一篇笔记中的代码进行改造
首先,建立一个 interface
,取名为 IContentDao
,专门负责对 content
表的操作,记录 package 名, interface 名以及接口内包含的方法ming
1 2 3 4 5 6 7 8 9 10 11 package com.example.microapp.dao;import com.example.microapp.bean.ContentAss;import java.util.List;public interface IContentDao { List<ContentAss> queryContentList (List<Integer> contentId) throws Exception ; }
把之前的MyBatis配置文件 Content.xml
中的命名空间namespace进行修改,改为前面的package名,把接口方法在实现是要调用的sql标签的id改为接口方法名,如下
1 2 3 4 5 6 7 8 9 10 11 <?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace ="com.example.microapp.dao.IContentDao" > .... <select id ="queryContentList" parameterType ="java.util.List" resultMap ="ContentResultAss" > .... </select > .... </mapper >
再重写 dao 中调用MyBatis的方法,写成如下形式
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 public class ContentDaoImpl { public List<ContentAss> queryContentList (List<Integer> contentId) throws Exception { SqlSession sqlSession = ListMessageDb.getInstance().getSqlSession(); IContentDao contentDao = sqlSession.getMapper(IContentDao.class); List<ContentAss> result = contentDao.queryContentList(contentId); return result; } }
和之前的dao对比一下
1 2 3 4 5 6 7 public List<ContentAss> queryContentList (List<Integer> contentId) throws Exception { SqlSession sqlSession = ListMessageDb.getInstance().getSqlSession(); List<ContentAss> result = (ArrayList)sqlSession.selectList("Content.QueryContentList" , contentId); return result; }
使用之前的测试文件进行测试,效果相同,但是这样写代码就变得十分规范,减少出错率,因为interface接口规范可以在项目设计的时候就订好,MyBatis配置文件和Dao层的实现都可以参照这个规范,所以不太可能出现id名或namespace重复的情况,程序员在实现dao时手贱敲错名称也不会出现
实现原理:动态代理 学习动态代理技术要对反射技术有一定的了解
代码举例 这里编写一个demo先对动态代理有一个概念,对Interface接口的方法的代理
和前面的一样,先编写一个interface文件,规定一些方法
1 2 3 4 5 6 package com.example.demo1;public interface IFunc { void func1 () ; void func2 () ; }
编写一个类,负责实现需要代理的方法,这个类需要实现 InvocationHandler
接口
1 2 3 4 5 6 7 8 9 package com.example.demo1;public class MProxyHandlar implements InvocationHandler { @Override public Object invoke (Object proxy, Method method, Object[] args) throws Throwable { System.out.println("代理启动,被代理的类为 " + method.getDeclaringClass().getName() +" ,代理方法 " + method.getName() +" 执行" ); return null ; } }
编写一个类,负责注册需要代理的接口
1 2 3 4 5 6 7 8 9 10 package com.example.demo1;public class ProxyCreater { public static <T> T getProxy (Class<T> type) { return (T) Proxy.newProxyInstance(type.getClassLoader(), new Class[]{type}, new MProxyHandlar()); } }
最后编写测试类
1 2 3 4 5 6 7 8 package com.example.demo1;spublic class Main { public static void main (String[] args) { IFunc funcs= ProxyCreater.getProxy(IFunc.class); funcs.func1(); funcs.func2(); } }
执行此类,打印如下
1 2 代理启动,被代理的类为 com.example.demo1.IFunc ,代理方法 func1 执行 代理启动,被代理的类为 com.example.demo1.IFunc ,代理方法 func2 执行
过程分析 关键点就是 Proxy.newProxyInstance
方法,真是它返回了实现了代理功能,同时实现了IFunc
接口的对象实例,尝试阅读了源码,其中的关键点是如下的函数
1 2 3 4 5 Class<?> cl = getProxyClass0(loader, intfs);
后面就是对这个类进行实例化,并返回实例
1 2 3 final Constructor<?> cons = cl.getConstructor(constructorParams);return cons.newInstance(new Object[]{h});
其中 constructorParams
在之前有定义
1 2 3 private static final Class<?>[] constructorParams = { InvocationHandler.class };
而那个 h
就是传入的第三个参数,也就是那个 ProxyHandler
的实例
cl
正是那个对象实例的类,关键公的关键。本来想尝试去阅读 getProxyClass0
源码的,无奈功力不够,完全看不懂,但是又非常想知道这个类是个啥,幸好,可以配置这么一行JVM参数-Dsun.misc.ProxyGenerator.saveGeneratedFiles=true
,可以查看自动生成的代理类
再次运行程序,就会发现在项目根目录下类似于这样的class
1 2 3 4 5 . ├── com │ └── sun │ └── proxy │ └── $Proxy0.class
点进去,通过IDE的解码工具,结果蛮惊喜的,这个类大体如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 public final class $Proxy0 extends Proxy implements IFunc { private static Method m1; private static Method m3; private static Method m4; private static Method m2; private static Method m0; public $Proxy0(InvocationHandler var1) throws { super (var1); } public final void func1 () throws { super .h.invoke(this , m3, (Object[])null ); } public final void func2 () throws { super .h.invoke(this , m4, (Object[])null ); } public final String toString () throws { return (String)super .h.invoke(this , m2, (Object[])null ); } public final int hashCode () throws { return (Integer)super .h.invoke(this , m0, (Object[])null ); } public final boolean equals (Object var1) throws { return (Boolean)super .h.invoke(this , m1, new Object[]{var1}); } static { try { m1 = Class.forName("java.lang.Object" ).getMethod("equals" , Class.forName("java.lang.Object" )); m3 = Class.forName("com.example.demo1.IFunc" ).getMethod("func1" ); m4 = Class.forName("com.example.demo1.IFunc" ).getMethod("func2" ); m2 = Class.forName("java.lang.Object" ).getMethod("toString" ); m0 = Class.forName("java.lang.Object" ).getMethod("hashCode" ); } catch (NoSuchMethodException var2) { throw new NoSuchMethodError(var2.getMessage()); } catch (ClassNotFoundException var3) { throw new NoClassDefFoundError(var3.getMessage()); } } }
这个类继承了 Proxy
类,同时实现了我们自定义的接口 IFunc
,实现了自定义接口的方法一个一些可以Override的方法,构造函数传入的参数就是之前自定义的实现了接口InvocationHandler
的 MProxyHandlar 的实例,调用了super方法,这就有看看Proxy
类的必要了
1 2 3 4 5 6 7 8 9 public class Proxy implements java .io .Serializable { .... protected InvocationHandler h; protected Proxy (InvocationHandler h) { Objects.requireNonNull(h); this .h = h; } .... }
可以看到,MProxyHandlar 的实例赋给了这个h变量,后面就调用h变量的 invoke
的方法通过反射技术实现对相应方法的代理,而最后的静态代码块中的内容是用来获得 invoke
方法的第二个参数,也就是 Method
对象;就是这个类的实例最后返回给了调用者,最终调用者调用的就是这个类中的方法,而这个方法中进一步用过反射机制使用代理类的 invoke
方法,调用了 MProxyHandlar 中实现的 invoke
方法中的内容,同时传入代理类(此类$Proxy0),被代理方法以及方法参数的信息。
这里可以做一个实验,对 MProxyHandlar
进行修改
1 2 3 4 5 6 7 8 9 public class MProxyHandlar implements InvocationHandler { @Override public Object invoke (Object proxy, Method method, Object[] args) throws Throwable { System.out.println("代理类名为: " + proxy.getClass()); System.out.println("代理启动,被代理的类为 " + method.getDeclaringClass().getName() +" ,代理方法 " + method.getName() +" 执行" ); return null ; } }
让它打印调用它的代理类的名称,运行代码,如果那个类是是以$Proxy
就算成功
1 2 3 4 代理类名为: class com.sun.proxy.$Proxy0 代理启动,被代理的类为 com.example.demo1.IFunc ,代理方法 func1 执行 代理类名为: class com.sun.proxy.$Proxy0 代理启动,被代理的类为 com.example.demo1.IFunc ,代理方法 func2 执行
在MyBatis中的运用 那一段 sqlSession.getMapper
就等于是实例代码中的 ProxyCreater.getProxy
,而实例代码中的 MProxyHandlar
在MyBatis中则是由它自己实现的,传入的第二参数然它知道调用的接口名字和方法的名字,而之前它已经对配置文件进行了解析,把 “namespace.id” 拼接起来正好就是”接口名字.方法名“,两者必须比较就可以实现方法映射了
当然,sqlSession.getMapper
内的关于map的使用我还是没懂,老师上课的时候给出了抽象的实现,这里贴出来吧
1 2 3 4 5 6 public interface MyInterface { List<Object> query (Object parameter) ; }
1 2 3 4 5 6 7 8 9 10 11 12 public class SqlSession { @SuppressWarnings ("unchecked" ) public <T> T getMapper (Class<T> type) { System.out.println("通过接口的Class从代理工厂Map取出对应的代理工厂" ); System.out.println("通过代理工厂实例化一个代理类" ); System.out.println("用这个代理类生成一个代理实例返回出去" ); return (T) Proxy.newProxyInstance(type.getClassLoader(), new Class[]{type}, new MapperProxy()); } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 public class MapperProxy implements InvocationHandler { @Override public Object invoke (Object proxy, Method method, Object[] args) throws Throwable { System.out.println("通过接口与method获取对应的配置文件中的信息:" ); System.out.println("接口名称.方法名==namespace.id" ); System.out.println("通过配置文件中的信息获取SQL语句的类型" ); System.out.println("根据SQL语句类型调用sqlSession对应的增删改查方法" ); System.out.println("当SQL语句类型是查询时" ); System.out.println("根据返回值的类型是List、Map、Object" ); System.out.println("分别调用selectList、selectMap、selectOne方法" ); List<Object> list = new ArrayList<Object>(); list.add("1" ); list.add("2" ); list.add("3" ); return list; } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 public class MyMain { public static void main (String[] args) { System.out.println("加载配置信息……" ); System.out.println("通过加载配置信息加载一个代理工厂Map:" ); System.out.println("这个Map存放的是接口Class与对应的代理工厂" ); SqlSession sqlSession = new SqlSession(); MyInterface myInterface = sqlSession.getMapper(MyInterface.class); List<Object> list = myInterface.query(new Object()); System.out.println(list.size()); } }
运行结果
1 2 3 4 5 6 7 8 9 10 11 12 13 14 加载配置信息…… 通过加载配置信息加载一个代理工厂Map: 这个Map存放的是接口Class与对应的代理工厂 通过接口的Class从代理工厂Map取出对应的代理工厂 通过代理工厂实例化一个代理类 用这个代理类生成一个代理实例返回出去 通过接口与method获取对应的配置文件中的信息: 接口名称.方法名==namespace.id 通过配置文件中的信息获取SQL语句的类型 根据SQL语句类型调用sqlSession对应的增删改查方法 当SQL语句类型是查询时 根据返回值的类型是List、Map、Object 分别调用selectList、selectMap、selectOne方法 3
动态代理的另一种方式 Proxy类的文档中写了两种实现动态代理的方式
1 2 3 4 5 6 7 8 9 InvocationHandler handler = new MyInvocationHandler(...); Class<?> proxyClass = Proxy.getProxyClass(Foo.class.getClassLoader(), Foo.class); Foo f = (Foo) proxyClass.getConstructor(InvocationHandler.class). newInstance(handler); Foo f = (Foo) Proxy.newProxyInstance(Foo.class.getClassLoader(), new Class<?>[] { Foo.class }, handler);
之前使用的是第二种方式,这里再使用第一种方式,就把 ProxyCreater
改一下就可以了
1 2 3 4 5 6 7 8 9 10 public class ProxyCreater { public static <T> T getProxy (Class<T> type) throws Exception { Class<?> proxyClass = Proxy.getProxyClass(type.getClassLoader(), type); Constructor<?> con = proxyClass.getConstructor(InvocationHandler.class); return (T)con.newInstance(new MProxyHandlar()); } }
执行和会发现结果和之前的一样;仔细和第二种方式的源码比较一下就会发现,其实就是把第二种方式的源码的一部分自己写了一下,至于 newInstance
源码嘛,功力不够还是看不懂…