动态生成注解形式的select语句

根据Map动态生成select

在Provider.java中添加返回动态SQL的方法

EmployeeDynamicSQLProvider.java中加入如下方法:

G:\workspace_web2\MyADynamicSQLTest\src\mapper\EmployeeDynamicSQLProvider.java
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
public String selectEmployeeWithParamMap(Map<String, Object> param)
{
return new SQL() {
{
SELECT("*");
FROM("tb_employee");
if (param.get("id") != null)
{
WHERE("id=#{id}");
}
if (param.get("loginname") != null)
{
WHERE("loginname=#{loginname}");
}
if (param.get("password") != null)
{
WHERE("password=#{password}");
}
if (param.get("name") != null)
{
WHERE("name=#{name}");
}
if (param.get("sex") != null)
{
WHERE("sex=#{sex}");
}
if (param.get("age") != null)
{
WHERE("age=#{age}");
}
if (param.get("phone") != null)
{
WHERE("phone=#{phone}");
}
if (param.get("sal") != null)
{
WHERE("sal=#{sal}");
}
if (param.get("state") != null)
{
WHERE("state=#{state}");
}
}
}.toString();
}

该方法根据mapkey来动态生成WHERE子句。

mapper接口的方法中引用Provider.java中的方法

并在mapper接口中添加如下方法:

G:\workspace_web2\MyADynamicSQLTest\src\mapper\EmployeeMapper.java
1
2
3
4
5
@SelectProvider(
type = EmployeeDynamicSQLProvider.class,
method = "selectEmployeeWithParamMap"
)
List<Employee> selectEmployeeWithParamMap(Map<String, Object> param);

测试类 SelectByMapParam.java

/MyADynamicSQLTest/src/test/SelectByMapParam.java
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
package test;

import java.util.HashMap;
import java.util.List;
import org.apache.ibatis.session.SqlSession;
import domain.Employee;
import fractory.SqlSessionFactoryTools;
import mapper.EmployeeMapper;

public class SelectByMapParam {
public static void main(String[] args) {
SqlSession sqlSession = null;
try {
// 加载mybatis-config.xml,获取SqlSession实例
sqlSession = SqlSessionFactoryTools.getSqlSession();
// 获取mapper接口代理对象
EmployeeMapper employeeMapper = sqlSession.getMapper(EmployeeMapper.class);
// 创建Map,作为查询的参数
HashMap<String, Object> param = new HashMap<String, Object>();
// param.put("id", 1);
param.put("sex", "男");
param.put("age", 19);
// 执行按map中的参数查询
List<Employee> employees = employeeMapper.selectEmployeeWithParamMap(param);
employees.forEach(employee -> System.out.println(" " + employee));
// 提交事务
sqlSession.commit();
} catch (Exception e) {
// 出错回滚事务
sqlSession.rollback();
e.printStackTrace();
} finally {
// 关闭会话
if (sqlSession != null)
sqlSession.close();
}
}
}

运行测试类,控制台输出如下:

DEBUG [main] ==>  Preparing: SELECT * FROM tb_employee WHERE (sex=? AND age=?) 
DEBUG [main] ==> Parameters: 男(String), 19(Integer)
DEBUG [main] <==      Total: 2
    Employee [id=1, loginname=xiaoming, password=xiaoming, name=小明, sex=男, age=19, phone=123456789123, sal=9800.0, state=active]
    Employee [id=2, loginname=xiaowang, password=xiaowang, name=小王, sex=男, age=19, phone=123456789123, sal=6800.0, state=active]

可以看到执行的SQL:

1
SELECT * FROM tb_employee WHERE (sex=? AND age=?) 

是根据输入的map参数动态生成的。

再添加一个查询条件

现在取消上门的SelectByMapParam.java中id的注释,再往map中放入一个id作为查询条件:

......
// 创建Map,作为查询的参数
HashMap<String, Object> param = new HashMap<String, Object>();
param.put("id", 1);
param.put("sex", "男");
param.put("age", 19);
// 执行按map中的参数查询
List<Employee> employees = employeeMapper.selectEmployeeWithParamMap(param);
employees.forEach(employee -> System.out.println("    " + employee));
......

再次运行,控制台输出如下:

DEBUG [main] ==>  Preparing: SELECT * FROM tb_employee WHERE (id=? AND sex=? AND age=?) 
DEBUG [main] ==> Parameters: 1(Integer), 男(String), 19(Integer)
DEBUG [main] <==      Total: 1
    Employee [id=1, loginname=xiaoming, password=xiaoming, name=小明, sex=男, age=19, phone=123456789123, sal=9800.0, state=active]

可以看到mybatis根据提供的id,sex,age三个参数动态的生成了如下SQL语句:

1
SELECT * FROM tb_employee WHERE (id=? AND sex=? AND age=?) 

根据持久化对象动态生成select

Provider类中添加动态select方法

在动态SQL生成器EmployeeDynamicSQLProvider.java中添加如下方法:

G:\workspace_web2\MyADynamicSQLTest\src\mapper\EmployeeDynamicSQLProvider.java
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
public String selectEmployeeWithParamPO(Employee parm)
{
return new SQL() {
{
SELECT("*");
FROM("tb_employee");
if (parm.getId() != null)
{
WHERE("id=#{id}");
}
if (parm.getLoginname() != null)
{
WHERE("loginname=#{loginname}");
}
if (parm.getPassword() != null)
{
WHERE("password=#{password}");
}
if (parm.getName() != null)
{
WHERE("name=#{name}");
}
if (parm.getSex() != null)
{
WHERE("sex=#{sex}");
}
if (parm.getAge() != null)
{
WHERE("age=#{age}");
}
if (parm.getPhone() != null)
{
WHERE("phone=#{phone}");
}
if (parm.getSal() != null)
{
WHERE("sal=#{sal}");
}
if (parm.getState() != null)
{
WHERE("state=#{state}");
}
}
}.toString();
}

在mapper接口中使用Provider类的方法

然后在mapper接口中,添加如下方法:

G:\workspace_web2\MyADynamicSQLTest\src\mapper\EmployeeMapper.java
1
2
3
4
5
@SelectProvider(
type = EmployeeDynamicSQLProvider.class,
method = "selectEmployeeWithParamPO"
)
List<Employee> selectEmployeeWithParamPo(Employee param);

测试 SelectByPoParam.java

/MyADynamicSQLTest/src/test/SelectByPoParam.java
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
package test;

import java.util.List;
import org.apache.ibatis.session.SqlSession;
import domain.Employee;
import fractory.SqlSessionFactoryTools;
import mapper.EmployeeMapper;

public class SelectByPoParam {
public static void main(String[] args) {
SqlSession sqlSession = null;
try {
// 加载mybatis-config.xml,获取SqlSession实例
sqlSession = SqlSessionFactoryTools.getSqlSession();
// 获取mapper接口代理对象
EmployeeMapper employeeMapper = sqlSession.getMapper(EmployeeMapper.class);
// 创建存放查询条件的PO类
Employee param = new Employee();
// param.setId(1);
param.setAge(19);
param.setSex("男");
// 使用PO类里面的套件进行查询
List<Employee> employees = employeeMapper.selectEmployeeWithParamPo(param);
// 输出查询结果
employees.forEach(employee -> System.out.println(" " + employee));
// 提交事务
sqlSession.commit();
} catch (Exception e) {
// 出错回滚事务
sqlSession.rollback();
e.printStackTrace();
} finally {
// 关闭会话
if (sqlSession != null)
sqlSession.close();
}
}
}

我们在测试类中提供了sexage两个参数。mybatis将会根据这两个参数生成SQL语句,运行该测试类,控制台输出如下:

DEBUG [main] ==>  Preparing: SELECT * FROM tb_employee WHERE (sex=? AND age=?) 
DEBUG [main] ==> Parameters: 男(String), 19(Integer)
DEBUG [main] <==      Total: 2
  Employee [id=1, loginname=xiaoming, password=xiaoming, name=小明, sex=男, age=19, phone=123456789123, sal=9800.0, state=active]
  Employee [id=2, loginname=xiaowang, password=xiaowang, name=小王, sex=男, age=19, phone=123456789123, sal=6800.0, state=active]

再添加一个查询条件

现在取消SelectByPoParam.java中id参数前面的注释,再添加一个id参数作为查询条件,运行测试类,控制台输出如下:

DEBUG [main] ==>  Preparing: SELECT * FROM tb_employee WHERE (id=? AND sex=? AND age=?) 
DEBUG [main] ==> Parameters: 1(Integer), 男(String), 19(Integer)
DEBUG [main] <==      Total: 1
  Employee [id=1, loginname=xiaoming, password=xiaoming, name=小明, sex=男, age=19, phone=123456789123, sal=9800.0, state=active]

18.4 使用反射生成并操作对象 18.4.3 访问成员变量值

通过Class对象的getFields()getField()方法可以获取该类所包括的全部成员变量或指定成员变量(Field对象).

Field类

Field提供了如下两组方法来读取或设置指定成员变量的值。

访问基本类型的成员变量

  • getXxx(Object object):获取object对象的该成员变量的值。此处的Xxx对应8种基本类型,
  • setXxx(Object object,Xxx value):将object对象的该成员变量设置成value值。此处的Xxx对应8种基本类型,

访问引用类型的成员变量

对应引用类型,直接使用getset方法即可

  • get(Object object):获取object对象的该成员变量的值。
  • set(Object object,Xxx value):将object对象的该成员变量设置成value值。

使用这两个方法可以随意地访问指定对象的所有成员变量,包括private修饰的成员变量。

程序示例

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
import java.lang.reflect.*;
class Person
{
//私有的成员变量
private String name;
//私有的成员变量
private int age;
public String toString()
{
return "Person[name:" + name +
" , age:" + age + " ]";
}
}
public class FieldTest
{
public static void main(String[] args)
throws Exception
{
// 创建一个Person对象
Person p = new Person();
// 获取Person类对应的Class对象
Class<Person> personClazz = Person.class;
// 获取Person的名为name的成员变量
// 使用getDeclaredField()方法表明可获取各种访问控制符的成员变量
Field nameField = personClazz.getDeclaredField("name");
// 设置通过反射访问 name 成员变量时 取消访问权限检查
nameField.setAccessible(true);
// 调用set()方法为p对象的name成员变量设置值
nameField.set(p , "小明");

// 获取Person类名为age的成员变量
Field ageField = personClazz.getDeclaredField("age");
// 通过反射访问 age 成员变量时 取消访问权限检查
// (要在getter和setter方法之前设置)
ageField.setAccessible(true);
// 调用setInt()方法为p对象的age成员变量设置值
ageField.setInt(p , 30);
// 获取p对象的age成员变量的值
System.out.println("p对象的age成员变量的值:"+ageField.getInt(p));
System.out.println(p);
}
}

运行结果:

1
2
p对象的age成员变量的值:30
Person[name:小明 , age:30 ]

代码详解

上面程序中先定义了一个Person类,该类里包含两个private成员变量:nameage,在通常情况下,这两个成员变量只能在Person类里访问。但本程序FieldTestmain()方法中通过反射修改了Person对象的nameage两个成员变量的值。

  • 代码:personClazz.getDeclaredField("name");中的getDeclaredField方法获取了名为name的成员变量,注意此处不是使用getField方法,因为**getField方法只能获取public访问控制的成员变量,而getDeclaredField方法则可以获取所有的成员变量**:
  • 代码:nameField.setAccessible(true);设置通过反射访问成员变量name时不受访问权限的控制;
  • 代码nameField.set(p , "Yeeku.H.Lee");修改了Person对象的name成员变量的值。

修改Person对象的age成员变量的值的方式与此完全相同。
编译、运行上面程序,会看到如下输出:

1
Person[name:Yeeku.H.Lee , age:30 ]

总结

  • Class对象的**getField方法只能获取public访问控制的成员变量,而getDeclaredField方法则可以获取所有的成员变量**
  • 获取(get)或者设置(set)某个私有的成员变量之前,必须先调用该成员变量(Field)setAccessible方法,并把参数设置为true.
    • 例如要获取成员变量name的值之前,要先调用:nameField.setAccessible(true);`

18.4.2 调用方法

当获得某个类对应的Class对象后,就可以通过该Class对象的getMethods()方法或者getMethod()方法来获取全部方法或指定方法—这两个方法的返回值是Method数组,或者Method对象。
每个Method对象对应一个方法,获得Method对象后,程序就可通过该Method来调用它对应的方法。在Method里包含一个invoke()方法,该方法的签名如下:
Object invoke(Object object,Object.args):该方法中的object是执行该方法的类的实例,后面的args是执行该方法时传入该方法的实参。

实例 对象池工厂

下面程序对前面的对象池工厂进行加强,允许在配置文件中增加配置对象的成员变量的值,对象池工厂会读取为该对象配置的成员变量值,并利用该对象对应的setter法设置成员变量的值。

ExtendedObjectPoolFactory.java

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
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
import java.util.*;
import java.io.*;
import java.lang.reflect.*;
public class ExtendedObjectPoolFactory
{
// 定义一个对象池,前面是对象名,后面是实际对象
private Map<String ,Object> objectPool = new HashMap<>();
private Properties config = new Properties();
// 从指定属性文件中初始化Properties对象
public void init(String fileName)
{
try(
FileInputStream fis = new FileInputStream(fileName))
{
config.load(fis);
}
catch (IOException ex)
{
System.out.println("读取" + fileName + "异常");
}
}
// 定义一个创建对象的方法,
// 该方法只要传入一个字符串类名,程序可以根据该类名生成Java对象
private Object createObject(String clazzName)
throws InstantiationException
, IllegalAccessException , ClassNotFoundException
{
// 根据字符串来获取对应的Class对象
Class<?> clazz =Class.forName(clazzName);
// 使用clazz对应类的默认构造器创建实例
return clazz.newInstance();
}
// 该方法根据指定文件来初始化对象池,
// 它会根据配置文件来创建对象
public void initPool()throws InstantiationException
,IllegalAccessException , ClassNotFoundException
{
for (String name : config.stringPropertyNames())
{
// 每取出一对key-value对,如果key中不包含百分号(%)
// 这就标明是根据value来创建一个对象
// 调用createObject创建对象,并将对象添加到对象池中
if (!name.contains("%"))
{
objectPool.put(name ,
createObject(config.getProperty(name)));
}
}
}
// 该方法将会根据属性文件来调用指定对象的setter方法
public void initProperty()throws InvocationTargetException
,IllegalAccessException,NoSuchMethodException
{
for (String name : config.stringPropertyNames())
{
// 每取出一对key-value对,如果key中包含百分号(%)
// 即可认为该key用于控制调用对象的setter方法设置值,
// %前半为对象名字,后半控制setter方法名
if (name.contains("%"))
{
// 将配置文件中key按%分割
String[] objAndProp = name.split("%");
// 取出调用setter方法的参数值
Object target = getObject(objAndProp[0]);
// 获取setter方法名:set + "首字母大写" + 剩下部分
String mtdName = "set" +
objAndProp[1].substring(0 , 1).toUpperCase()
+ objAndProp[1].substring(1);
// 通过target的getClass()获取它实现类所对应的Class对象
Class<?> targetClass = target.getClass();
// 获取希望调用的setter方法
Method mtd = targetClass.getMethod(mtdName , String.class);
// 通过Method的invoke方法执行setter方法,
// 将config.getProperty(name)的值作为调用setter的方法的参数
mtd.invoke(target , config.getProperty(name));
}
}
}
public Object getObject(String name)
{
// 从objectPool中取出指定name对应的对象。
return objectPool.get(name);
}
public static void main(String[] args)
throws Exception
{
//创建对象工厂
ExtendedObjectPoolFactory epf = new ExtendedObjectPoolFactory();
//加载配置文件
epf.init("extObj.txt");
//初始化对象池
epf.initPool();
epf.initProperty();
System.out.println(epf.getObject("a"));
}
}

上面程序中initProperty()方法里的第一行粗体字代码获取目标类中包含一个String参数的setter方法,第二行粗体字代码通过调用Methodinvoke()方法来执行该setr方法,该方法执行完成后,就相当于执行了目标对象的setter方法。为上面程序提供如下配置文件:

extObj.txt

1
2
3
4
a=javax.swing.JFrame
b=javax.swing.JLabel
#set the title of a
a%title=Test Title

上面配置文件中的a%title行表明希望调用a对象的setTitle()方法,调用该方法的参数值为Test Title编译、运行上面的ExtendedObjectPoolFactory.java程序,输出如下:

1
javax.swing.JFrame[frame0,0,0,0x0,invalid,hidden,layout=java.awt.BorderLayout,title=Test Title,resizable,normal,defaultCloseOperation=HIDE_ON_CLOSE,rootPane=javax.swing.JRootPane[,0,0,0x0,invalid,layout=javax.swing.JRootPane$RootLayout,alignmentX=0.0,alignmentY=0.0,border=,flags=16777673,maximumSize=,minimumSize=,preferredSize=],rootPaneCheckingEnabled=true]

可以看到输出一个JFrame窗口,该窗口的标题为Test Title.

应用

Spring框架就是通过这种方式将成员变量值以及依赖对象等都放在配置文件中进行管理的,从而实现了较好的解耦。这也是Spring框架的loC的秘密。

setAccessible取消权限检查

当通过Methodinvoke()方法来调用对应的方法时,Java会要求程序必须有调用该方法的权限。如果程序确实需要调用某个对象的private方法,则可以先调用Method对象的如下方法。

  • setAccessible(boolean flag):将Method对象的accessible设置为指定的布尔值。
    • 如果值为true,指示该Method在使用时取消Java语言的访问权限检查;
    • 如果值为false,则指示该Method在使用时要实施Java语言的访问权限检查

实际上,setAccessible()方法并不属于Method,而是属于它的父类AccessibleObject因此MethodConstructorField都可调用该方法,从而实现通过反射来调用private方法、private构造器和private成员变量,下一节将会让读者看到这种示例。也就是说,它们可以通过调用setAccessible()方法来取消访问权限检查,从而可以通过反射来访问private成员

18.4 使用反射生成并操作对象

Class对象可以获得该类里的

  • 方法(由Method对象表示)、
  • 构造器(由Constructor对象表示)、
  • 成员变量(由Field对象表示),

这三个类都位于java.lang.reflect包下,并实现了java.lang.reflect.Member接口。

  • 程序可以通过Method对象来执行对应的方法,
  • 可以通过Constructor对象来调用对应的构造器创建实例,
  • 可以通过Field对象直接访问并修改对象的成员变量值

18.4.1 创建对象

通过反射生成对象的两种方式

通过反射来生成对象有如下两种方式。

  • 使用Class对象的newInstance()方法来创建该Class对象对应类的实例,这种方式要求该Class对象的对应类有默认构造器,而执行newInstance()方法时实际上是利用默认构造器来创建该类的实例.
  • 先使用Class对象获取指定的Constructor对象,再调用Constructor对象的newInstance()方法来创建该Class对象对应类的实例。通过这种方式可以选择使用指定的构造器来创建实例

使用Class对象的newInstance方法

通过第一种方式来创建对象是比较常见的情形,因为在很多Java EE框架中都需要根据配置文件信息来创建Java对象,从配置文件读取的只是某个类的字符串类名,程序需要根据该字符串来创建对应的实例,就必须使用反射。

实例 简单对象池

下面程序就实现了一个简单的对象池,该对象池会根据配置文件读取key-value对,然后创建这些对象,并将这些对象放入一个HashMap中。

ObjectPoolFactory.java

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
47
48
49
50
51
52
53
54
55
56
import java.util.*;
import java.io.*;
public class ObjectPoolFactory
{
// 定义一个对象池,key是对象名,value是实际对象
private Map<String ,Object> objectPool = new HashMap<>();
// 定义一个创建对象的方法,
// 该方法只要传入一个字符串类名,程序可以根据该类名生成Java对象
private Object createObject(String clazzName)
throws InstantiationException
, IllegalAccessException , ClassNotFoundException
{
// 根据字符串来获取对应的Class对象
Class<?> clazz = Class.forName(clazzName);
// 使用clazz对应类的默认构造器创建实例
return clazz.newInstance();
}
// 该方法根据指定文件来初始化对象池,
// 它会根据配置文件来创建对象
public void initPool(String fileName)
throws InstantiationException
, IllegalAccessException ,ClassNotFoundException
{
try(
FileInputStream fis = new FileInputStream(fileName))
{
Properties props = new Properties();
props.load(fis);
for (String name : props.stringPropertyNames())
{
// 每取出一对key-value对,就根据value创建一个对象
// 调用createObject()创建对象,并将对象添加到对象池中
objectPool.put(name ,
createObject(props.getProperty(name)));
}
}
catch (IOException ex)
{
System.out.println("读取" + fileName + "异常");
}
}
public Object getObject(String name)
{
// 从objectPool中取出指定name对应的对象。
return objectPool.get(name);
}
public static void main(String[] args)
throws Exception
{
ObjectPoolFactory pf = new ObjectPoolFactory();
//读取文件,初始化对象池
pf.initPool("obj.txt");
System.out.println(pf.getObject("a")); // ①
System.out.println(pf.getObject("b")); // ②
}
}

上面程序中createObject()方法里的代码就是根据字符串来创建Java对象的关键代码,程序调用Class对象的newInstance()方法即可创建一个Java对象。程序中的initPool()方法会读取属性文件,对属性文件中每个key-value对创建一个Java对象,其中value是该Java对象的实现类,而key是该Java对象放入对象池中的名字。为该程序提供如下属性配置文件obj.txt.

obj.txt

1
2
a=java.util.Date
b=javax.swing.JFrame

编译、运行上面的ObjectPoolFactory程序,

  • 执行到main方法中的①号代码处,将看到输出系统当前时间——这表明对象池中已经有了一个名为a的对象,该对象是一个java.util.Date对象。
  • 执行到②号代码处,将看到输出一个JFrame对象。

运行效果如下:

1
2
Sun Jun 16 10:00:06 CST 2019
javax.swing.JFrame[frame0,0,0,0x0,invalid,hidden,layout=java.awt.BorderLayout,title=,resizable,normal,defaultCloseOperation=HIDE_ON_CLOSE,rootPane=javax.swing.JRootPane[,0,0,0x0,invalid,layout=javax.swing.JRootPane$RootLayout,alignmentX=0.0,alignmentY=0.0,border=,flags=16777673,maximumSize=,minimumSize=,preferredSize=],rootPaneCheckingEnabled=true]

通过属性文件配置对象的应用

这种使用配置文件来配置对象,然后由程序根据配置文件来创建对象的方式非常有用,大名鼎鼎的Spring框架就采用这种方式大大简化了Java EE应用的开发。当然,Spring采用的是XML配置文件—毕竟属性文件能配置的信息太有限了,而XML配置文件能配置的信息就丰富多了.

利用Constructor对象来创建对象

如果不想利用默认构造器来创建Java对象,而想利用指定的构造器来创建Java对象,则需要利用Constructor对象,每个Constructor对应一个构造器。为了利用指定的构造器来创建Java对象,需要如下三个步骤。

  1. 获取该类的Class对象
  2. 利用Class对象的getConstructor()方法来获取指定的构造器
  3. 调用ConstructornewInstance()方法来创建Java对象。

实例

下面程序利用反射来创建一个JFrame对象,而且使用指定的构造器.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import java.lang.reflect.*;
public class CreateJFrame
{
public static void main(String[] args)
throws Exception
{
// 1.获取JFrame对应的Class对象
Class<?> jframeClazz = Class.forName("javax.swing.JFrame");
// 2.获取JFrame中带一个字符串参数的构造器
Constructor ctor = jframeClazz.getConstructor(String.class);
// 3.调用Constructor的newInstance方法创建对象
Object obj = ctor.newInstance("测试窗口");
// 输出JFrame对象
System.out.println(obj);
}
}

运行结果:

1
javax.swing.JFrame[frame0,0,0,0x0,invalid,hidden,layout=java.awt.BorderLayout,title=测试窗口,resizable,normal,defaultCloseOperation=HIDE_ON_CLOSE,rootPane=javax.swing.JRootPane[,0,0,0x0,invalid,layout=javax.swing.JRootPane$RootLayout,alignmentX=0.0,alignmentY=0.0,border=,flags=16777673,maximumSize=,minimumSize=,preferredSize=],rootPaneCheckingEnabled=true]

代码详解

上面程序中第二行代码(这里说的第二行是指main方法中第二个可执行代码,不包括注释):Constructor ctor = jframeClazz.getConstructor(String.class);用于获取JFrame类的指定构造器,前面已经提到:如果要唯一地确定某类中的构造器,只要指定构造器的形参列表即可。在获取构造器时传入了一个String类型,就表示获取只有一个字符串参数的构造器
程序中第三行可执行代码使用指定构造器的newInstance()方法来创建一个Java对象,当调用Constructor对象的newInstance()方法时通常需要传入参数,因为调用ConstructornewInstance()方法实际上等于调用它对应的构造器,传给newInstance()方法的参数将作为对应构造器的参数.

小结

对于上面的CreateFrame.java中已知java.swing.JFrame类的情形,通常没有必要使用反射来创建该对象,因为通过反射创建对象时性能要稍低一些。实际上,只有当程序需要动态创建某个类的对象时才会考虑使用反射,通常在开发通用性比较广的框架、基础平台时可能会大量使用反射。

18.1 类的加载 连接和初始化

系统可能在第一次使用某个类时加载该类,也可能采用预加载机制来加载某个类。本节将会详细介绍类加载、连接和初始化过程中的每个细节。

18.1.1 JVM和类

当调用java命令运行某个Java程序时,该命令将会启动一个Java虚拟机进程,不管该Java程序有多么复杂,该程序启动了多少个线程,它们都处于该Java虚拟机进程里。正如前面介绍的,同一个JVM的所有线程、所有变量都处于同一个进程里,它们都使用该JVM进程的内存区

JVM进程终止的情况

  • 当系统出现以下几种情况时,JVM进程将被终止。
  • 程序运行到最后正常结束。
  • 程序运行到使用System.exit()Runtime.getRuntime().exit()代码处结束程序.
  • 程序执行过程中遇到未捕获的异常或错误而结束
  • 程序所在平台强制结束了JVM进程。

从上面的介绍可以看出,Java程序运行结束时,JVM进程结束,该进程在内存中的状态将会丢失。下面以类的类变量来说明这个问题。

程序示例

下面程序先定义了一个包含类变量的类。

1
2
3
4
public class A {
// 定义该类的类变量
public static int a = 6;
}

上面程序中定义了一个类变量a,接下来定义一个类创建A类的实例,并访问A对象的类变量a。

1
2
3
4
5
6
7
8
9
public class ATest1 {
public static void main(String[] args) {
// 创建A类的实例
A a = new A();
// 让a实例的类变量a的值自加
a.a++;
System.out.println(a.a);
}
}

下面程序也创建A对象,并访问其类变量a的值

1
2
3
4
5
6
7
8
public class ATest2 {
public static void main(String[] args) {
// 创建A类的实例
A b = new A();
// 输出b实例的类变量a的值
System.out.println(b.a);
}
}

ATest1.Java程序中创建了A类的实例,并让该实例的类变量a的值自加,程序输出该实例的类变量a的值将看到7,相信读者对这个答案没有疑问。关键是运行第二个程序ATest2时,程序再次创建了A对象,并输出A对象类变量的a的值,此时a的值是多少呢?结果依然是6,并不是7。这是因为运行ATest1ATest2是两次运行JVM进程,第一次运行JVM结束后,它对A类所做的修改将全部丢失,第二次运行JVM时将再次初始化A类

两次运行Java程序处于两个不同的JVM进程中,两个JVM之间并不会共享数据。

18.1.2 类的加载

类加载

当程序主动使用某个类时,如果该类还未被加载到内存中,则系统会通过加载连接初始化三个步骤来对该类进行初始化。如果没有意外,JVM将会连续完成这三个步骤,所以有时也把这三个步骤统称为类加载类初始化

什么是类加载

类加载指的是将类的class文件读入内存,并为之创建一个java.lang.Class对象,也就是说,当程序中使用任何类时,系统都会为之建立一个java.lang.Class对象。

类也是对象

系统中所有的类实际上也是实例,它们都是java.lang.Class的实例。

类加载器

类的加载由类加载器完成,类加载器通常由JVM提供,这些类加载器也是前面所有程序运行的基础,

  • JVM提供的这些类加载器通常被称为系统类加载器
  • 除此之外,开发者可以通过继承ClassLoader基类来创建自己的类加载器。

类加载来源

通过使用不同的类加载器,可以从不同来源加载类的二进制数据,通常有如下几种来源:

  • 从本地文件系统加载class文件,这是前面绝大部分示例程序的类加载方式.
  • JAR包加载class文件,这种方式也是很常见的,前面介绍JDBC编程时用到的数据库驱动类就放在JAR文件中,JVM可以从JAR文件中直接加载该class文件。
  • 通过网络加载class文件。
  • 把一个Java源文件动态编译,并执行加载。

类加载器通常无须等到”首次使用”该类时才加载该类,**Java虚拟机规范允许系统预先加载某些类**。

18.1.3 类的连接

当类被加载之后,系统为之生成一个对应的Class对象,接着将会进入连接阶段,连接阶段负责把类的二进制数据合并到JRE中。类连接又可分为如下三个阶段。

  • 验证:验证阶段用于检验被加载的类是否有正确的内部结构,并和其他类协调一致。
  • 准备:类准备阶段则负责为类的类变量分配内存,并设置默认初始值
  • 解析:将类的二进制数据中的符号引用替换成直接引用。

18.1.4 类的初始化

在类的初始化阶段,虚拟机负责对类进行初始化,主要就是对类变量进行初始化。

对类变量指定初始值的两种方式

Java类中对类变量指定初始值有两种方式:

  1. 声明类变量时指定初始值;
  2. 使用静态初始化块为类变量指定初始值。

例如下面代码片段。

1
2
3
4
5
6
7
8
9
10
11
public calss Test
{
//声明变量a时指定初始值
static int a=5;
static int b;
static int c;
static{
//使用静态初始化块为变量b指定初始值
b=6;
}
}

对于上面代码,程序为类变量a、b都显式指定了初始值,所以这两个类变量的值分别为5、6,但类变量c则没有指定初始值,它将采用默认初始值0。

静态初始化块被当成类的初始化语句,JVM会按这些语句在程序中的排列顺序依次执行它们.例如下面的类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Test {
static {
// 使用静态初始化块为变量b指定出初始值
b = 6;
System.out.println("----------");
}
// 声明变量a时指定初始值
static int a = 5;
static int b = 9; // ①
static int c;

public static void main(String[] args) {
System.out.println(Test.b);
}
}

上面代码先在静态初始化块中为b变量赋值,此时类变量b的值为6;
接着程序向下执行,执行到①号代码处,这行代码也属于该类的初始化语句,所以程序再次为类变量b赋值。也就是说,当Test类初始化结束后,该类的类变量b的值为9

JVM初始化类步骤

JVM初始化一个类包含如下几个步骤。

  1. 假如这个类还没有被加载和连接,则程序先加载并连接该类
  2. 假如该类的直接父类还没有被初始化,则先初始化其直接父类
  3. 假如该类中有初始化语句,则系统依次执行这些初始化语句

当执行第2个步骤时,系统对直接父类的初始化步骤也遵循此步骤1到3;如果该直接父类又有直接父类,则系统再次重复这三个步骤来先初始化这个父类…依此类推。

JVM总是最先初始化Object类

所以**JVM最先初始化的总是java.lang.Object当程序主动使用任何一个类时,系统会保证该类以及所有父类(包括直接父类和间接父类)都会被初始化**。

18.1.5 类初始化的时机

Java程序首次通过下面6种方式来使用某个类或接口时,系统就会初始化该类或接口.

  1. 创建类的实例。为某个类创建实例的方式包括:
    • 使用new操作符来创建实例,
    • 通过反射来创建实例,
    • 通过反序列化的方式来创建实例。
  2. 调用某个类的类方法(静态方法)
  3. 访问某个类或接口的类变量,或为该类变量赋值。
  4. 使用反射方式来强制创建某个类或接口对应的java.lang.Class对象。例如代码:Class.forName("Person");如果系统还未初始化Person类,则这行代码将会导致该Person类被初始化,并返回Person类对应的java.lang.Class对象。
  5. 初始化某个类的子类。当初始化某个类的子类时,该子类的所有父类都会被初始化。
  6. 直接使用java.exe命令来运行某个主类。当运行某个主类时,程序会先初始化该主类。

访问编译时就可以确定的类变量不会初始化类

除此之外,下面的几种情形需要特别指出。

访问编译时可确定值得final变量不会初始化类

对于一个 final型的类变量,如果该类变量的值在编译时就可以确定下来,那么这个类变量相当于“宏变量”。Java编译器会在编译时直接把这个类变量出现的地方替换成它的值,因此即使程序使用该静态类变量,也不会导致该类的初始化。例如下面示例程序的结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class MyTest
{
static
{
System.out.println("静态初始化块...");
}
// 使用一个字符串直接量为static final的类变量赋值
static final String compileConstant = "final static字符串";
}
public class CompileConstantTest
{
public static void main(String[] args)
{
// 访问、输出MyTest中的compileConstant类变量
System.out.println(MyTest.compileConstant); // ①
}
}

运行结果

1
final static字符串

上面程序的MyTest类中有一个compileConstant的类变量,该类变量使用了final修饰,而且它的值可以在编译时确定下来,因此compileConstant会被当成”宏变量”处理。程序中所有使用compileConstant的地方都会在编译时被直接替换成它的值——也就是说,上面程序中①处的代码在编译时就会被替换成”final static字符串“,所以代码①不会导致初始化MyTest类,静态代码块将不会执行。

final修饰的类常量

当某个类变量(也叫静态变量)使用了final修饰,而且它的值可以在编译时就确定下来,那么程序其他地方使用该类变量时,实际上并没有使用该类变量,而是相当于使用常量

反之,如果final修饰的类变量的值不能在编译时确定下来,则必须等到运行时才可以确定该类变量的值,如果通过该类来访问它的类变量,则会导致该类被初始化。例如将上面程序中定义compileConstant的代码改为如下:

1
2
// 采用系统当前时间为static final类变量赋值
static final String compileConstant = System.currentTimeMillis()+"静态字符串嘻嘻嘻";//①

因为上面定义的compileConstant类变量的值必须在运行时才可以确定,所以①处的字代码必须保留为对MyTest类的类变量的引用,这行代码就变成了使用MyTest的类变量,这将导致MyTest类被初始化。
运行效果如下:

1
2
静态初始化块...
1560616123824静态字符串嘻嘻嘻

ClassLoader类的loadClass方法 不会初始化类

当使用ClassLoader类的loadClass()方法来加载某个类时,这个loadClass()方法只是加载该类,并不会执行该类的初始化。

Class类的forName方法会强制初始化类

使用ClassforName()静态方法才会导致强制初始化该类。例如如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Tester
{
static
{
System.out.println("Tester类的静态初始化块...");
}
}
public class ClassLoaderTest
{
public static void main(String[] args)
throws ClassNotFoundException
{
ClassLoader cl = ClassLoader.getSystemClassLoader();
// 下面语句仅仅是加载Tester类
cl.loadClass("Tester");//代码1
System.out.println("---------------------------------------------");
System.out.println("系统加载Tester类");
// 下面语句才会初始化Tester类
Class.forName("Tester");//代码2
}
}

上面程序中的代码代码1 代码2都用到了Tester类,代码1只是加载Tester类,并不会初始化Tester类。运行上面程序,会看到如下运行结果:

1
2
系统加载Tester类
Tester类的静态初始化块...

从上面运行结果可以看出,必须等到执行Class.forName("Tester")时才完成对Tester类的初始化。

第18章 类加载机制与反射

本章要点

  • 类加载
  • 类连接的过程
  • 类初始化的过程
  • 类加载器以及实现机制
  • 继承ClassLoader实现自定义类加载器
  • 使用URLClassLoader
  • 使用Class对象
  • Java8新增的方法参数反射
  • 动态创建Java对象
  • 动态调用方法
  • 访问并修改Java对象的属性值
  • 使用反射操作数组
  • 使用ProxyInvocationHandler创建动态代理
  • AOP入门
  • Class类的泛型
  • 通过反射获取泛型类型

本章将会深入介绍Java类的加载、连接和初始化知识,并重点介绍Java反射的相关内容。

自定义加载器

Java类加载器除了根类加载器之外,其他类加载器都是使用Java语言编写的,所以程序员完全可以开发自己的类加载器,通过使用自定义类加载器,可以完成一些特定的功能.

重点介绍java.lang.reflect包下的接口和类

本章将重点介绍java.lang.reflect包下的接口和类。

反射相关类

类主要有ClassMethodFieldConstructorArray等,这些类分别代表类、方法、成员变量、构造器和数组,Java程序使用这些类

  • 可以动态地获取某个对象、某个类的运行时信息,
  • 可以动态地创建Java对象,
  • 可以动态地调用Java方法,
  • 访问并修改指定对象的成员变量值。

放射相关接口

本章还将介绍该包下的TypeParameterizedType两个接口,其中TypeClass类所实现的接口,而ParameterizedType则代表一个带泛型参数的类型

JDK动态代理

本章将介绍使用ProxyInvocationHandler来创建JDK动态代理,并会通过JDK动态代理向读者介绍高层次解耦的方法,还会讲解JDK动态代理和AOP(Aspect Orient Programming,面向切面编程)之间的内在关系。

11.2 注解的使用示例5 测试动态SQL

MyBatis的注解也支持动态SQLMyBatis提供了各种注解,如@InsertProvider@UpdateProvider@DeleteProvider@SelectProvider,来帮助构建动态SQL语句,然后MyBatis可以执行这些SQL语句。

以上4个Provider注解都有

  • type属性,该属性指定了一个类。
  • method属性指定该类的方法,其用来提供需要执行的SQL语句。

SQL类

使用字符串拼接的方法构建SQL语句是非常困难的,并且容易出错。所以MyBaits提供了一个SQL工具类org.apache.ibatis.jdbc.SQL该类不使用字符串拼接的方式,并且会以合适的空格前缀和后缀来构造SQL语句。
SQL类的常用方法如下:

方法 描述
T LEFT_OUTER_JOIN(String join) JOIN子句,连接方式是左外连接(LEFT_OUTER)JOIN)。
T RIGHT_OUTER_JOIN(String join) JOIN子句,连接方式是右外连接(RIGHT_OUTER _JOIN)。
T WHERE(String conditions) 追加一个新的WHERE子句条件,可以多次调用。
T OR() 使用OR拆分当前WHERE子句条件,可以不止一次调用.
T AND() 使用AND拆分当前WHERE子句条件,可以不止一次调用。
T GROUP_BY(String columns) 追加一个新的GROUP BY子句元素。
T HAVING( String conditions) 追加一个新的 HAVING子句条件.
T ORDER_BY(String columns) 追加一个新的ORDER BY子句元素。
T INSERT_INTO(String tableName) 启动INSERT语句插入到指定表,应遵循由个或多个VALUES()调用。
T VALUES(String columns,String values) 追加的INSERT语句。第一个参数是要插入的列,第二个参数是插入的值。
T DELETE_FROM(String table) 启动DELETE语句,并指定删除表.
T UPDATE(String table) 启动一个更新(UPDATE)语句,并指定更新表。
T SET(String sets) 追加一个SET更新语句列表。

动态SQL provider方法可以接受以下参数:

  • 无参数
  • java对象
  • java.util.Map

搭建基本项目框架

项目结构

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
G:\workspace_web2\MyADynamicSQLTest
├─src
│ ├─db.properties
│ ├─domain
│ │ └─Employee.java
│ ├─fractory
│ │ └─SqlSessionFactoryTools.java
│ ├─log4j.xml
│ ├─mapper
│ │ ├─EmployeeDynamicSQLProvider.java
│ │ └─EmployeeMapper.java
│ ├─mybatis-config.xml
│ ├─tb_employee.sql
│ └─test
│ ├─TestParamPO.java
│ └─TestParmMap.java
└─WebContent
└─WEB-INF
└─lib
├─commons-logging-1.2.jar
├─log4j-1.2.17.jar
├─log4j-api-2.3.jar
├─log4j-core-2.3.jar
├─mybatis-3.4.5.jar
└─mysql-connector-java-5.1.44-bin.jar

数据库表

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 创建数据表
DROP TABLE IF EXISTS `tb_employee`;
CREATE TABLE `tb_employee` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`loginname` varchar(18) DEFAULT NULL,
`password` varchar(18) DEFAULT NULL,
`name` varchar(18) DEFAULT NULL,
`sex` char(2) DEFAULT NULL,
`age` int(11) DEFAULT NULL,
`phone` varchar(21) DEFAULT NULL,
`sal` double DEFAULT NULL,
`state` varchar(255) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
# 插入数据
INSERT INTO `tb_employee` VALUES ('1', 'xiaoming', 'xiaoming', '小明', '男', '19', '123456789123', '9800', 'active');
INSERT INTO `tb_employee` VALUES ('2', 'xiaowang', 'xiaowang', '小王', '男', '19', '123456789123', '6800', 'active');
INSERT INTO `tb_employee` VALUES ('3', 'xiaoli', 'xiaoli', '小丽', '女', '23', '123456789123', '7800', 'active');
INSERT INTO `tb_employee` VALUES ('4', 'xiaofang', 'xiaofang', '小芳', '女', '22', '123456789123', '8800', 'active');

持久化对象

/MyADynamicSQLTest/src/domain/Employee.java
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
package domain;
public class Employee {
// 员工编号
private Integer id;
// 登录名
private String loginname;
// 密码
private String password;
private String name;
private String sex;
private Integer age;
private String phone;
// 薪水
private Double sal;
// 状态
private String state;
public Employee()
{
}
public Employee(Integer id, String loginname, String password, String name, String sex,
Integer age, String phone, double sal, String state)
{
super();
this.id = id;
this.loginname = loginname;
this.password = password;
this.name = name;
this.sex = sex;
this.age = age;
this.phone = phone;
this.sal = sal;
this.state = state;
}
// 此处省略getter和setter方法,请自己补上
@Override
public String toString()
{
return "Employee [id=" + id + ", loginname=" + loginname + ", password=" + password
+ ", name=" + name + ", sex=" + sex + ", age=" + age + ", phone=" + phone + ", sal="
+ sal + ", state=" + state + "]";
}
}

工具类

/MyADynamicSQLTest/src/fractory/SqlSessionFactoryTools.java
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
package fractory;
import java.io.IOException;
import java.io.InputStream;
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;
public class SqlSessionFactoryTools {
private static SqlSessionFactory sqlSessionFactory = null;
static
{
try
{
InputStream mybatisConfigXML = Resources.getResourceAsStream("mybatis-config.xml");
sqlSessionFactory = new SqlSessionFactoryBuilder().build(mybatisConfigXML);
} catch (IOException e)
{
e.printStackTrace();
}
}
public static SqlSession getSqlSession()
{
return sqlSessionFactory.openSession();
}
}

mybatis相关配置文件

数据库信息属性文件db.properties

/MyADynamicSQLTest/src/db.properties
1
2
3
4
5
6
7
8
# 保存为db.properties文件,然后在mybatis-config.xml中通过下面标签引入:
# <properties resource="db.properties"/>
# 下面的标签放在mybatis-config.xml文件的environments标签的environment子标签的子标签dataSource标签中
# <property name="driver" value="${driver}"/><property name="url" value="${url}"/><property name="username" value="${username}"/><property name="password" value="${password}"/>
driver=com.mysql.jdbc.Driver
url=jdbc:mysql://localhost:3306/mybatis
username=root
password=root

log4j.xml

/MyADynamicSQLTest/src/log4j.xml
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
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE log4j:configuration
PUBLIC "-//LOG4J//DTD LOG4J//EN"
"https://logging.apache.org/log4j/1.2/apidocs/org/apache/log4j/xml/doc-files/log4j.dtd" >
<!-- 请在mybatis-config.xml中配置如下设置 -->
<!-- <settings> -->
<!-- <setting -->
<!-- name="logImpl" -->
<!-- value="log4j"/> -->
<!-- </settings> -->
<log4j:configuration>
<appender
name="STDOUT"
class="org.apache.log4j.ConsoleAppender">
<layout class="org.apache.log4j.PatternLayout">
<param
name="ConversionPattern"
value="%5p [%t] %m%n"/>
</layout>
</appender>
<logger name="mapper.EmployeeMapper">
<level value="DEBUG"/>
</logger>
<root>
<level value="ERROR"/>
<appender-ref ref="STDOUT"/>
</root>
</log4j:configuration>

mybatis-config.xml

/MyADynamicSQLTest/src/mybatis-config.xml
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
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<!-- 该配置文件包含对 MyBatis 系统的核心设置 -->
<configuration>
<properties resource="db.properties" />
<settings>
<setting name="logImpl" value="log4j" />
</settings>
<environments default="mysql">
<environment id="mysql">
<transactionManager type="JDBC" />
<dataSource type="pooled">
<property name="driver" value="${driver}" />
<property name="url" value="${url}" />
<property name="username" value="${username}" />
<property name="password" value="${password}" />
</dataSource>
</environment>
</environments>
<mappers>
<mapper class="mapper.EmployeeMapper" />
</mappers>
</configuration>

EmployeeMapper接口

/MyADynamicSQLTest/src/mapper/EmployeeMapper.java
1
2
3
4
package mapper;
public interface EmployeeMapper {

}

动态SQL生成器

/MyADynamicSQLTest/src/mapper/EmployeeDynamicSQLProvider.java
1
2
3
4
5
6
7
package mapper;
import java.util.Map;
import org.apache.ibatis.jdbc.SQL;
import domain.Employee;
public class EmployeeDynamicSQLProvider {

}

动态SQL方法写法

1
2
3
4
5
6
7
8
public String 方法名称(参数列表)
{
return new SQL() {
{
//动态sql语句写在这里
}
}.toString();
}

注意构造函数SQL()后面有两个嵌套的大括号,

  • 内层大括号里面写上动态SQL的生成逻辑,
  • 外层大括号后面接上.toString(),以调用toString方法生成SQL语句(字符串)

11.2 注解的使用 多对多关联关系

例如商品和订单之间是多对多关系,即一个商品可以放在多个订单之中,而一个订单中放多个不同类型的商品。
对于这种多对多关联关系,

  • 在查询商品时,使用@Many注解来关联查询包含该商品的所有订单。
  • 在查询订单时,同样使用@Many注解来关联查询该订单中的所有商品信息。

示例 多对多关联关系

项目结构

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
G:\workspace_web2\MyAManyToManyTest
├─src
│ ├─db.properties
│ ├─domain
│ │ ├─Article.java
│ │ ├─Order.java
│ │ └─User.java
│ ├─fractory
│ │ └─SqlSessionFactoryTools.java
│ ├─log4j.xml
│ ├─manytomany.sql
│ ├─mapper
│ │ ├─ArticleMapper.java
│ │ ├─OrderMapper.java
│ │ └─UserMapper.java
│ ├─mybatis-config.xml
│ └─test
│ ├─SelectArticleByIdTest.java
│ ├─SelectOrderByIdTest.java
│ └─SelectUserByIdTest.java
└─WebContent
└─WEB-INF
└─lib
├─commons-logging-1.2.jar
├─log4j-1.2.17.jar
├─log4j-api-2.3.jar
├─log4j-core-2.3.jar
├─mybatis-3.4.5.jar
└─mysql-connector-java-5.1.44-bin.jar

数据库表

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
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
-- 关闭外键检查
SET FOREIGN_KEY_CHECKS = 0;
use mybatis;
-- 创建用户表
DROP TABLE IF EXISTS `tb_user`;
CREATE TABLE `tb_user` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`username` varchar(18) DEFAULT NULL,
`loginname` varchar(18) NOT NULL,
`password` varchar(18) NOT NULL,
`phone` varchar(18) DEFAULT NULL,
`address` varchar(18) DEFAULT NULL,
PRIMARY KEY (`id`)
);
-- 插入用户
INSERT INTO `tb_user` VALUES ('1', '小明', 'xiaoming', 'xiaoming', '123456789123', '北京');
-- 创建商品表
DROP TABLE IF EXISTS `tb_article`;
CREATE TABLE `tb_article` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(18) DEFAULT NULL,
`price` double DEFAULT NULL,
`remark` varchar(18) DEFAULT NULL,
PRIMARY KEY (`id`)
);
-- 插入商品
INSERT INTO `tb_article` VALUES ('1', '商品1', '123.12', 'xxx的伟大著作');
INSERT INTO `tb_article` VALUES ('2', '商品2', '12.3', 'yyy的伟大著作');
INSERT INTO `tb_article` VALUES ('3', '商品3', '34.22', 'zzz的著作');
-- 创建订单表
DROP TABLE IF EXISTS `tb_order`;
CREATE TABLE `tb_order` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`code` varchar(32) DEFAULT NULL,
`total` double DEFAULT NULL,
`user_id` int(11) DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `user_id` (`user_id`),
CONSTRAINT `tb_order_ibfk_1` FOREIGN KEY (`user_id`) REFERENCES `tb_user` (`id`) ON DELETE CASCADE ON UPDATE CASCADE
);
-- 插入订单
INSERT INTO `tb_order` VALUES ('1', 'abcseeeahoaugoeijgiej', '223.33', '1');
INSERT INTO `tb_order` VALUES ('2', 'sfaofosfhodsfuefie', '111.22', '1');
-- 创建订单-商品关系表
DROP TABLE IF EXISTS `tb_item`;
CREATE TABLE `tb_item` (
`order_id` int(11) NOT NULL DEFAULT '0',
`article_id` int(11) NOT NULL DEFAULT '0',
`amount` int(11) DEFAULT NULL,
PRIMARY KEY (`order_id`,`article_id`),
KEY `article_id` (`article_id`),
CONSTRAINT `tb_item_ibfk_1` FOREIGN KEY (`order_id`) REFERENCES `tb_order` (`id`) ON DELETE CASCADE ON UPDATE CASCADE,
CONSTRAINT `tb_item_ibfk_2` FOREIGN KEY (`article_id`) REFERENCES `tb_article` (`id`) ON DELETE CASCADE ON UPDATE CASCADE
);
-- 插入商品-订单
INSERT INTO `tb_item` VALUES ('1', '1', '1');
INSERT INTO `tb_item` VALUES ('1', '2', '1');
INSERT INTO `tb_item` VALUES ('1', '3', '3');
INSERT INTO `tb_item` VALUES ('2', '1', '2');
INSERT INTO `tb_item` VALUES ('2', '2', '3');
-- 开启外键检查
SET FOREIGN_KEY_CHECKS = 1;

持久化对象

User.java

/MyAManyToManyTest/src/domain/User.java
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
package domain;

import java.util.List;

public class User {
// 用户id
private Integer id;
// 用户姓名
private String username;
// 登录名
private String loginname;
// 密码
private String password;
// 手机号
private String phone;
// 地址
private String address;
// 用户的订单列表
List<Order> orders;

public User() {
}

public User(String username, String loginname, String password, String phone, String address) {
super();
this.username = username;
this.loginname = loginname;
this.password = password;
this.phone = phone;
this.address = address;
}

// 此处省略getter和setter方法,请自己补上
@Override
public String toString() {
return "User [id=" + id + ", username=" + username + ", loginname=" + loginname + ", password=" + password
+ ", phone=" + phone + ", address=" + address + "]";
}
}

Order.java

/MyAManyToManyTest/src/domain/Order.java
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
package domain;
import java.util.List;
public class Order {
// 订单编号
private Integer id;
// 订单描述
private String code;
// 订单总金额
private double total;
// 该订单对应的用户
private User user;
// 该订单中的商品列表
List<Article> articles;
public Order() {
}
public Order(String code, double total, User user) {
super();
this.code = code;
this.total = total;
this.user = user;
}
// 此处省略getter和setter方法,请自己补上
public List<Article> getArticles() {
return articles;
}
public void setArticles(List<Article> articles) {
this.articles = articles;
}
@Override
public String toString() {
return "Order [id=" + id + ", code=" + code + ", total=" + total + "]";
}
}

Article.java

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
package domain;
import java.util.List;
public class Article {
// 商品编号
private Integer id;
// 商品名称
private String name;
// 商品价格
private double price;
// 商品描述
private String remark;
// 包含该商品的订单列表
List<Order> orders;

public Article()
{
super();
}

public Article(String name, double price, String remark)
{
super();
this.name = name;
this.price = price;
this.remark = remark;
}
// 此处省略getter和setter方法,请自己补上
@Override
public String toString()
{
return "Article [id=" + id + ", name=" + name + ", price=" + price + ", remark=" + remark
+ "]";
}
}

工具类SqlSessionFactoryTools.java

/MyAManyToManyTest/src/fractory/SqlSessionFactoryTools.java
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
package fractory;

import java.io.IOException;
import java.io.InputStream;
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;

public class SqlSessionFactoryTools {
private static SqlSessionFactory sqlSessionFactory = null;
static
{
try
{
InputStream mybatisConfigXML = Resources.getResourceAsStream("mybatis-config.xml");
sqlSessionFactory = new SqlSessionFactoryBuilder().build(mybatisConfigXML);
} catch (IOException e)
{
e.printStackTrace();
}
}
public static SqlSession getSqlSession()
{
return sqlSessionFactory.openSession();
}
}

mybatis相关配置文件

db.propertis

1
2
3
4
5
6
7
8
# 保存为db.properties文件,然后在mybatis-config.xml中通过下面标签引入:
# <properties resource="db.properties"/>
# 下面的标签放在mybatis-config.xml文件的environments标签的environment子标签的子标签dataSource标签中
# <property name="driver" value="${driver}"/><property name="url" value="${url}"/><property name="username" value="${username}"/><property name="password" value="${password}"/>
driver=com.mysql.jdbc.Driver
url=jdbc:mysql://localhost:3306/mybatis
username=root
password=root

log4j.xml

/MyAManyToManyTest/src/log4j.xml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE log4j:configuration
PUBLIC "-//LOG4J//DTD LOG4J//EN"
"https://logging.apache.org/log4j/1.2/apidocs/org/apache/log4j/xml/doc-files/log4j.dtd" >
<!-- 请在mybatis-config.xml中配置如下设置 -->
<!-- <settings> -->
<!-- <setting -->
<!-- name="logImpl" -->
<!-- value="log4j"/> -->
<!-- </settings> -->
<log4j:configuration>
<appender name="STDOUT" class="org.apache.log4j.ConsoleAppender">
<layout class="org.apache.log4j.PatternLayout">
<param name="ConversionPattern" value="%5p [%t] %m%n" />
</layout>
</appender>
<logger name="mapper">
<level value="DEBUG" />
</logger>
<root>
<level value="ERROR" />
<appender-ref ref="STDOUT" />
</root>
</log4j:configuration>

mybatis-config.xml

/MyAManyToManyTest/src/mybatis-config.xml
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
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<!-- 该配置文件包含对 MyBatis 系统的核心设置 -->
<configuration>
<properties resource="db.properties" />
<settings>
<setting name="logImpl" value="log4j" />
</settings>
<environments default="mysql">
<environment id="mysql">
<transactionManager type="JDBC" />
<dataSource type="pooled">
<property name="driver" value="${driver}" />
<property name="url" value="${url}" />
<property name="username" value="${username}" />
<property name="password" value="${password}" />
</dataSource>
</environment>
</environments>
<mappers>
<mapper class="mapper.UserMapper" />
<mapper class="mapper.OrderMapper" />
<mapper class="mapper.ArticleMapper" />
</mappers>
</configuration>

测试查询用户

UserMapper.java

/MyAManyToManyTest/src/mapper/UserMapper.java
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
package mapper;

import java.util.List;
import org.apache.ibatis.annotations.Many;
import org.apache.ibatis.annotations.Result;
import org.apache.ibatis.annotations.Results;
import org.apache.ibatis.annotations.Select;
import org.apache.ibatis.mapping.FetchType;
import domain.Order;
import domain.User;

public interface UserMapper {
@Select("select * from tb_user where id=#{id}")
@Results({
@Result(column = "id",property = "id", id = true),
@Result(column = "username",property = "username"),
@Result(column = "loginname",property = "loginname"),
@Result(column = "password",property = "password"),
@Result(column = "phone",property = "phone"),
@Result(column = "address",property = "address"),
@Result(column = "id",property = "orders",
many = @Many(select = "selectOrdersByUserId", fetchType = FetchType.LAZY)
)
})
User selectUserById(Integer id);

@Select("select id,code,total from tb_order where user_id=#{id}")
List<Order> selectOrdersByUserId(Integer id);
}

SelectUserByIdTest.java

/MyAManyToManyTest/src/test/SelectUserByIdTest.java
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
package test;
import java.util.List;
import org.apache.ibatis.session.SqlSession;
import domain.Order;
import domain.User;
import fractory.SqlSessionFactoryTools;
import mapper.UserMapper;
public class SelectUserByIdTest {
public static void main(String[] args)
{
SqlSession sqlSession = null;
try
{
// 加载mybatis-config.xml,获取SqlSession实例
sqlSession = SqlSessionFactoryTools.getSqlSession();
// 获取mapper接口代理对象
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
User user = userMapper.selectUserById(1);
System.out.println(" User [id=" + user.getId() + ", username=" + user.getUsername()
+ ", loginname=" + user.getLoginname() + ", password=" + user.getPassword()
+ ", phone=" + user.getPhone() + ", address=" + user.getAddress() + "]");
System.out.println(
"------------------------------------------------------------------------------");
System.out.println("该用户的订单列表:");
List<Order> orders = user.getOrders();
orders.forEach(order -> System.out.println(" " + order));
// 提交事务
sqlSession.commit();
} catch (Exception e)
{
// 出错回滚事务
sqlSession.rollback();
e.printStackTrace();
} finally
{
// 关闭会话
if (sqlSession != null)
sqlSession.close();
}
}
}

运行效果

1
2
3
4
5
6
7
8
9
10
11
DEBUG [main] ==>  Preparing: select * from tb_user where id=? 
DEBUG [main] ==> Parameters: 1(Integer)
DEBUG [main] <== Total: 1
User [id=1, username=小明, loginname=xiaoming, password=xiaoming, phone=123456789123, address=北京]
------------------------------------------------------------------------------
该用户的订单列表:
DEBUG [main] ==> Preparing: select id,code,total from tb_order where user_id=?
DEBUG [main] ==> Parameters: 1(Integer)
DEBUG [main] <== Total: 2
Order [id=1, code=abcseeeahoaugoeijgiej, total=223.33]
Order [id=2, code=sfaofosfhodsfuefie, total=111.22]

测试查询订单

OrderMapper.java

/MyAManyToManyTest/src/mapper/OrderMapper.java
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
package mapper;

import java.util.List;
import org.apache.ibatis.annotations.Many;
import org.apache.ibatis.annotations.One;
import org.apache.ibatis.annotations.Result;
import org.apache.ibatis.annotations.Results;
import org.apache.ibatis.annotations.Select;
import org.apache.ibatis.mapping.FetchType;
import domain.Article;
import domain.Order;
import domain.User;

public interface OrderMapper {
@Select("select * from tb_order where id=#{id}")
@Results({
@Result(column = "id",property = "id", id = true),
@Result(column = "code",property = "code"),
@Result(column = "total",property = "total"),
@Result(column = "user_id",property = "user",
one = @One(select = "selectUserByOrderId", fetchType = FetchType.LAZY)),
@Result(column = "id",property = "articles",
many = @Many(select = "selectArticlesByOrderId", fetchType = FetchType.LAZY)
)
})
Order selectOrderById(Integer id);

@Select("select * from tb_user where id=#{id}")
User selectUserByOrderId(Integer id);

@Select("select id,name,price,remark from tb_article where id in(select article_id from tb_item where order_id=#{id})")
List<Article> selectArticlesByOrderId(Integer id);
}

SelectOrderByIdTest.java

/MyAManyToManyTest/src/test/SelectOrderByIdTest.java
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
package test;
import java.util.List;
import org.apache.ibatis.session.SqlSession;
import domain.Article;
import domain.Order;
import domain.User;
import fractory.SqlSessionFactoryTools;
import mapper.OrderMapper;
public class SelectOrderByIdTest {
public static void main(String[] args)
{
SqlSession sqlSession = null;
try
{
// 加载mybatis-config.xml,获取SqlSession实例
sqlSession = SqlSessionFactoryTools.getSqlSession();
// 获取mapper接口代理对象
OrderMapper orderMapper = sqlSession.getMapper(OrderMapper.class);
Order order = orderMapper.selectOrderById(1);
System.out.println("---------------------------------------------------------------");
System.out.println("Order [id=" + order.getId() + ",code=" + order.getCode() + ",total="
+ order.getTotal() + "]");
System.out.println("---------------------------------------------------------------");
User user = order.getUser();
System.out.println("订单对应的用户:" + user);
System.out.println("---------------------------------------------------------------");
List<Article> articles = order.getArticles();
System.out.println("订单中的商品列表:");
articles.forEach(article -> System.out.println(" " + article));
// 提交事务
sqlSession.commit();
} catch (Exception e)
{
// 出错回滚事务
sqlSession.rollback();
e.printStackTrace();
} finally
{
// 关闭会话
if (sqlSession != null)
sqlSession.close();
}
}
}

运行结果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
DEBUG [main] ==>  Preparing: select * from tb_order where id=? 
DEBUG [main] ==> Parameters: 1(Integer)
DEBUG [main] <== Total: 1
---------------------------------------------------------------
Order [id=1,code=abcseeeahoaugoeijgiej,total=223.33]
---------------------------------------------------------------
DEBUG [main] ==> Preparing: select * from tb_user where id=?
DEBUG [main] ==> Parameters: 1(Integer)
DEBUG [main] <== Total: 1
订单对应的用户:User [id=1, username=小明, loginname=xiaoming, password=xiaoming, phone=123456789123, address=北京]
---------------------------------------------------------------
DEBUG [main] ==> Preparing: select id,name,price,remark from tb_article where id in(select article_id from tb_item where order_id=?)
DEBUG [main] ==> Parameters: 1(Integer)
DEBUG [main] <== Total: 3
订单中的商品列表:
Article [id=1, name=商品1, price=123.12, remark=xxx的伟大著作]
Article [id=2, name=商品2, price=12.3, remark=yyy的伟大著作]
Article [id=3, name=商品3, price=34.22, remark=zzz的著作]

测试查询商品

ArticleMapper.java

/MyAManyToManyTest/src/mapper/ArticleMapper.java
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
package mapper;

import java.util.List;
import org.apache.ibatis.annotations.Many;
import org.apache.ibatis.annotations.Result;
import org.apache.ibatis.annotations.Results;
import org.apache.ibatis.annotations.Select;
import org.apache.ibatis.mapping.FetchType;
import domain.Article;
import domain.Order;

public interface ArticleMapper {
@Select("select * from tb_article where id=#{id}")
@Results({
@Result(column = "id",property = "id", id = true),
@Result(column = "name",property = "name"),
@Result(column = "price",property = "price"),
@Result(column = "remark",property = "remark"),
@Result(column = "id",property = "orders",
many = @Many(select = "selectOrderByArticleId", fetchType = FetchType.LAZY)
)
})
Article selectArticleById(Integer id);

@Select("select * from tb_order where id in(select order_id from tb_item where article_id=#{id})")
@Results({
@Result(column = "id",property = "id", id = true),
@Result(column = "code",property = "code"),
@Result(column = "total",property = "total")
})
List<Order> selectOrderByArticleId(Integer id);
}

SelectArticleByIdTest.java

/MyAManyToManyTest/src/test/SelectArticleByIdTest.java
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
package test;
import java.util.List;
import org.apache.ibatis.session.SqlSession;
import domain.Article;
import domain.Order;
import fractory.SqlSessionFactoryTools;
import mapper.ArticleMapper;
public class SelectArticleByIdTest {
public static void main(String[] args)
{
SqlSession sqlSession = null;
try
{
// 加载mybatis-config.xml,获取SqlSession实例
sqlSession = SqlSessionFactoryTools.getSqlSession();
// 获取mapper接口代理对象
ArticleMapper articleMapper = sqlSession.getMapper(ArticleMapper.class);
Article article = articleMapper.selectArticleById(1);
System.out.println("Article [id=" + article.getId() + ", name=" + article.getName()
+ ", price=" + article.getPrice() + ", remark=" + article.getRemark() + "]");
System.out.println("---------------------------------------------------");
System.out.println("包含该商品的所有订单:");
List<Order> orders = article.getOrders();
orders.forEach(order -> System.out.println(" " + order));
// 提交事务
sqlSession.commit();
} catch (Exception e)
{
// 出错回滚事务
sqlSession.rollback();
e.printStackTrace();
} finally
{
// 关闭会话
if (sqlSession != null)
sqlSession.close();
}
}
}

运行结果

1
2
3
4
5
6
7
8
9
10
11
DEBUG [main] ==>  Preparing: select * from tb_article where id=? 
DEBUG [main] ==> Parameters: 1(Integer)
DEBUG [main] <== Total: 1
Article [id=1, name=商品1, price=123.12, remark=xxx的伟大著作]
---------------------------------------------------
包含该商品的所有订单:
DEBUG [main] ==> Preparing: select * from tb_order where id in(select order_id from tb_item where article_id=?)
DEBUG [main] ==> Parameters: 1(Integer)
DEBUG [main] <== Total: 2
Order [id=1, code=abcseeeahoaugoeijgiej, total=223.33]
Order [id=2, code=sfaofosfhodsfuefie, total=111.22]

11.2 注解的使用示例3 一对多关联关系

例如班级和学生是一对多的关联关系,则在查询学生的时候使用@one注解来关联查询班级。
查询班级的时候,使用@Many注解来查询该班级的所有学生.示例项目如下所示:

示例:一对多关联关系

项目结构

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
G:\workspace_web2\MyAOneToManyTest
├─src
│ ├─db.properties
│ ├─domain
│ │ ├─Clazz.java
│ │ └─Student.java
│ ├─fractory
│ │ └─SqlSessionFactoryTools.java
│ ├─log4j.xml
│ ├─mapper
│ │ ├─ClazzMapper.java
│ │ └─StudentMapper.java
│ ├─mybatis-config.xml
│ ├─tb_clazz_tb_student.sql
│ └─test
│ ├─SelectClazzByIdTest.java
│ └─SelectStudentByIdTest.java
└─WebContent
├─META-INF
│ └─MANIFEST.MF
└─WEB-INF
└─lib
├─ant-1.9.6.jar
├─ant-launcher-1.9.6.jar
├─asm-5.2.jar
├─cglib-3.2.5.jar
├─commons-logging-1.2.jar
├─javassist-3.22.0-CR2.jar
├─log4j-1.2.17.jar
├─log4j-api-2.3.jar
├─log4j-core-2.3.jar
├─mybatis-3.4.5.jar
├─mysql-connector-java-5.1.44-bin.jar
├─ognl-3.1.15.jar
├─slf4j-api-1.7.25.jar
└─slf4j-log4j12-1.7.25.jar

数据库脚本

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
-- 创建tb_clazz表
DROP TABLE IF EXISTS `tb_clazz`;
CREATE TABLE `tb_clazz` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`code` varchar(18) NOT NULL,
`name` varchar(18) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- 插入数据到tb_clazz表
INSERT INTO `tb_clazz` VALUES ('1', 'B151516', 'Java基础班');
DROP TABLE IF EXISTS `tb_student`;
CREATE TABLE `tb_student` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(18) DEFAULT NULL,
`sex` varchar(18) DEFAULT NULL,
`age` int(11) DEFAULT NULL,
`clazz_id` int(11) DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `clazz_id` (`clazz_id`),
CONSTRAINT `tb_student_ibfk_1` FOREIGN KEY (`clazz_id`)
REFERENCES `tb_clazz` (`id`) ON DELETE CASCADE ON UPDATE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
INSERT INTO `tb_student` VALUES ('1', '小明', '男', '24', '1');
INSERT INTO `tb_student` VALUES ('2', '小王', '男', '23', '1');
INSERT INTO `tb_student` VALUES ('3', '小芳', '女', '22', '1');
INSERT INTO `tb_student` VALUES ('4', '小丽', '女', '22', '1');

db.properties

1
2
3
4
5
6
# 保存为db.properties文件,然后在mybatis-config.xml中通过下面标签引入:
# <properties resource="db.properties"/>
driver=com.mysql.jdbc.Driver
url=jdbc:mysql://localhost:3306/mybatis
username=root
password=root

log4j.xml

/MyAOneToManyTest/src/log4j.xml
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
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE log4j:configuration
PUBLIC "-//LOG4J//DTD LOG4J//EN"
"https://logging.apache.org/log4j/1.2/apidocs/org/apache/log4j/xml/doc-files/log4j.dtd" >
<!-- 请在mybatis-config.xml中配置如下设置 -->
<!-- <settings> -->
<!-- <setting -->
<!-- name="logImpl" -->
<!-- value="log4j"/> -->
<!-- </settings> -->
<log4j:configuration>
<appender
name="STDOUT"
class="org.apache.log4j.ConsoleAppender">
<layout class="org.apache.log4j.PatternLayout">
<param
name="ConversionPattern"
value="%5p [%t] %m%n"/>
</layout>
</appender>
<logger name="mapper">
<level value="DEBUG"/>
</logger>
<root>
<level value="ERROR"/>
<appender-ref ref="STDOUT"/>
</root>
</log4j:configuration>

mybatis-config.xml

/MyAOneToManyTest/src/mybatis-config.xml
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
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<!-- 该配置文件包含对 MyBatis 系统的核心设置 -->
<configuration>
<properties resource="db.properties"/>
<settings>
<setting
name="logImpl"
value="log4j"/>
<!-- 要使延迟加载生效必须配置下面两个属性 -->
<setting
name="lazyLoadingEnabled"
value="true"/>
<setting
name="aggressiveLazyLoading"
value="false"/>
</settings>
<environments default="mysql">
<environment id="mysql">
<transactionManager type="JDBC"/>
<dataSource type="pooled">
<property
name="driver"
value="${driver}"/>
<property
name="url"
value="${url}"/>
<property
name="username"
value="${username}"/>
<property
name="password"
value="${password}"/>
</dataSource>
</environment>
</environments>
<mappers>
<mapper class="mapper.ClazzMapper"/>
<mapper class="mapper.StudentMapper"/>
</mappers>
</configuration>

持久化对象

Clazz.java

/MyAOneToManyTest/src/domain/Clazz.java
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
47
48
49
50
51
52
53
54
55
56
57
58
package domain;

import java.io.Serializable;
import java.util.List;

public class Clazz implements Serializable {
private static final long serialVersionUID = -1924365559257770561L;
private Integer id;
private String code;
private String name;
List<Student> students;

public Clazz() {
}

public Clazz(String code, String name) {
super();
this.code = code;
this.name = name;
}

public Integer getId() {
return id;
}

public void setId(Integer id) {
this.id = id;
}

public String getCode() {
return code;
}

public void setCode(String code) {
this.code = code;
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public List<Student> getStudents() {
return students;
}

public void setStudents(List<Student> students) {
this.students = students;
}

@Override
public String toString() {
return "Clazz [id=" + id + ", code=" + code + ", name=" + name + "]";
}
}

Student.java

/MyAOneToManyTest/src/domain/Student.java
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
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
package domain;

import java.io.Serializable;

public class Student implements Serializable {
private static final long serialVersionUID = 2713166808172094576L;
private Integer id;
private String name;
private String sex;
private Integer age;
private Clazz clazz;

public Student() {
}

public Student(String name, String sex, Integer age) {
super();
this.name = name;
this.sex = sex;
this.age = age;
}

public Integer getId() {
return id;
}

public void setId(Integer id) {
this.id = id;
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public String getSex() {
return sex;
}

public void setSex(String sex) {
this.sex = sex;
}

public Integer getAge() {
return age;
}

public void setAge(Integer age) {
this.age = age;
}

public Clazz getClazz() {
return clazz;
}

public void setClazz(Clazz clazz) {
this.clazz = clazz;
}

@Override
public String toString() {
return "Student [id=" + id + ", name=" + name + ", sex=" + sex + ", age=" + age + "]";
}
}

会话工具类

/MyAOneToManyTest/src/fractory/SqlSessionFactoryTools.java
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
package fractory;
import java.io.IOException;
import java.io.InputStream;
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;
public class SqlSessionFactoryTools {
private static SqlSessionFactory sqlSessionFactory = null;
static
{
try
{
InputStream mybatisConfigXML = Resources.getResourceAsStream("mybatis-config.xml");
sqlSessionFactory = new SqlSessionFactoryBuilder().build(mybatisConfigXML);
} catch (IOException e)
{
e.printStackTrace();
}
}
public static SqlSession getSqlSession()
{
return sqlSessionFactory.openSession();
}
}

mapper接口

ClazzMapper.java

/MyAOneToManyTest/src/mapper/ClazzMapper.java
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
package mapper;

import org.apache.ibatis.annotations.Many;
import org.apache.ibatis.annotations.Result;
import org.apache.ibatis.annotations.Results;
import org.apache.ibatis.annotations.Select;
import org.apache.ibatis.mapping.FetchType;
import domain.Clazz;

public interface ClazzMapper {
@Select("select * from tb_clazz where id=#{id}")
@Results({
@Result(column = "id",property = "id", id = true),
@Result(column = "code",property = "code"),
@Result(column = "name",property = "name"),
@Result(column = "id",property = "students",
many = @Many(select = "mapper.StudentMapper.selectStudentsByClassId", fetchType = FetchType.LAZY)
)
})
Clazz selectClazzById(Integer id);

@Select("select * from tb_clazz where id=#{id}")
@Results({
@Result(column = "id",property = "id", id = true),
@Result(column = "code",property = "code"),
@Result(column = "name",property = "name")
})
Clazz selectClazzByUserClazzId(Integer id);
}

StudentMapper.java

/MyAOneToManyTest/src/mapper/StudentMapper.java
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
package mapper;

import java.util.List;
import org.apache.ibatis.annotations.One;
import org.apache.ibatis.annotations.Result;
import org.apache.ibatis.annotations.Results;
import org.apache.ibatis.annotations.Select;
import org.apache.ibatis.mapping.FetchType;
import domain.Student;

public interface StudentMapper {
@Select("select * from tb_student where id=#{id}")
@Results({
@Result(column = "id",property = "id", id = true),
@Result(column = "name",property = "name"),
@Result(column = "sex",property = "sex"),
@Result(column = "age",property = "age"),
@Result(column = "clazz_id",property = "clazz",
one = @One(select = "mapper.ClazzMapper.selectClazzByUserClazzId", fetchType = FetchType.EAGER)
)
})
Student selectStudentById(Integer id);

@Select("select * from tb_student where clazz_id=#{clazzId}")
@Results({
@Result(column = "id",property = "id", id = true),
@Result(column = "name",property = "name"),
@Result(column = "sex",property = "sex"),
@Result(column = "age",property = "age")
})
List<Student> selectStudentsByClassId(Integer clazzId);
}

fetchType = FetchType.EAGER表示立即加载。

测试类

SelectClazzByIdTest.java

/MyAOneToManyTest/src/test/SelectClazzByIdTest.java
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
package test;

import java.util.List;
import org.apache.ibatis.session.SqlSession;
import domain.Clazz;
import domain.Student;
import fractory.SqlSessionFactoryTools;
import mapper.ClazzMapper;

public class SelectClazzByIdTest {
public static void main(String[] args) {
SqlSession sqlSession = null;
try {
// 加载mybatis-config.xml,获取SqlSession实例
sqlSession = SqlSessionFactoryTools.getSqlSession();
// 获取mapper接口代理对象
ClazzMapper clazzMapper = sqlSession.getMapper(ClazzMapper.class);
Clazz clazz = clazzMapper.selectClazzById(1);
System.out.println(clazz.toStringSimple());
// System.out.println(clazz.toString());
System.out.println("--------------------------------------------------------------");
List<Student> students = clazz.getStudents();
students.forEach(student -> System.out.println(student));
// 提交事务
sqlSession.commit();
} catch (Exception e) {
// 出错回滚事务
sqlSession.rollback();
e.printStackTrace();
} finally {
// 关闭会话
if (sqlSession != null)
sqlSession.close();
}

}
}

运行效果:

1
2
3
4
5
6
7
8
9
10
DEBUG [main] ==>  Preparing: select * from tb_clazz where id=? 
DEBUG [main] ==> Parameters: 1(Integer)
DEBUG [main] <== Total: 1
Clazz [id=1, code=B100, name=Java基础班]
--------------------------------------------------------------
DEBUG [main] ==> Preparing: select * from tb_student where clazz_id=?
DEBUG [main] ==> Parameters: 1(Integer)
DEBUG [main] <== Total: 2
Student [id=2, name=小王, sex=男, age=23]
Student [id=4, name=小丽, sex=女, age=22]

SelectStudentByIdTest.java

/MyAOneToManyTest/src/test/SelectStudentByIdTest.java
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
package test;

import org.apache.ibatis.session.SqlSession;
import domain.Student;
import fractory.SqlSessionFactoryTools;
import mapper.StudentMapper;

public class SelectStudentByIdTest {
public static void main(String[] args) {
SqlSession sqlSession = null;
try {
// 加载mybatis-config.xml,获取SqlSession实例
sqlSession = SqlSessionFactoryTools.getSqlSession();
// 获取mapper接口代理对象
StudentMapper studentMapper = sqlSession.getMapper(StudentMapper.class);
// 执行mapper接口方法
Student student = studentMapper.selectStudentById(1);
// System.out.println(student.toString());
System.out.println(student.toStringSimple());
System.out.println("===========================================");
System.out.println(student.getClazz().toString());
// 提交事务
sqlSession.commit();
} catch (Exception e) {
// 出错回滚事务
sqlSession.rollback();
e.printStackTrace();
} finally {
// 关闭会话
if (sqlSession != null)
sqlSession.close();
}

}
}

运行效果:

1
2
3
4
5
6
7
8
9
DEBUG [main] ==>  Preparing: select * from tb_student where id=? 
DEBUG [main] ==> Parameters: 1(Integer)
DEBUG [main] ====> Preparing: select * from tb_clazz where id=?
DEBUG [main] ====> Parameters: 2(Integer)
DEBUG [main] <==== Total: 1
DEBUG [main] <== Total: 1
Student [id=1, name=小明, sex=男, age=24]
===========================================
Clazz [id=2, code=B101, name=JSP]

11.2 注解的使用示例2 一对一关联关系

目录结构

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
G:\workspace_web2\MyAOneToOneTest
├─src
│ ├─db.properties
│ ├─domain
│ │ ├─Card.java
│ │ └─Person.java
│ ├─fractory
│ │ └─SqlSessionFratoryTools.java
│ ├─log4j.xml
│ ├─mapper
│ │ ├─CardMapper.java
│ │ └─PersonMapper.java
│ ├─mybatis-config.xml
│ ├─mybatis_tb_card_tb_person.sql
│ └─test
│ └─OneToOneTest.java
└─WebContent
├─META-INF
└─WEB-INF
└─lib
├─ant-1.9.6.jar
├─ant-launcher-1.9.6.jar
├─asm-5.2.jar
├─cglib-3.2.5.jar
├─commons-logging-1.2.jar
├─javassist-3.22.0-CR2.jar
├─log4j-1.2.17.jar
├─log4j-api-2.3.jar
├─log4j-core-2.3.jar
├─mybatis-3.4.5.jar
├─mysql-connector-java-5.1.44-bin.jar
├─ognl-3.1.15.jar
├─slf4j-api-1.7.25.jar
└─slf4j-log4j12-1.7.25.jar

创建数据库表

首先,给之前创建的mybatis数据库创建两个表tb_cardtb_person,并插入测试数据。SQL脚本如下使用方式:
保存成.sql文件,然后使用Navicat导入(设置编码格式utf-8,免得乱码)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
-- 创建tb_card表
DROP TABLE IF EXISTS `tb_card`;
CREATE TABLE `tb_card` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`code` varchar(18) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- 插入一条数据
INSERT INTO `tb_card` VALUES ('1', '43280119980091');
-- 创建tb_person表
DROP TABLE IF EXISTS `tb_person`;
CREATE TABLE `tb_person` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(18) NOT NULL,
`sex` varchar(18) NOT NULL,
`age` int(11) DEFAULT NULL,
`card_id` int(11) DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `card_id` (`card_id`),
CONSTRAINT `tb_person_ibfk_1` FOREIGN KEY (`card_id`)
REFERENCES `tb_card` (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- 插入一条数据
INSERT INTO `tb_person` VALUES ('1', '小明', '男', '22', '1');

创建持久化类

Card.java

/MyAOneToOneTest/src/domain/Card.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package domain;
public class Card {
private Integer id;
private String code;
public Card()
{
// TODO Auto-generated constructor stub
}
public Card(Integer id, String code)
{
this.id = id;
this.code = code;
}
// 此处省略getter和setter方法,请自己补上
@Override
public String toString()
{
return "Card [id=" + id + ", code=" + code + "]";
}
}

Person.java

/MyAOneToOneTest/src/domain/Person.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package domain;
public class Person {
private Integer id;
private String name;
private String sex;
private Integer age;
private Card card;
public Person()
{
super();
}
// 此处省略getter和setter方法,请自己补上
@Override
public String toString()
{
return "Person [id=" + id + ", name=" + name + ", sex=" + sex + ", age=" + age + ", card="
+ card + "]";
}
}

mapper接口

CardMapper.java

/MyAOneToOneTest/src/mapper/CardMapper.java
1
2
3
4
5
6
7
8
9
package mapper;

import org.apache.ibatis.annotations.Select;
import domain.Card;

public interface CardMapper {
@Select("select * from tb_card where id=#{id}")
Card selectCardById(Integer id);
}

PersonMapper.java

/MyAOneToOneTest/src/mapper/PersonMapper.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package mapper;

import org.apache.ibatis.annotations.One;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Result;
import org.apache.ibatis.annotations.Results;
import org.apache.ibatis.annotations.Select;
import org.apache.ibatis.mapping.FetchType;
import domain.Person;

public interface PersonMapper {
@Select("select * from tb_person where id=#{id}")
@Results({
@Result(column = "id",property = "id", id = true),
@Result(column = "name",property = "name"),
@Result(column = "sex",property = "sex"),
@Result(column = "age",property = "age"),
@Result(column = "card_id",property = "card",
one = @One(select = "mapper.CardMapper.selectCardById", fetchType = FetchType.LAZY)
)
})
Person selectPersonById(@Param("id") Integer id);
}

selectPersonById方法使用了@Select注解,其根据id查询对应的Person数据。因为需要将Person对应的Card数据也查询出来,所以PersonCard属性使用了一个@Result结果映射。column="card_id",property="card"表示PersonCard属性对应tb_person表的card_id列,one属性表示是一个一对一关联关系,
@one注解的select属性表示需要关联执行的SQL语句,
fetchType表示查询的类型是立即加载(EAGER)还是懒加载(LAZY)。

测试类

/MyAOneToOneTest/src/test/OneToOneTest.java
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
47
48
49
50
51
52
53
package test;

import org.apache.ibatis.session.SqlSession;
import domain.Person;
import fractory.SqlSessionFactoryTools;
import mapper.PersonMapper;

public class OneToOneTest {
public static void main(String[] args) {
SqlSession sqlSession = null;
try {
sqlSession = SqlSessionFactoryTools.getSqlSession();
PersonMapper personMapper = sqlSession.getMapper(PersonMapper.class);
// 执行查询
Person person = personMapper.selectPersonById(1);
// 设置懒加载,立即加载Card
testLazyUseCardNow(person);
// 设置懒加载,不立即加载Card
// testLazyUseCardLazy(person);
// 提交事务
sqlSession.commit();
} catch (Exception e) {
// 出错回滚事务
sqlSession.rollback();
e.printStackTrace();
} finally {
// 关闭会话
if (sqlSession != null)
sqlSession.close();
}
}

/**
* 当设置为懒加载时,立即访问card成员变量的情况。
*
* @param person
*/
private static void testLazyUseCardNow(Person person) {
// 调用toString方法会立即加载Card
System.out.println(person.toString());
}

/**
* 当设置为懒加载时,不立即访问card成员变量的。
*
* @param person 查询数据库得到的对象
*/
private static void testLazyUseCardLazy(Person person) {
System.out.println(person.toStringSimple());
System.out.println("=================================");
System.out.println(person.getCard());
}
}

运行效果

运行OneToOneTest类的main方法,该方法通过SqlSessiongetMapper(Class<T> type)方法获得mapper接口的代理对象PersonMapper。调用PersonMapperselectPersonById方法时会执行该方法上的注解。需要注意的是,Person的一对一关联使用的注解@oneselect属性,要执行的SQL语句在CardMapper接口的selectCardById方法的注解中。控制台显示如下所示:

1
2
3
4
5
6
7
DEBUG [main] ==>  Preparing: select * from tb_person where id=? 
DEBUG [main] ==> Parameters: 1(Integer)
DEBUG [main] <== Total: 1
DEBUG [main] ==> Preparing: select * from tb_card where id=?
DEBUG [main] ==> Parameters: 1(Integer)
DEBUG [main] <== Total: 1
Person [id=1, name=小明, sex=男, age=22, card=Card [id=1, code=450101199701243862]]

可以看到,查询Person信息时Person对应的Card对象也被查询出来了。
在上面PersonMapper接口中的设置的是懒加载。
由于Person类的toString方法里面有访问到card属性,所以会执行关联查询。

懒加载验证

如果把上面的testLazyUseCardNow(person);语句注释掉,打开testLazyUseCardLazy(person);语句。如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public static void main(String[] args) {
SqlSession sqlSession = null;
try {
sqlSession = SqlSessionFactoryTools.getSqlSession();
PersonMapper personMapper = sqlSession.getMapper(PersonMapper.class);
// 执行查询
Person person = personMapper.selectPersonById(1);
// 设置懒加载,立即加载Card
// testLazyUseCardNow(person);
// 设置懒加载,不立即加载Card
testLazyUseCardLazy(person);
// 提交事务
sqlSession.commit();
} catch (Exception e) {
// 出错回滚事务
sqlSession.rollback();
e.printStackTrace();
} finally {
// 关闭会话
if (sqlSession != null)
sqlSession.close();
}
}

则先执行一条SQL语句,取出Person,当需要访问Person类的card成员时,才会执行下一条SQL语句。

运行结果如下:

1
2
3
4
5
6
7
8
9
DEBUG [main] ==>  Preparing: select * from tb_person where id=? 
DEBUG [main] ==> Parameters: 1(Integer)
DEBUG [main] <== Total: 1
Person [id=1, name=小明, sex=男, age=22]
=================================
DEBUG [main] ==> Preparing: select * from tb_card where id=?
DEBUG [main] ==> Parameters: 1(Integer)
DEBUG [main] <== Total: 1
Card [id=1, code=450101199701243862]

这就是懒加载的效果。

立即加载验证

再PersonMapper接口中使用立即加载

public interface PersonMapper {
    @Select("select * from tb_person where id=#{id}")
    @Results({
        @Result(column = "id",property = "id", id = true),
        @Result(column = "name",property = "name"),
        @Result(column = "sex",property = "sex"),
        @Result(column = "age",property = "age"),
        @Result(column = "card_id",property = "card", 
            one = @One(select = "mapper.CardMapper.selectCardById", fetchType = FetchType.EAGER)
        ) 
    })
    Person selectPersonById(@Param("id") Integer id);
}

再次运行上面的测试类,运行结果如下:

1
2
3
4
5
6
7
8
9
DEBUG [main] ==>  Preparing: select * from tb_person where id=? 
DEBUG [main] ==> Parameters: 1(Integer)
DEBUG [main] ====> Preparing: select * from tb_card where id=?
DEBUG [main] ====> Parameters: 1(Integer)
DEBUG [main] <==== Total: 1
DEBUG [main] <== Total: 1
Person [id=1, name=小明, sex=男, age=22]
=================================
Card [id=1, code=450101199701243862]

可以看到在立即加载模式下,就算还没有访问Person类的card属性,也会执行关联查询,加载card内容。