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工程,结构如下图:
其中,代码如下。
pom.xml:
4.0.0
com.lfqy
mybatistest
1.0-SNAPSHOT
org.mybatis
mybatis
3.4.5
mysql
mysql-connector-java
5.1.6
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:
Conf.xml:
这样,写一个如下的测试类,测试上面的程序是否正确。
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类,如下图:
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方法之间的映射关系。
如下是在前面例子基础上,写完注解示例之后的目录结构。
从上面可以看出,首先新增了一个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配置文件在一个地方)。修改后的配置文件如下。
最后新增一个测试类来测试基于注解的配置是否生效。
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表达式引用其中的内容。如下介绍。
我们首先将数据库相关的配置放到单独的配置文件。
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:
这样修改之后,前面的几个示例都能够直接跑通,和之前的运行结果没有变化。
3、MyBatis简版实现
3.1 原理介绍
其实,从实现原理上来说,对于非代理模式,MyBatis相当于实现了对连接数据库等操作进行了封装,从而实现了较为友好的数据库连接形式。对于前面的代理模式,也就是大家使用比较普遍的情况,MyBatis框架通过使用动态代理机制,对定义的mapper接口通过动态代理,增强它们的功能,在得到的动态代理对象中添加了数据库连接、数据库会话管理、SQL语句执行、结果解析等公共功能。
而这些对于我们使用方都是透明的,我们只需要调动mapper接口定义的方法,即可得到SQL执行的结果,非常简便。这么说来,这个框架非常高明。
3.2 手动实现简版的MyBatis框架
首先,在IDEA中新建一个Maven项目,pom.xml
文件如下:
4.0.0
com.lfqy
MyBatisImpl
1.0-SNAPSHOT
src/main/resources
false
src/main/java
**/*.xml
**/*.properties
org.apache.maven.plugins
maven-compiler-plugin
8
mysql
mysql-connector-java
5.1.45
junit
junit
4.10
dom4j
dom4j
1.6.1
jaxen
jaxen
1.1.6
c3p0
c3p0
0.9.1.2
定义实体类。
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 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 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 getMapper(Class 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;
/**
*
* Title: DefaultSqlSession
* Description: 具体用于创建dao代理实现类的对象
* @author lfqy
* @date 2019年9月16日
*/
public class DefaultSqlSession implements SqlSession {
private DataSource dataSource;
private Connection connection;
private Map mapperMap = new HashMap();
public void setDataSource(DataSource dataSource) {
this.dataSource = dataSource;
}
public void setConnection(Connection connection) {
this.connection = connection;
}
public void setMapperMap(Map mapperMap) {
this.mapperMap.putAll(mapperMap);
}
/**
* 创建Dao接口的代理实现类
* 动态代理
* 特点:
* 字节码随用随创建,随用随加载
* 涉及的类:
* Proxy
* 提供者:
* jdk官方
* 使用要求:
* 被代理对象最少实现一个接口
* 如何创建代理对象
* newProxyInstance(ClassLoader loader,Class[] interfaces,InvocationHandler handler )
* 方法的参数:
* ClassLoader : 类加载器, 用于加载代理对象的字节码。和被代理对象使用相同的类加载器。它是固定的写法
* Class[]:实现的接口。要求和被代理对象具有相同的行为(实现相同的接口)。它是固定写法
* InvocationHandler:增强的代码,如何增强都需要写此接口的实现类。通常我们是写一个匿名内部类。不写匿名内部类也可以
*/
@Override
public T getMapper(Class 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 List 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 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 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 loadAnnationMapper(String mapperPath) throws Exception {
Map 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
//判断当前得type是不是参数化类型
if (genericReturnType instanceof ParameterizedType) {
ParameterizedType type = (ParameterizedType) genericReturnType;//list
//获取参数化类型中实际类型参数
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 loadXMLMapper(String mapperPath) {
Map map = new HashMap();
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 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;
/**
*
* Title: ProxyMapper
* Description: 用于创建代理对象的增强方法
* @author lfqy
* @date 2019年9月16日
*/
public class ProxyMapper implements InvocationHandler {
//数据库连接
private Connection connection;
//映射的配置
private Map mapperMap;//有查询的语句和返回值类型
//创建对象必须提供这2个参数
public ProxyMapper(Connection connection, Map 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:
MyBatis配置文件如下。
最后写一个测试类。
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 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 pets = petAnnotationMapper.getPets(1);
System.out.println(pets.get(0));
sqlSession.close();
in.close();
}
}
完成之后的目录结构如下:
运行测试类的结果如下:
九月 16, 2019 1:33:36 上午 com.mchange.v2.log.MLog
信息: 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方法多次执行。详见第三个参考链接。
参考资料
- MyBatis_百度百科
- MySQL 8.0 - Client does not support authentication protocol requested by server; consider upgrading MySQL client
- 关于IDEA执行JDK动态代理debug时重复输出问题