1、MyBatis介绍
MyBatis是一款优秀的持久层框架,它支持定制化SQL、存储过程以及高级映射。MyBatis避免了几乎所有的 JDBC 代码和手动设置参数以及获取结果集。MyBatis可以使用简单的XML或注解来配置和映射原生信息,将接口和Java的 POJOs(Plain Ordinary Java Object,普通的Java对象)映射成数据库中的记录。
2、MyBatis入门示例
2.1 MySQL数据库准备
Mysql安装后,通过如下命令用root用户连接mysql:
mysql -u root -p
然后,通过如下SQL语句创建一个名为mybatis的数据库,创建一个名为pets的数据库表,然后,插入两条记录。
create database mybatis;
use mybatis;
CREATE TABLE pets(id INT PRIMARY KEY AUTO_INCREMENT, NAME VARCHAR(20), age INT);
INSERT INTO pets(NAME, age) VALUES('Qiuqiu', 1);
INSERT INTO pets(NAME, age) VALUES('Paoao', 3);
效果如下:
mysql> create database mybatis;
Query OK, 1 row affected (0.01 sec)
mysql> use mybatis;
Database changed
mysql> CREATE TABLE pets(id INT PRIMARY KEY AUTO_INCREMENT, NAME VARCHAR(20), age INT);
Query OK, 0 rows affected (0.01 sec)
mysql> INSERT INTO pets(NAME, age) VALUES('Qiuqiu', 1);
Query OK, 1 row affected (0.00 sec)
mysql> INSERT INTO pets(NAME, age) VALUES('Paoao', 3);
Query OK, 1 row affected (0.00 sec)
mysql> commit;
Query OK, 0 rows affected (0.00 sec)
mysql>
2.2 MyBatis简单示例
2.2.1 示例1_非代理模式
新建一个Maven工程,结构如下图:
示例1_非代理模式
其中,代码如下。
pom.xml:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.lfqy</groupId>
<artifactId>mybatistest</artifactId>
<version>1.0-SNAPSHOT</version>
<dependencies>
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.4.5</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.6</version>
</dependency>
</dependencies>
</project>
com.lfqy.domain.Pet:
package com.lfqy.domain;
/**
* Created by chengxia on 2019/9/9.
*/
public class Pet {
//实体类的属性和表的字段名称一一对应
private int id;
private String name;
private int age;
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
@Override
public String toString() {
return "Pet [id=" + id + ", name=" + name + ", age=" + age + "]";
}
}
com/lfqy/mapping/PetMapper.xml:
<?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,namespace的值习惯上设置成包名+sql映射文件名,这样就能够保证namespace的值是唯一的
例如namespace="com.lfqy.mapping.PetMapper"就是com.lfqy.mapping(包名)+PetMapper(PetMapper.xml文件去除后缀)
-->
<mapper namespace="com.lfqy.mapping.PetMapper">
<!-- 在select标签中编写查询的SQL语句, 设置select标签的id属性为getPet,id属性值必须是唯一的,不能够重复
使用parameterType属性指明查询时使用的参数类型,resultType属性指明查询返回的结果集类型
resultType="com.lfqy.domain.Pet"就表示将查询结果封装成一个Pet类的对象返回
Pet类就是pets表所对应的实体类
-->
<!--
根据id查询得到一个pet对象
-->
<select id="getPet" parameterType="int"
resultType="com.lfqy.domain.Pet">
select * from pets where id=#{id}
</select>
</mapper>
Conf.xml:
<?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">
<configuration>
<environments default="development">
<environment id="development">
<transactionManager type="JDBC" />
<!-- 配置数据库连接信息 -->
<dataSource type="POOLED">
<property name="driver" value="com.mysql.jdbc.Driver" />
<property name="url" value="jdbc:mysql://localhost:3306/mybatis?characterEncoding=utf8" /><!-- 这里要显式指定编码,不然会报错 -->
<property name="username" value="root" />
<property name="password" value="paopao666" />
</dataSource>
</environment>
</environments>
<!--指定mapper所在的文件位置,注意我们不用写dao层的实现类,约束大于配置,dao层中接口名和resource下的xml名字要一致,这样打包
过后才能在一个文件夹下, 才能找到对应的,不用写实现类-->
<mappers>
<!--注意是以 "/" 为分隔符 基于xml得配置-->
<mapper resource="com/lfqy/mapping/PetMapper.xml"></mapper>
</mappers>
</configuration>
这样,写一个如下的测试类,测试上面的程序是否正确。
com.lfqy.domain.PetTest:
package com.lfqy.domain;
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import java.io.InputStream;
import java.util.List;
import static org.junit.Assert.*;
/**
* Created by chengxia on 2019/9/9.
*/
public class PetTest {
private InputStream in;
private SqlSessionFactory factory;
SqlSession session;
@Before//用于在测试方法执行之前先执行
public void init() throws Exception {
//1.读取主配置文件
in = Resources.getResourceAsStream("Conf.xml");//参数就是配置文件的路径和文件名
//2.创建SqlSessionFactory对象
factory = new SqlSessionFactoryBuilder().build(in);
//3.获得一个sqlSession对象
session = factory.openSession();
}
@After//在测试方法执行之后执行
public void destroy() throws Exception {
in.close();
session.close();
}
/**
* 最终的实现结果是:查询id为1的宠物
* @throws Exception
*/
@Test
public void testMyBatis() throws Exception {
String statement = "com.lfqy.mapping.PetMapper.getPet";//映射sql的标识字符串
//执行查询返回一个唯一user对象的sql
Pet pet = session.selectOne(statement, 1);
System.out.println(pet);
}
}
上面的测试代码运行结果如下:
log4j:WARN No appenders could be found for logger (org.apache.ibatis.logging.LogFactory).
log4j:WARN Please initialize the log4j system properly.
Pet [id=1, name=Qiuqiu, age=1]
Process finished with exit code 0
注意,如果执行时报错,可能是MySQL默认不能直接用root连接,在MySQL中执行如下语句即可解决:
ALTER USER 'root'@'localhost' IDENTIFIED WITH mysql_native_password BY 'password';
flush privileges;
Where root as your user localhost as your URL and password as your password
From:参考连接2
2.2.2 示例2_MyBatis代理模式
基于前面的例子,新增一个代理模式用到的mapper类,如下图:
示例2_MyBatis代理模式
com.lfqy.mapping.PetMapper:
package com.lfqy.mapping;
import com.lfqy.domain.Pet;
/**
* Created by chengxia on 2019/9/10.
*/
public interface PetMapper {
Pet getPet(Integer petId);
}
需要注意的是这个mapper类所在的包名和前面mapper配置文件的路径要一致,否则会有问题。
这样,再写一个测试类,如下。
com.lfqy.domain.PetProxyTest:
package com.lfqy.domain;
import com.lfqy.mapping.PetMapper;
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import java.io.InputStream;
/**
* Created by chengxia on 2019/9/9.
*/
public class PetProxyTest {
private InputStream in;
private SqlSessionFactory factory;
SqlSession session;
PetMapper petMapper;
@Before//用于在测试方法执行之前先执行
public void init() throws Exception {
//1.读取主配置文件
in = Resources.getResourceAsStream("Conf.xml");//参数就是配置文件的路径和文件名
//2.创建SqlSessionFactory对象
factory = new SqlSessionFactoryBuilder().build(in);
//3.获得一个sqlSession对象
session = factory.openSession();
//4.获得一个PetMapper代理对象
petMapper = session.getMapper(PetMapper.class);
}
@After//在测试方法执行之后执行
public void destroy() throws Exception {
in.close();
session.close();
}
/**
* 最终的实现结果是:查询id为1的宠物
* @throws Exception
*/
@Test
public void testMyBatis() throws Exception {
//执行前面得到的动态代理对象Mapper对象的getPet方法
Pet pet = petMapper.getPet(1);
System.out.println(pet);
}
}
运行结果如下:
log4j:WARN No appenders could be found for logger (org.apache.ibatis.logging.LogFactory).
log4j:WARN Please initialize the log4j system properly.
Pet [id=1, name=Qiuqiu, age=1]
Process finished with exit code 0
2.2.3 示例3_MyBatis代理模式注解实现
前面的例子中,MyBatis的例子是基于XML配置文件的,需要配置一个和mapper接口在同样相对路径下的xml配置文件(mapper配置文件)。除了这种,MyBatis还支持通过注解配置SQL语句和mapper方法之间的映射关系。
如下是在前面例子基础上,写完注解示例之后的目录结构。
示例3_MyBatis代理模式注解实现
从上面可以看出,首先新增了一个mapper接口定义,在其中通过注解说明了接口方法和对应的SQL语句的映射关系。
com.lfqy.mapping.PetAnnotationMapper:
package com.lfqy.mapping;
import com.lfqy.domain.Pet;
import org.apache.ibatis.annotations.Select;
/**
* Created by chengxia on 2019/9/10.
*/
public interface PetAnnotationMapper {
@Select("select * from pets where id=#{id}")
Pet getPet(Integer petId);
}
接下来,基于注解的方式虽然不需要配置mapper对应的xml配置文件,但是需要在MyBatis的配置文件中,标记mapper是基于注解实现(和映射mapper配置文件在一个地方)。修改后的配置文件如下。
<?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">
<configuration>
<environments default="development">
<environment id="development">
<transactionManager type="JDBC" />
<!-- 配置数据库连接信息 -->
<dataSource type="POOLED">
<property name="driver" value="com.mysql.jdbc.Driver" />
<property name="url" value="jdbc:mysql://localhost:3306/mybatis?characterEncoding=utf8" /><!-- 这里要显式指定编码,不然会报错 -->
<property name="username" value="root" />
<property name="password" value="paopao666" />
</dataSource>
</environment>
</environments>
<!--指定mapper所在的文件位置,注意我们不用写dao层的实现类,约束大于配置,dao层中接口名和resource下的xml名字要一致,这样打包
过后才能在一个文件夹下, 才能找到对应的,不用写实现类-->
<mappers>
<!--注意是以 "/" 为分隔符 基于xml得配置-->
<mapper resource="com/lfqy/mapping/PetMapper.xml"></mapper>
<!--基于注解的配置-->
<mapper class="com.lfqy.mapping.PetAnnotationMapper"></mapper>
</mappers>
</configuration>
最后新增一个测试类来测试基于注解的配置是否生效。
com.lfqy.domain.PetProxyAnnotationTest:
package com.lfqy.domain;
import com.lfqy.mapping.PetAnnotationMapper;
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import java.io.InputStream;
/**
* Created by chengxia on 2019/9/9.
*/
public class PetProxyAnnotationTest {
private InputStream in;
private SqlSessionFactory factory;
SqlSession session;
PetAnnotationMapper petMapper;
@Before//用于在测试方法执行之前先执行
public void init() throws Exception {
//1.读取主配置文件
in = Resources.getResourceAsStream("Conf.xml");//参数就是配置文件的路径和文件名
//2.创建SqlSessionFactory对象
factory = new SqlSessionFactoryBuilder().build(in);
//3.获得一个sqlSession对象
session = factory.openSession();
//4.获得一个PetMapper代理对象
petMapper = session.getMapper(PetAnnotationMapper.class);
}
@After//在测试方法执行之后执行
public void destroy() throws Exception {
in.close();
session.close();
}
/**
* 最终的实现结果是:查询id为1的宠物
* @throws Exception
*/
@Test
public void testMyBatis() throws Exception {
//执行前面得到的动态代理对象Mapper对象的getPet方法
Pet pet = petMapper.getPet(1);
System.out.println(pet);
}
}
运行结果如下:
log4j:WARN No appenders could be found for logger (org.apache.ibatis.logging.LogFactory).
log4j:WARN Please initialize the log4j system properly.
Pet [id=1, name=Qiuqiu, age=1]
Process finished with exit code 0
这里可以看出MyBatis配置文件中,原来基于xml配置文件的mapper配置并没有去掉。这时候如果运行实例2中的测试类,发现也能够正常运行。这说明,MyBatis应该是支持注解和配置文件混用的,只不过一般大家用配置文件的方式更为普遍。
2.2.4 实例4_将数据库的配置放到单独的配置文件
在实际使用MyBatis时,我们通常将MyBatis数据库相关的配置放到单独的配置文件,然后在MyBatis的配置文件中,通过el表达式引用其中的内容。如下介绍。
实例4_将数据库的配置放到单独的配置文件
我们首先将数据库相关的配置放到单独的配置文件。
jdbcConfig.properties:
jdbc.driver=com.mysql.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/mybatis?characterEncoding=utf8
jdbc.username=root
jdbc.password=paopao666
修改MyBatis配置文件,引用上面的数据库配置。
Conf.xml:
<?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">
<configuration>
<!-- properties标签:
它的子标签:property:可以配置一些key=value(键值对的内容)
在获取时,使用${key}来获取value
我们在使用时,通常会通过这种方式定义外部配置文件,而不是在Conf.xml中直接定义
引入外部文件,需要使用对应的属性,resource,它是按照包的结构指定配置文件的位置。例如:类的根路径:直接写配置文件名称。
-->
<properties resource="jdbcConfig.properties"/>
<environments default="development">
<environment id="development">
<transactionManager type="JDBC" />
<!-- 配置数据库连接信息 -->
<dataSource type="POOLED">
<property name="driver" value="${jdbc.driver}"/><!-- mybatis的el表达式 expression language 表达式语言 -->
<property name="url" value="${jdbc.url}"/>
<property name="username" value="${jdbc.username}"/>
<property name="password" value="${jdbc.password}"/>
</dataSource>
</environment>
</environments>
<!--指定mapper所在的文件位置,注意我们不用写dao层的实现类,约束大于配置,dao层中接口名和resource下的xml名字要一致,这样打包
过后才能在一个文件夹下, 才能找到对应的,不用写实现类-->
<mappers>
<!--注意是以 "/" 为分隔符 基于xml得配置-->
<mapper resource="com/lfqy/mapping/PetMapper.xml"></mapper>
<!--基于注解的配置-->
<mapper class="com.lfqy.mapping.PetAnnotationMapper"></mapper>
</mappers>
</configuration>
这样修改之后,前面的几个示例都能够直接跑通,和之前的运行结果没有变化。
3、MyBatis简版实现
3.1 原理介绍
其实,从实现原理上来说,对于非代理模式,MyBatis相当于实现了对连接数据库等操作进行了封装,从而实现了较为友好的数据库连接形式。对于前面的代理模式,也就是大家使用比较普遍的情况,MyBatis框架通过使用动态代理机制,对定义的mapper接口通过动态代理,增强它们的功能,在得到的动态代理对象中添加了数据库连接、数据库会话管理、SQL语句执行、结果解析等公共功能。
而这些对于我们使用方都是透明的,我们只需要调动mapper接口定义的方法,即可得到SQL执行的结果,非常简便。这么说来,这个框架非常高明。
3.2 手动实现简版的MyBatis框架
首先,在IDEA中新建一个Maven项目,pom.xml
文件如下:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.lfqy</groupId>
<artifactId>MyBatisImpl</artifactId>
<version>1.0-SNAPSHOT</version>
<build>
<!-- 声明下maven在打包时,要同时将配置文件打包,否则在运行时会报找不到配置文件,尤其是该项目被其它项目依赖时,单独运行的话,这块儿可以注掉。 -->
<resources>
<!-- resources文件 -->
<resource>
<directory>src/main/resources</directory>
<!-- 是否被过滤,如果被过滤则无法使用 -->
<filtering>false</filtering>
</resource>
<!-- java文件夹 -->
<resource>
<directory>src/main/java</directory>
<!-- 引入映射文件等 -->
<includes>
<include>**/*.xml</include>
<include>**/*.properties</include>
</includes>
</resource>
</resources>
<plugins>
<!-- 这里要声明下jdk是1.8,否则项目里面的lambda表达式可能不会被认出 -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>8</source>
<target>8</target>
</configuration>
</plugin>
</plugins>
</build>
<dependencies>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.45</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.10</version>
</dependency>
<!-- 解析xml文件时用 -->
<dependency>
<groupId>dom4j</groupId>
<artifactId>dom4j</artifactId>
<version>1.6.1</version>
</dependency>
<!-- 解析xml文件时,处理xpath要用到的依赖 -->
<dependency>
<groupId>jaxen</groupId>
<artifactId>jaxen</artifactId>
<version>1.1.6</version>
</dependency>
<!-- a JDBC Connection pooling / Statement caching library, 数据库连接池 -->
<dependency>
<groupId>c3p0</groupId>
<artifactId>c3p0</artifactId>
<version>0.9.1.2</version>
</dependency>
</dependencies>
</project>
定义实体类。
com.lfqy.domain.Pet:
package com.lfqy.domain;
/**
* Created by chengxia on 2019/9/16.
*/
public class Pet {
//实体类的属性和表的字段名称一一对应
private int id;
private String name;
private int age;
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
@Override
public String toString() {
return "Pet [id=" + id + ", name=" + name + ", age=" + age + "]";
}
}
然后,定义实体类对应的mapper,有两个一个是基于xml的,一个是基于注解的:
com.lfqy.dao.PetMapper:
package com.lfqy.dao;
import com.lfqy.domain.Pet;
import java.util.List;
/**
* Created by chengxia on 2019/9/10.
*/
public interface PetMapper {
List<Pet> getPets(Integer petId);
}
com.lfqy.dao.PetAnnotationMapper:
package com.lfqy.dao;
import com.lfqy.domain.Pet;
import com.lfqy.mybatis.annotations.Select;
import java.util.List;
/**
* Created by chengxia on 2019/9/10.
*/
public interface PetAnnotationMapper {
//由于这里基于注解的实现中,省略了参数解析的部分,这里直接写死参数
@Select("select * from pets where id=2")
List<Pet> getPets(Integer petId);
}
定义mapper结构,用于存放配置文件解析之后的结果:
com.lfqy.mapper.Mapper:
package com.lfqy.mapper;
/**
* @Author: lfqy
* @Date: 2019/9/15 23:55
* @describe:用于封装查询时的sql语句、参数类型和返回值类型
*/
public class Mapper {
private String queryString;
private String resultType;
private String parameterType;
public String getQueryString() {
return queryString;
}
public void setQueryString(String queryString) {
this.queryString = queryString;
}
public String getResultType() {
return resultType;
}
public void setResultType(String resultType) {
this.resultType = resultType;
}
public String getParameterType() {
return parameterType;
}
public void setParameterType(String parameterType) {
this.parameterType = parameterType;
}
}
定义一个注解:
com.lfqy.mybatis.annotations.Select:
package com.lfqy.mybatis.annotations;
import java.lang.annotation.*;
/**
* @Author: lfqy
* @Date: 2019/9/16 00:09
* @describe
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@Documented
@Inherited
public @interface Select {
/**
* 用于指定sql
* @return
*/
String value();
}
定义一个读取配置文件的类:
com.lfqy.mybatis.io.Resources:
package com.lfqy.mybatis.io;
import java.io.InputStream;
/**
* @Author: lfqy
* @Date: 2019/9/16 00:10
* @describe
*/
public class Resources {
/**
* 根据传入的参数, 输出字节流
*
* @param resource
* @return
*/
public static InputStream getResourceAsStream(String resource) throws Exception {
if (null == resource || "".equals(resource.trim())) {
throw new NullPointerException("请传入一个正确的配置文件路径");
}
//注意使用类加载器来读取这个resource类文件,不要用fileinputstream
// InputStream in = Resources.getResourceAsStream("SqlMapConfig.xml");这个是相对路径是在src文件下,如果是web路径,就没有src了
InputStream in = Resources.class.getClassLoader().getResourceAsStream(resource);
if (in.available() == 0) {
throw new IllegalArgumentException("请传入带有类路径的配置文件");
}
return in;
}
}
定义会话接口、会话工厂类和会话工厂类Builder。
com.lfqy.mybatis.sqlsession.SqlSession:
package com.lfqy.mybatis.sqlsession;
/**
* @Author: lfqy
* @Date: 2019/9/16 00:03
* @describe
*/
public interface SqlSession {
//StudentMapper getMapper(Class clazzProxy);
/**
* 泛型方法的创建,根据字节码,创建出接口的代理类
* @param classProxy 接口的字节码
* @return
*/
<T> T getMapper(Class<T> classProxy);
void close();
}
com.lfqy.mybatis.sqlsession.SqlSessionFactory:
package com.lfqy.mybatis.sqlsession;
/**
* @Author: lfqy
* @Date: 2019/9/16 00:05
* @describe
*/
public interface SqlSessionFactory {
SqlSession openSession();
}
com.lfqy.mybatis.sqlsession.SqlSessionFactoryBuilder:
package com.lfqy.mybatis.sqlsession;
import com.lfqy.mybatis.sqlsession.defaults.DefalutSqlSessionFactroy;
import java.io.InputStream;
/**
* @Author: lfqy
* @Date: 2019/9/16 00:06
* @describe:使用构建者模式,创建一个sqlsessionfactory,隐藏创建的过程
*/
public class SqlSessionFactoryBuilder {
public SqlSessionFactory build(InputStream in) {
DefalutSqlSessionFactroy sqlSessionFactory = new DefalutSqlSessionFactroy();
sqlSessionFactory.setConfig(in);
return sqlSessionFactory;
}
}
定义缺省的SQL会话和会话工厂类:
com.lfqy.mybatis.sqlsession.defaults.DefaultSqlSession:
package com.lfqy.mybatis.sqlsession.defaults;
import com.lfqy.mapper.Mapper;
import com.lfqy.mybatis.sqlsession.SqlSession;
import com.lfqy.mybatis.sqlsession.proxymapper.ProxyMapper;
import javax.sql.DataSource;
import java.beans.PropertyDescriptor;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.ResultSetMetaData;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
*
* <p>Title: DefaultSqlSession</p>
* <p>Description: 具体用于创建dao代理实现类的对象</p>
* @author lfqy
* @date 2019年9月16日
*/
public class DefaultSqlSession implements SqlSession {
private DataSource dataSource;
private Connection connection;
private Map<String, Mapper> mapperMap = new HashMap<String,Mapper>();
public void setDataSource(DataSource dataSource) {
this.dataSource = dataSource;
}
public void setConnection(Connection connection) {
this.connection = connection;
}
public void setMapperMap(Map<String, Mapper> mapperMap) {
this.mapperMap.putAll(mapperMap);
}
/**
* 创建Dao接口的代理实现类
* 动态代理
* 特点:
* 字节码随用随创建,随用随加载
* 涉及的类:
* Proxy
* 提供者:
* jdk官方
* 使用要求:
* 被代理对象最少实现一个接口
* 如何创建代理对象
* newProxyInstance(ClassLoader loader,Class[] interfaces,InvocationHandler handler )
* 方法的参数:
* ClassLoader : 类加载器, 用于加载代理对象的字节码。和被代理对象使用相同的类加载器。它是固定的写法
* Class[]:实现的接口。要求和被代理对象具有相同的行为(实现相同的接口)。它是固定写法
* InvocationHandler:增强的代码,如何增强都需要写此接口的实现类。通常我们是写一个匿名内部类。不写匿名内部类也可以
*/
@Override
public <T> T getMapper(Class<T> daoClass) {
try {
return (T) Proxy.newProxyInstance(daoClass.getClassLoader(),new Class[] {daoClass},new ProxyMapper(getConnection(),mapperMap));
} catch (Exception e) {
throw new RuntimeException(e);
}
}
/**
* 获取连接
* @return
*/
public Connection getConnection() {
try {
//判断,如果数据源有值,根本不用Connection,而是从数据源中取一个用
if(dataSource != null) {
connection = dataSource.getConnection();
}
return connection;
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
/**
* 释放资源
*/
@Override
public void close() {
try {
connection.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
/**
* 把rs中的内容封装到集合中
* @param rs
* @param resultType
* @return
* @throws Exception
*/
public static <E> List<E> handleResult(ResultSet rs, String resultType) throws Exception {
//1.定义返回值对象
List list = new ArrayList();
//2.遍历结果集
while(rs.next()) {
//反射获取实体类的字节码
Class domainClass = Class.forName(resultType);
//实例化实体类对象
Object model = domainClass.newInstance();
//获取结果集中的所有列信息:结果集的元信息
ResultSetMetaData rsmd = rs.getMetaData();
//获取总列数
int columnCount = rsmd.getColumnCount();
//遍历总列数
for(int i=1;i<=columnCount;i++) {
//取出每个列名
String columnName = rsmd.getColumnName(i);
//取出列的值
Object columnValue = rs.getObject(columnName);
//使用实体类的字节码反射出他的所有属性:属性描述器
PropertyDescriptor pd = new PropertyDescriptor(columnName, domainClass);
//获取属性描述器中的所有写方法
Method method = pd.getWriteMethod();
//执行方法,执行setXXX方法
method.invoke(model, columnValue);
}
//把model添加到集合之中
list.add(model);
}
return list;
}
}
com.lfqy.mybatis.sqlsession.defaults.DefalutSqlSessionFactroy:
package com.lfqy.mybatis.sqlsession.defaults;
import com.lfqy.mapper.Mapper;
import com.lfqy.mybatis.annotations.Select;
import com.lfqy.mybatis.io.Resources;
import com.lfqy.mybatis.sqlsession.SqlSession;
import com.lfqy.mybatis.sqlsession.SqlSessionFactory;
import com.mchange.v2.c3p0.ComboPooledDataSource;
import org.dom4j.Document;
import org.dom4j.Element;
import org.dom4j.io.SAXReader;
import javax.sql.DataSource;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.sql.Connection;
import java.sql.DriverManager;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* @Author: lfqy
* @Date: 2019/9/16 00:07
* @describe
*/
public class DefalutSqlSessionFactroy implements SqlSessionFactory {
//主配置文件
private InputStream config;
private String driver;
private String url;
private String username;
private String password;
public void setConfig(InputStream config) {
this.config = config;
}
@Override
public SqlSession openSession() {
//'new 对象时,加载类成员,datasoure ,connection都是null;
DefaultSqlSession defaultSqlSession = new DefaultSqlSession();
//加载数据源
loadConfiguration(defaultSqlSession);
return defaultSqlSession;
}
/**
* 根据字节流,补全信息 设计知识点dom4j Xpath
*
* @param defaultSqlSession
*/
private void loadConfiguration(DefaultSqlSession defaultSqlSession) {
try {
Document document = new SAXReader().read(config);
//获取根节点
Element root = document.getRootElement();
//获取所有property节点,使用xpath路径的写法
List<Element> elementList = root.selectNodes("//property");
//遍历所有的property节点
elementList.stream().forEach(element -> {
String name = element.attributeValue("name");
if ("driver".equals(name)) {
driver = element.attributeValue("value");
}
if ("url".equals(name)) {
url = element.attributeValue("value");
}
if ("username".equals(name)) {
username = element.attributeValue("value");
}
if ("password".equals(name)) {
password = element.attributeValue("value");
}
});
//到底是用datasoure ,还是connect;
String pooled = root.selectSingleNode("//dataSource").valueOf("@type");
if ("POOLED".equalsIgnoreCase(pooled)) {
DataSource ds = createDataSource();
defaultSqlSession.setDataSource(ds);
} else {
Connection connection = createConnection();
defaultSqlSession.setConnection(connection);
}
//取mapper中的内容
List<Element> mapperElements = root.element("mappers").elements("mapper");
for (Element mapperElement : mapperElements) {
//取出属性名称
if (mapperElement.attributeValue("resource") != null) {
//基于xml的
String mapperPath = mapperElement.attributeValue("resource");
//给default中map赋值
defaultSqlSession.setMapperMap(loadXMLMapper(mapperPath));
} else if (mapperElement.attributeValue("class") != null) {
//基于注解的
String mapperPath = mapperElement.attributeValue("class");
defaultSqlSession.setMapperMap(loadAnnationMapper(mapperPath));
}
}
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
config.close();
} catch (IOException e) {
}
}
}
/**
* 根据接口得全限定类名,来补全信息
*
* @param mapperPath
* @return
*/
private Map<String, Mapper> loadAnnationMapper(String mapperPath) throws Exception {
Map<String, Mapper> map = new HashMap<>();
//根据全限定类名反射
Class clazz = Class.forName(mapperPath);
//当前方法所在得类名
String clazzName = clazz.getName();
Method[] methods = clazz.getMethods();
for (Method method : methods) {
boolean annotationPresent = method.isAnnotationPresent(Select.class);
if (annotationPresent) {
//取出当前得方法名
String methodName = method.getName();
String key = clazzName + "." + methodName;
Select select = method.getAnnotation(Select.class);
String sql = select.value();
String resultType = "";
//获取当前方法得返回值,带有泛型,此处注意使用generic,带有泛型得,不能returntype,它返回得是list得字节码
Type genericReturnType = method.getGenericReturnType();//实际得到得是list<Pet>
//判断当前得type是不是参数化类型
if (genericReturnType instanceof ParameterizedType) {
ParameterizedType type = (ParameterizedType) genericReturnType;//list<Pet>
//获取参数化类型中实际类型参数
Type[] actualTypeArguments = type.getActualTypeArguments();
//取出第一个元素
Class domainClass = (Class) actualTypeArguments[0];//pet.class就有了
//获取domain得权限顶类名
resultType = domainClass.getName();
}
//这里省略了获得参数类型,并处理sql的过程,这里的sql参数直接在注解中写死。
//Class [] paramType = method.getParameterTypes();
Mapper mapper = new Mapper();
mapper.setResultType(resultType);
mapper.setQueryString(sql);
map.put(key, mapper);
}
}
return map;
}
private Map<String, Mapper> loadXMLMapper(String mapperPath) {
Map<String, Mapper> map = new HashMap<String, Mapper>();
InputStream resourceAsStream = null;
try {
resourceAsStream = Resources.getResourceAsStream(mapperPath);
Document document = new SAXReader().read(resourceAsStream);
Element rootElement = document.getRootElement();
//获取namespace
String namespace = rootElement.attributeValue("namespace");
//获取select
List<Element> selectElments = rootElement.elements("select");
selectElments.stream().forEach(selectElment -> {
//取出id属性的值
String id = selectElment.attributeValue("id");
//取出resulttype的值
String resultType = selectElment.attributeValue("resultType");
//取出parameterType的值
String parameterType = selectElment.attributeValue("parameterType");
//取出select节点的文本内容
String sql = selectElment.getText();
String key = namespace + "." + id;
Mapper mapper = new Mapper();
mapper.setQueryString(sql);
mapper.setResultType(resultType);
mapper.setParameterType(parameterType);
map.put(key, mapper);
});
return map;
} catch (Exception e) {
throw new RuntimeException(e);
} finally {
try {
resourceAsStream.close();
} catch (IOException e) {
}
}
}
private Connection createConnection() throws Exception {
Class.forName(driver);
Connection connection = DriverManager.getConnection(url, username, password);
return connection;
}
private DataSource createDataSource() throws Exception {
ComboPooledDataSource ds = new ComboPooledDataSource();
ds.setDriverClass(driver);
ds.setJdbcUrl(url);
ds.setUser(username);
ds.setPassword(password);
return ds;
}
}
为了解析配置文件中的SQL参数,定义一个参数类型枚举。
com.lfqy.mybatis.sqlsession.proxymapper.ParamType:
package com.lfqy.mybatis.sqlsession.proxymapper;
/**
* Created by chengxia on 2019/9/16.
*/
public enum ParamType {
INT, STRING;
}
代理对象mapper。
com.lfqy.mybatis.sqlsession.proxymapper.ProxyMapper:
package com.lfqy.mybatis.sqlsession.proxymapper;
import com.lfqy.mapper.Mapper;
import com.lfqy.mybatis.sqlsession.defaults.DefaultSqlSession;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.util.Map;
/**
*
* <p>Title: ProxyMapper</p>
* <p>Description: 用于创建代理对象的增强方法</p>
* @author lfqy
* @date 2019年9月16日
*/
public class ProxyMapper implements InvocationHandler {
//数据库连接
private Connection connection;
//映射的配置
private Map<String,Mapper> mapperMap;//有查询的语句和返回值类型
//创建对象必须提供这2个参数
public ProxyMapper(Connection connection, Map<String, Mapper> mapperMap) {
this.connection = connection;
this.mapperMap = mapperMap;
}
/**
* 实现查询和封装
*/
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
PreparedStatement pstm = null;
ResultSet rs = null;
try {
//1.获取当前执行方法的所在类类名以及当前方法名
String className = method.getDeclaringClass().getName();//当前方法所在类名
String methodName = method.getName();//方法名
System.out.println("=========" + className + "." + methodName +"==========");
//2.使用类名和方法名组成id,在map中获取对应的value
Mapper mapper = mapperMap.get(className+"."+methodName);
//3.判断mapper是否存在
if(mapper == null) {
throw new RuntimeException("没有对应的映射信息");
}
//4.取出sql语句、参数类型和返回值类型
String sql = mapper.getQueryString();
String resultType = mapper.getResultType();
//拼接sql参数,简单实现
String parameterType = mapper.getParameterType();
if(parameterType != null){
String paramVal = "";
ParamType pt = ParamType.valueOf(parameterType.toUpperCase());
switch (pt){
case INT:
paramVal = String.valueOf(args[0]);
break;
case STRING:
paramVal = "\"" + args[0] + "\"";
break;
}
sql = sql.replaceAll("#\\{.+}", paramVal);
}
//5.定义执行sql语句和结果集对象
pstm = connection.prepareStatement(sql);
//6.获取结果集
rs = pstm.executeQuery();
//7.调用方法封装结果集
Object obj = DefaultSqlSession.handleResult(rs,resultType);
return obj;
}catch(Exception e){
e.printStackTrace();
} finally {
rs.close();
pstm.close();
}
return null;
}
}
对应的Mapper配置文件和MyBatis框架中的一样。
com/lfqy/dao/PetMapper.xml:
<?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.lfqy.dao.PetMapper">
<!-- 在select标签中编写查询的SQL语句, 设置select标签的id属性为getPet,id属性值必须是唯一的,不能够重复
使用parameterType属性指明查询时使用的参数类型,resultType属性指明查询返回的结果集类型
resultType="com.lfqy.domain.Pet"就表示将查询结果封装成一个Pet类的对象返回
Pet类就是pets表所对应的实体类
-->
<!--
根据id查询得到一个pet对象
-->
<select id="getPets" parameterType="int"
resultType="com.lfqy.domain.Pet">
select * from pets where id=#{id}
</select>
</mapper>
MyBatis配置文件如下。
<?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">
<configuration>
<environments default="development">
<environment id="development">
<!--事务的控制方式-->
<transactionManager type="JDBC"/>
<!--是否使用连接池,UNPOOLED-->
<dataSource type="POOLED">
<property name="driver" value="com.mysql.jdbc.Driver"></property>
<property name="url" value="jdbc:mysql://localhost:3306/mybatis?characterEncoding=utf8"></property>
<property name="username" value="root"></property>
<property name="password" value="paopao666"></property>
</dataSource>
</environment>
</environments>
<!--指定mapper所在的文件位置,注意我们不用写dao层的实现类,约束大于配置,dao层中接口名和resource下的xml名字要一致,这样打包
过后才能在一个文件夹下, 才能找到对应的,不用写实现类-->
<mappers>
<!--注意是以 "/" 为分隔符-->
<mapper resource="com/lfqy/dao/PetMapper.xml"></mapper>
<!--基于注解的配置 -->
<mapper class="com.lfqy.dao.PetAnnotationMapper"/>
</mappers>
</configuration>
最后写一个测试类。
com.lfqy.PetTest:
package com.lfqy;
import com.lfqy.dao.PetAnnotationMapper;
import com.lfqy.dao.PetMapper;
import com.lfqy.domain.Pet;
import com.lfqy.mybatis.io.Resources;
import com.lfqy.mybatis.sqlsession.SqlSession;
import com.lfqy.mybatis.sqlsession.SqlSessionFactory;
import com.lfqy.mybatis.sqlsession.SqlSessionFactoryBuilder;
import java.io.InputStream;
import java.util.List;
/**
* @Author: lfqy
* @Date: 2019/9/16 00:11
* @describe
*/
public class PetTest {
/**
* 读取住配置文件
* 创建操作对象,没有实现类
* 创建代理对象
* 使用代理对象,调用,释放资源
*/
@org.junit.Test
public void test1() throws Exception {
InputStream in = Resources.getResourceAsStream("SqlMapConfig.xml");
//构造者模式
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(in);
SqlSession sqlSession = sqlSessionFactory.openSession();
//创建代理对象
PetMapper petMapper = sqlSession.getMapper(PetMapper.class);
List<Pet> pets = petMapper.getPets(1);
System.out.println(pets.get(0));
sqlSession.close();
in.close();
}
/**
* 读取住配置文件
* 创建操作对象,没有实现类
* 创建代理对象
* 使用代理对象,调用,释放资源
*/
@org.junit.Test
public void test2() throws Exception {
InputStream in = Resources.getResourceAsStream("SqlMapConfig.xml");
//构造者模式
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(in);
SqlSession sqlSession = sqlSessionFactory.openSession();
//创建代理对象
PetAnnotationMapper petAnnotationMapper = sqlSession.getMapper(PetAnnotationMapper.class);
List<Pet> pets = petAnnotationMapper.getPets(1);
System.out.println(pets.get(0));
sqlSession.close();
in.close();
}
}
完成之后的目录结构如下:
简版MyBatis框架示例结构
运行测试类的结果如下:
九月 16, 2019 1:33:36 上午 com.mchange.v2.log.MLog <clinit>
信息: MLog clients using java 1.4+ standard logging.
九月 16, 2019 1:33:41 上午 com.mchange.v2.c3p0.C3P0Registry banner
信息: Initializing c3p0-0.9.1.2 [built 21-May-2007 15:04:56; debug? true; trace: 10]
九月 16, 2019 1:33:43 上午 com.mchange.v2.c3p0.impl.AbstractPoolBackedDataSource getPoolManager
信息: Initializing c3p0 pool... com.mchange.v2.c3p0.ComboPooledDataSource [ acquireIncrement -> 3, acquireRetryAttempts -> 30, acquireRetryDelay -> 1000, autoCommitOnClose -> false, automaticTestTable -> null, breakAfterAcquireFailure -> false, checkoutTimeout -> 0, connectionCustomizerClassName -> null, connectionTesterClassName -> com.mchange.v2.c3p0.impl.DefaultConnectionTester, dataSourceName -> 1hge13aa5ez9jhd1irjrim|3a82f6ef, debugUnreturnedConnectionStackTraces -> false, description -> null, driverClass -> com.mysql.jdbc.Driver, factoryClassLocation -> null, forceIgnoreUnresolvedTransactions -> false, identityToken -> 1hge13aa5ez9jhd1irjrim|3a82f6ef, idleConnectionTestPeriod -> 0, initialPoolSize -> 3, jdbcUrl -> jdbc:mysql://localhost:3306/mybatis?characterEncoding=utf8, maxAdministrativeTaskTime -> 0, maxConnectionAge -> 0, maxIdleTime -> 0, maxIdleTimeExcessConnections -> 0, maxPoolSize -> 15, maxStatements -> 0, maxStatementsPerConnection -> 0, minPoolSize -> 3, numHelperThreads -> 3, numThreadsAwaitingCheckoutDefaultUser -> 0, preferredTestQuery -> null, properties -> {user=******, password=******}, propertyCycle -> 0, testConnectionOnCheckin -> false, testConnectionOnCheckout -> false, unreturnedConnectionTimeout -> 0, usesTraditionalReflectiveProxies -> false ]
Mon Sep 16 01:33:43 CST 2019 WARN: Establishing SSL connection without server's identity verification is not recommended. According to MySQL 5.5.45+, 5.6.26+ and 5.7.6+ requirements SSL connection must be established by default if explicit option isn't set. For compliance with existing applications not using SSL the verifyServerCertificate property is set to 'false'. You need either to explicitly disable SSL by setting useSSL=false, or set useSSL=true and provide truststore for server certificate verification.
Mon Sep 16 01:33:43 CST 2019 WARN: Establishing SSL connection without server's identity verification is not recommended. According to MySQL 5.5.45+, 5.6.26+ and 5.7.6+ requirements SSL connection must be established by default if explicit option isn't set. For compliance with existing applications not using SSL the verifyServerCertificate property is set to 'false'. You need either to explicitly disable SSL by setting useSSL=false, or set useSSL=true and provide truststore for server certificate verification.
Mon Sep 16 01:33:43 CST 2019 WARN: Establishing SSL connection without server's identity verification is not recommended. According to MySQL 5.5.45+, 5.6.26+ and 5.7.6+ requirements SSL connection must be established by default if explicit option isn't set. For compliance with existing applications not using SSL the verifyServerCertificate property is set to 'false'. You need either to explicitly disable SSL by setting useSSL=false, or set useSSL=true and provide truststore for server certificate verification.
=========com.lfqy.dao.PetMapper.getPets==========
Pet [id=1, name=Qiuqiu, age=1]
九月 16, 2019 1:33:48 上午 com.mchange.v2.c3p0.impl.AbstractPoolBackedDataSource getPoolManager
信息: Initializing c3p0 pool... com.mchange.v2.c3p0.ComboPooledDataSource [ acquireIncrement -> 3, acquireRetryAttempts -> 30, acquireRetryDelay -> 1000, autoCommitOnClose -> false, automaticTestTable -> null, breakAfterAcquireFailure -> false, checkoutTimeout -> 0, connectionCustomizerClassName -> null, connectionTesterClassName -> com.mchange.v2.c3p0.impl.DefaultConnectionTester, dataSourceName -> 1hge13aa5ez9jhd1irjrim|2d8f65a4, debugUnreturnedConnectionStackTraces -> false, description -> null, driverClass -> com.mysql.jdbc.Driver, factoryClassLocation -> null, forceIgnoreUnresolvedTransactions -> false, identityToken -> 1hge13aa5ez9jhd1irjrim|2d8f65a4, idleConnectionTestPeriod -> 0, initialPoolSize -> 3, jdbcUrl -> jdbc:mysql://localhost:3306/mybatis?characterEncoding=utf8, maxAdministrativeTaskTime -> 0, maxConnectionAge -> 0, maxIdleTime -> 0, maxIdleTimeExcessConnections -> 0, maxPoolSize -> 15, maxStatements -> 0, maxStatementsPerConnection -> 0, minPoolSize -> 3, numHelperThreads -> 3, numThreadsAwaitingCheckoutDefaultUser -> 0, preferredTestQuery -> null, properties -> {user=******, password=******}, propertyCycle -> 0, testConnectionOnCheckin -> false, testConnectionOnCheckout -> false, unreturnedConnectionTimeout -> 0, usesTraditionalReflectiveProxies -> false ]
Mon Sep 16 01:33:48 CST 2019 WARN: Establishing SSL connection without server's identity verification is not recommended. According to MySQL 5.5.45+, 5.6.26+ and 5.7.6+ requirements SSL connection must be established by default if explicit option isn't set. For compliance with existing applications not using SSL the verifyServerCertificate property is set to 'false'. You need either to explicitly disable SSL by setting useSSL=false, or set useSSL=true and provide truststore for server certificate verification.
Mon Sep 16 01:33:48 CST 2019 WARN: Establishing SSL connection without server's identity verification is not recommended. According to MySQL 5.5.45+, 5.6.26+ and 5.7.6+ requirements SSL connection must be established by default if explicit option isn't set. For compliance with existing applications not using SSL the verifyServerCertificate property is set to 'false'. You need either to explicitly disable SSL by setting useSSL=false, or set useSSL=true and provide truststore for server certificate verification.
Mon Sep 16 01:33:48 CST 2019 WARN: Establishing SSL connection without server's identity verification is not recommended. According to MySQL 5.5.45+, 5.6.26+ and 5.7.6+ requirements SSL connection must be established by default if explicit option isn't set. For compliance with existing applications not using SSL the verifyServerCertificate property is set to 'false'. You need either to explicitly disable SSL by setting useSSL=false, or set useSSL=true and provide truststore for server certificate verification.
=========com.lfqy.dao.PetAnnotationMapper.getPets==========
Pet [id=2, name=Paoao, age=3]
Process finished with exit code 0
3.3 注意
在调试的时候,发现动态代理中,同样的代码运行和调试的输出不一样。调试时会多次执行invoke方法,查了下原因,原来是IDEA中每次运行到断点时都会调用被代理对象的toString方法,而这个方法也会被动态代理截获。这样,就导致invoke方法多次执行。详见第三个参考链接。
网友评论