如果自己能夠寫一個模仿mybatis工作的程序,那麼看mybatis的源碼就會很容易。
how mybatis works?
pom
<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類的映射
java一切皆對象。
我們首先要把db.properties
和userMapper.xml
給讀進來存儲到對象中。
存db.properties
的對象叫做Configuration
:
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
:
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
最終將長這個樣子:
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的時候做:
/**
* 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
讀取數據庫連接數據:
driver=com.mysql.jdbc.Driver
url=jdbc:mysql://localhost:3306/mybatis?userUnicode=true&characterEncoding=utf-8
username=root
password=123456
用SAXReader
讀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.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
:
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
:
public interface UserMapper {
User selectByPrimaryKey(Integer id);
List<User> findAll();
}
nothing special。
執行sql併爲實體類填充值
在不知道下一步怎麼辦的情況下,我們知道,用jdbc查sql這一步是逃不了的。
所以,先寫jdbc。
public interface Executor {
<T>List<T> query(MapperStatement mapperStatement, Object parameter);
}
我們傳進MapperStatement
和參數。並統一返回list。
/**
* 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);
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是怎麼做的:
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("[email protected]");
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
:
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層面的增刪改查方法。
然後是實現類:
/**
* 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
寫出來:
/**
* 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
幹活。
if ("java.lang.Object".equals(method.getDeclaringClass().getName()))
{
return null;
}
這句很奇怪,因爲程序有bug,我只有這麼加纔能有結果,知道bug原因的朋友可以告訴我。
Test
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
生成了,構造器也順利拿到了,就是
return cons.newInstance(new Object[]{h});
返回了一個null。
知道爲什麼的朋友可以告訴我。