学习笔记:javaEE基础11 面向接口编程

学习来源 通过自动回复机器人学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 {
/**
* 根据 contentId 查询相关的content
*/
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 sqlSession = ListMessageDb.getInstance().getSqlSession();

// 使用sqlSession的getMapper方法,传入接口类
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;s
public 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
// ......
/*
* Look up or generate the designated proxy class.
*/
Class<?> cl = getProxyClass0(loader, intfs);

后面就是对这个类进行实例化,并返回实例

1
2
3
final Constructor<?> cons = cl.getConstructor(constructorParams);
//.....
return cons.newInstance(new Object[]{h});

其中 constructorParams 在之前有定义

1
2
3
/** parameter types of a proxy class constructor */
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);
}

// 以下方法均去掉了 try ... catch ...
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
/**
* 模拟 SqlSession.getMapper 实现
*/
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
/**
* 模拟将配置文件的 "namespace.id" 和 "package.method" 想映射的代理类实现
*/
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
// To create a proxy for some interface Foo:
InvocationHandler handler = new MyInvocationHandler(...);
Class<?> proxyClass = Proxy.getProxyClass(Foo.class.getClassLoader(), Foo.class);
Foo f = (Foo) proxyClass.getConstructor(InvocationHandler.class).
newInstance(handler);
// or more simply:
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 源码嘛,功力不够还是看不懂…