如果自己能够写一个模仿mybatis工作的程序,那么看mybatis的源码就会很容易。
pom
java"> <dependencies>
<dependency>
<groupId>dom4j</groupId>
<artifactId>dom4j</artifactId>
<version>1.6.1</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.47</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>
</dependencies>
我们没有依赖mybatis,但是我们要实现和mybatis类似的功能,就是直接调用mapper接口进行查询。
我们的包结构:
java_34">配置文件与java类的映射
java一切皆对象。
我们首先要把db.properties
和userMapper.xml
给读进来存储到对象中。
存db.properties
的对象叫做Configuration
:
java">public class Configuration {
private String driver;
private String url;
private String username;
private String password;
private Map<String,MapperStatement> map = new HashMap<String, MapperStatement>();
public String getDriver() {
return driver;
}
public void setDriver(String driver) {
this.driver = driver;
}
public String getUrl() {
return url;
}
public void setUrl(String url) {
this.url = url;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public Map<String, MapperStatement> getMap() {
return map;
}
@Override
public String toString() {
return "Configuration{" +
"driver='" + driver + '\'' +
", url='" + url + '\'' +
", username='" + username + '\'' +
", password='" + password + '\'' +
", map=" + map +
'}';
}
}
存userMapper.xml
的对象叫做MapperStatement
:
java">public class MapperStatement {
private String namespace;
private String id;
private String resultType;
private String sql;
public String getNamespace() {
return namespace;
}
public void setNamespace(String namespace) {
this.namespace = namespace;
}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getResultType() {
return resultType;
}
public void setResultType(String resultType) {
this.resultType = resultType;
}
public String getSql() {
return sql;
}
public void setSql(String sql) {
this.sql = sql;
}
@Override
public String toString() {
return "MapperStatement{" +
"namespace='" + namespace + '\'' +
", id='" + id + '\'' +
", resultType='" + resultType + '\'' +
", sql='" + sql + '\'' +
'}';
}
}
注意在Configuration
中,有一个属性new HashMap<String, MapperStatement>()
,它的作用是通过key找到唯一的MapperStatement
。
那么需要哪些条件才能做到呢?
只需要namespace
和id
就行。知道这两个值,就能找到唯一确定的sql(找到MapperStatement
也是为了找到sql)。
我们的Configuration
最终将长这个样子:
java">Configuration
{driver='com.mysql.jdbc.Driver',
url='jdbc:mysql://localhost:3306/mybatis?userUnicode=true&characterEncoding=utf-8',
username='root',
password='123456',
map={
com.ocean.mapper.UserMapper.findAll=MapperStatement{
namespace='com.ocean.mapper.UserMapper', id='findAll', resultType='com.ocean.entity.User',
sql='
select * from t_user;
'},
com.ocean.mapper.UserMapper.selectByPrimaryKey=MapperStatement{
namespace='com.ocean.mapper.UserMapper', id='selectByPrimaryKey', resultType='com.ocean.entity.User',
sql='
select id, username from t_user where id = ?
'}
}
}
加载配置文件
加载配置文件要在最开始做,也就是初始SqlSessionFactory的时候做:
java">/**
* load db.properties and xxxmapper.xml
*/
public class SqlSessionFactory {
private Configuration configuration;
private static final String MAPPER_FOLDER = "./mapper";
private static final String DB_LOCATION = "./db.properties";
public SqlSessionFactory() {
configuration = new Configuration();
loadDBInfo();
loadMappers();
System.out.println("init sql session factory, the configuration is : " + configuration);
}
public SqlSession openSession(){
return new DefaultSession(configuration);
}
private void loadMappers() {
URL resource = SqlSessionFactory.class.getClassLoader().getResource(MAPPER_FOLDER);
File mapper = new File(resource.getFile());
if (mapper.isDirectory()) {
File[] files = mapper.listFiles();
for (File file : files) {
loadMapper(file);
}
}
}
private void loadMapper(File file) {
SAXReader reader = new SAXReader();
try {
Document document = reader.read(file);
Element rootElement = document.getRootElement();
String namespace = rootElement.attributeValue("namespace");
List<Element> elements = rootElement.elements();
for (Element element : elements) {
MapperStatement mapperStatement = new MapperStatement();
String id = element.attributeValue("id");
String resultType = element.attributeValue("resultType");
String sql = element.getData().toString();
mapperStatement.setId(id);
mapperStatement.setNamespace(namespace);
mapperStatement.setResultType(resultType);
mapperStatement.setSql(sql);
configuration.getMap().put(namespace + "." + id, mapperStatement);
}
}
catch (Exception e) {
e.printStackTrace();
}
}
private void loadDBInfo() {
InputStream inputStream = SqlSessionFactory.class.getClassLoader().getResourceAsStream(DB_LOCATION);
Properties properties = new Properties();
try {
properties.load(inputStream);
configuration.setDriver(properties.getProperty("driver"));
configuration.setUrl(properties.getProperty("url"));
configuration.setUsername(properties.getProperty("username"));
configuration.setPassword(properties.getProperty("password"));
}
catch (Exception e) {
e.printStackTrace();
}
}
}
这里就是用Properties
读取数据库连接数据:
java">driver=com.mysql.jdbc.Driver
url=jdbc:mysql://localhost:3306/mybatis?userUnicode=true&characterEncoding=utf-8
username=root
password=123456
用SAXReader
读xml:
java"><?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.ocean.mapper.UserMapper">
<!--User selectByPrimaryKey(Integer id);-->
<select id="selectByPrimaryKey" resultType="com.ocean.entity.User">
select id, username from t_user where id = ?
</select>
<!--List<User> findAll();-->
<select id="findAll" resultType="com.ocean.entity.User">
select * from t_user;
</select>
</mapper>
为了测试,我们只准备了两个查询。
这时候我正好可以介绍实体类和mapper接口:
User
:
java">public class User implements Serializable {
private Integer id;
private String username;
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
@Override
public String toString() {
return "User{" +
"id=" + id +
", username='" + username + '\'' +
'}';
}
}
mapper
:
java">public interface UserMapper {
User selectByPrimaryKey(Integer id);
List<User> findAll();
}
nothing special。
执行sql并为实体类填充值
在不知道下一步怎么办的情况下,我们知道,用jdbc查sql这一步是逃不了的。
所以,先写jdbc。
java">public interface Executor {
<T>List<T> query(MapperStatement mapperStatement, Object parameter);
}
我们传进MapperStatement
和参数。并统一返回list。
java">/**
* given the configuration loaded by SqlSessionFactory, execute the standard query using jdbc
*/
public class DefaultExecutor implements Executor {
private Configuration configuration;
public DefaultExecutor(Configuration configuration) {
this.configuration = configuration;
}
/**
* a standard query using jdbc
* @param mapperStatement
* @param parameter
* @param <T>
* @return
*/
@Override
public <T> List<T> query(MapperStatement mapperStatement, Object parameter) {
List<T> list = new ArrayList<T>();
Connection connection = null;
PreparedStatement preparedStatement = null;
ResultSet resultSet = null;
try {
Class.forName(configuration.getDriver());
connection = DriverManager
.getConnection(configuration.getUrl(), configuration.getUsername(), configuration.getPassword());
preparedStatement = connection.prepareStatement(mapperStatement.getSql());
System.out.println("prepared statement is : " + preparedStatement);
parameterize(preparedStatement, parameter);
resultSet = preparedStatement.executeQuery();
handleResultset(list, resultSet, mapperStatement);
System.out.println("list containing entity is : " + list);
}
catch (Exception e) {
e.printStackTrace();
}
finally {
if (null != connection) {
try {
connection.close();
}
catch (SQLException e) {
e.printStackTrace();
}
}
if (null != preparedStatement) {
try {
preparedStatement.close();
}
catch (SQLException e) {
e.printStackTrace();
}
}
if (null != resultSet) {
try {
resultSet.close();
}
catch (SQLException e) {
e.printStackTrace();
}
}
}
return list;
}
/**
* set the result set value to the entity field
* @param list
* @param resultSet
* @param mapperStatement
* @param <T>
* @throws Exception
*/
private <T> void handleResultset(List<T> list, ResultSet resultSet, MapperStatement mapperStatement) throws Exception {
Class<?> clazz = Class.forName(mapperStatement.getResultType());
while (resultSet.next()) {
Object entity = clazz.newInstance();
ReflectionUtil.setPropertyFromResultSet(entity, resultSet);
list.add((T) entity);
}
}
/**
* set value to prepared statement with parameter
* @param preparedStatement
* @param parameter
* @throws Exception
*/
private void parameterize(PreparedStatement preparedStatement, Object parameter) throws Exception {
if (parameter instanceof String) {
preparedStatement.setString(1, (String) parameter);
}
else if (parameter instanceof Integer) {
preparedStatement.setInt(1, (Integer) parameter);
}
}
}
由于我们不知道传进来的sql是怎么样的,所以我们要用parameterize
方法动态地处理参数。
查出来的结果在ResultSet
中,我们要把它的值赋给实体类。
我们使用了一个工具类:ReflectionUtil.setPropertyFromResultSet(entity, resultSet);
java">public class ReflectionUtil {
/**
* take the value of result set out, and then give it to the entity field
* @param entity
* @param resultSet
* @throws Exception
*/
public static void setPropertyFromResultSet(Object entity, ResultSet resultSet)throws Exception{
Field[] declaredFields = entity.getClass().getDeclaredFields();
for (int i = 0; i < declaredFields.length; i++) {
if (declaredFields[i].getType().getSimpleName().equals("String")) {
setPropertyToEntity(entity,declaredFields[i].getName(),resultSet.getString(declaredFields[i].getName()));
}else if(declaredFields[i].getType().getSimpleName().equals("Integer")){
setPropertyToEntity(entity,declaredFields[i].getName(),resultSet.getInt(declaredFields[i].getName()));
}
}
}
/**
* set value of field for entity
* @param entity
* @param name
* @param value
* @throws Exception
*/
private static void setPropertyToEntity(Object entity, String name, Object value) throws Exception{
Field field;
field = entity.getClass().getDeclaredField(name);
field.setAccessible(true);
field.set(entity,value);
}
}
它的目的就是把resultset中的值一一取出来,给User赋上。
sqlsession
该做的工作都做好了。
暴露给程序员的就是一个session,它代表着与数据库的一次连接。
我们看一下之前mybatis是怎么做的:
java"> public static SqlSessionFactory getSqlSessionFactory() throws Exception {
String resources = "mybatis-config.xml";
InputStream resourceAsStream = Resources.getResourceAsStream(resources);
return new SqlSessionFactoryBuilder().build(resourceAsStream);
}
@Test
public void testCreateEmp() throws Exception {
SqlSessionFactory sqlSessionFactory = getSqlSessionFactory();
SqlSession sqlSession = sqlSessionFactory.openSession();
try {
EmployeeMapper employeeMapper = sqlSession.getMapper(EmployeeMapper.class);
Employee employee = new Employee();
employee.setLastName("Yeats");
employee.setEmail("78657@test.com");
Integer result = employeeMapper.createEmployee(employee);
System.out.println("if created successfully? " + result);
sqlSession.commit();
} finally {
sqlSession.close();
}
}
sqlSession
有一个getMapper
的方法,并且返回一个代理对象。
并且带代理对象去执行具体的增删改查方法。
如果是用jdk动态代理来做的,当上述代码执行createEmployee
方法时,一定会被一个InvocationHandler
拦截到,并且执行invoke
方法。
我的思路是把真正要执行的查询放在invoke
方法中,这个增删改查工作是由SqlSession
完成的,当然,SqlSession
肯定封装了底层干活的Executor
。
上面是大体的思路。
首先我们需要一个SqlSession
:
java">public interface SqlSession {
<T> T selectById(String sourceId, Object parameter);
<T>List<T> selectAll(String sourceId, Object parameter);
<T> T getMapper(Class<T> type);
}
它一定会有getMapper
方法,一定会有session层面的增删改查方法。
然后是实现类:
java">/**
* default session implementation
*
* @author
*/
public class DefaultSession implements SqlSession {
private Configuration configuration;
private Executor executor;
public DefaultSession(Configuration configuration){
this.configuration=configuration;
executor=new DefaultExecutor(configuration);
}
@Override
public <T> T selectById(String sourceId, Object parameter) {
List<Object> objects = selectAll(sourceId, parameter);
if(objects==null||objects.size()<0){
return null;
}else if(objects.size()>1){
throw new RuntimeException("too many results");
}else {
Object ret = objects.get(0);
return (T) ret;
}
}
@Override
public <T> List<T> selectAll(String sourceId, Object parameter) {
MapperStatement mapperStatement = configuration.getMap().get(sourceId);
return executor.query(mapperStatement,parameter);
}
@Override
public <T> T getMapper(Class<T> type) {
MapperHandler mapperHandler = new MapperHandler(this);
Object proxyInstance = Proxy.newProxyInstance(type.getClassLoader(), new Class[] {type}, mapperHandler);
return (T)proxyInstance;
}
}
先看selectById
和selectAll
这两个方法,它们最终是让Executor
去干活的,这和我们的设计思路是一致的。
然后是getMapper
。我们用Proxy.newProxyInstance
的方式返回一个代理。
所需要的参数是一个系统类加载器,一个接口对象,一个InvocationHandler
。
我们把InvocationHandler
写出来:
java">/**
* the handler of a proxy instance of a mapper
*/
public class MapperHandler implements InvocationHandler {
private SqlSession sqlSession;
public MapperHandler(SqlSession sqlSession) {
this.sqlSession = sqlSession;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
if ("java.lang.Object".equals(method.getDeclaringClass().getName())) {
return null;
}
System.out.println("sourceId : " + method.getDeclaringClass().getName() + "." + method.getName());
//if return type is instance of Collection(in our case , it's a list), execute selectAll
if (Collection.class.isAssignableFrom(method.getReturnType())) {
return sqlSession.selectAll(method.getDeclaringClass().getName() + "." + method.getName(), args==null?null:args[0]);
}
else {
return sqlSession.selectById(method.getDeclaringClass().getName() + "." + method.getName(), args==null?null:args[0]);
}
}
}
在invoke
方法里,我们委托sqlSession
干活。
java">if ("java.lang.Object".equals(method.getDeclaringClass().getName()))
{
return null;
}
这句很奇怪,因为程序有bug,我只有这么加才能有结果,知道bug原因的朋友可以告诉我。
Test
java">public class MyBatisTest {
public static void main(String[] args) throws Exception {
SqlSessionFactory sqlSessionFactory = new SqlSessionFactory();
SqlSession session = sqlSessionFactory.openSession();
UserMapper userMapper = session.getMapper(UserMapper.class);
List<User> all = userMapper.findAll();
all.forEach(System.out::println);
/*User userOne = userMapper.selectByPrimaryKey(1);
System.out.println("userOne : "+userOne);*/
}
}
得到正确结果。完事。
但是Proxy.newProxyInstance(type.getClassLoader(), new Class[] {type}, mapperHandler);
这段是有问题的。
我debug了一下,proxy0
生成了,构造器也顺利拿到了,就是
java"> return cons.newInstance(new Object[]{h});
返回了一个null。
知道为什么的朋友可以告诉我。