Mybatis自定义持久层框架
概述
Mybatis使我们常用的持久层框架,他的本质是通过反射和动态代理的方式对JDBC(这里以jdbc为例,当然mybatis不只是jdbc)的进一步封装,所以我们通过反射和动态代理来尝试自己实现一个建议的持久层框架。
思路
我们在开始之前先整理一下,我们的思路,根据我们使用mybatis的经验,来总结我们可能需要用到的东西。
- 数据库连接的xml配置文件和我们写sql的xml配置文件
- 我们需要一个类解析xml配置文件
- 还需要有一个接口类,其中包括,查询插入等方法,来以供我们调取
- 还需要有一个具体的执行类
大概的思路就是这样,然后我们开始吧
解析XML配置文件
首先我们在客户端创建两个xml配置文件,一个叫SqlConnectionMapper.xml用来数据库连接,一个叫UserMapper.xml用来写sql。
SqlConnectionMapper.xml
<configuration>
<dataresource>
<property name="driver" value="com.mysql.jdbc.Driver"></property>
<property name="jdbcUrl" value="jdbc:mysql://localhost:3306/mybatisdemo"></property>
<property name="user" value="root"></property>
<property name="password" value="123456"></property>
</dataresource>
<mapper name="UserMapper.xml"></mapper>
</configuration>
UserMapper.xml
<configuration namespace="Dao.UserDao">
<select id="queryAll" paramtertype="mode.User" resulttype="mode.User">
select * from user;
</select>
</configuration>
注意的是我们在SqlConnectionMapper.xml里面写入了UserMapper.xml的地址,这样的话,我们在解析xml配置文件的时候,只需要传递SqlConnectionMapper.xml这一个配置文件就能获取到所有xml文件,同时我们在实际的生产过程中,也不可能只有一个UserMapper.xml配置文件,而是多个xml配置文件,所以我们需要一个公共的入口,去加载所有的配置文件。
接下来我们去解析配置文件
首先我们创建一个SqlSessionFactoryBuild类,将传入的配置文件通过我们新建的build方法进行解析。
解析完的配置文件,我们需要把他们保存到类里面,方便我们操作。
新建ConfigBean存放数据库连接信息也就是SqlConnectionMapper.xml
新建SqlMpperBean存放sql语句信息也就是UserMapper.xml中的方法
ConfigBean
public class ConfigBean {
private DataSource dataSource;
private Map<String,SqlMpperBean> sqlMpperBeanMap=new HashMap<String,SqlMpperBean>();
dataSource:存放jdbc连接属性
sqlMpperBeanMap:是一个map,其中key是statementid,value是SqlMpperBean
statementid:用来标记要执行的方法,有UserMapper的namespace+要执行的方法的id组成
SqlMpperBean
public class SqlMpperBean {
// 对应xml中的namespace
private String namespace;
// 对应方法标签的id
private String id;
// 对应传入的参数类型
private String paramtertype;
// 对应返回参数类型
private String resulttype;
// sql语句
private String sqlText;
// 方法类型 例如:<select> <insert>...
private String type;
那么我们如何去解析xml配置文件呢?
这里我们使用dom4j工具来解析配置文件
新建XmlConfigBuild.class用来解析数据库连接信息
新建XmlSqlMapperBuild用来解析sql语句信息
然后在SqlSessionFactoryBuild中调用这两个方法
SqlSessionFactoryBuild.class
public class SqlSessionFactoryBuild {
public SqlSessionFactory build(InputStream inputStream) throws PropertyVetoException, DocumentException {
// 配置文件解析
XmlConfigBuild xmlConfigBuild = new XmlConfigBuild();
ConfigBean configBean = xmlConfigBuild.build(inputStream);
SqlSessionFactory defaultSqlSession = new DefaultSqlSession(configBean);
return defaultSqlSession;
}
}
XmlConfigBuild.class
public class XmlConfigBuild {
private ConfigBean configBean;
public XmlConfigBuild(){
this.configBean=new ConfigBean();
}
public ConfigBean build(InputStream inputStream) throws DocumentException, PropertyVetoException {
// 读取配置文件
SAXReader saxReader = new SAXReader();
Document document = saxReader.read(inputStream);
// 获取根标签
Element rootElement = document.getRootElement();
// 获取所有property标签
List<Element> propertys = rootElement.selectNodes("//property");
HashMap<String, String> jdbcMap = new HashMap<>();
// 将所有的property中的name属性存入map中,方便我们接下来使用
propertys.forEach(proper->{
jdbcMap.put(proper.attributeValue("name"),proper.attributeValue("value"));
});
// 使用c3p0数据连接池,保证数据连接的持久化
ComboPooledDataSource comboPooledDataSource = new ComboPooledDataSource();
// 获取数据库连接信息
comboPooledDataSource.setDriverClass(jdbcMap.get("driver"));
comboPooledDataSource.setJdbcUrl(jdbcMap.get("jdbcUrl"));
comboPooledDataSource.setUser(jdbcMap.get("user"));
comboPooledDataSource.setPassword(jdbcMap.get("password"));
this.configBean.setDataSource(comboPooledDataSource);
// 获取sqlMapper.xml
List<Element> mapperList = rootElement.selectNodes("//mapper");
mapperList.forEach(mapper->{
String path = mapper.attributeValue("name");
// XmlSqlMapperBuild解析对应的sql语句
XmlSqlMapperBuild xmlSqlMapperBuild = new XmlSqlMapperBuild(this.configBean);
try {
xmlSqlMapperBuild.build(path);
} catch (DocumentException e) {
e.printStackTrace();
}
});
return this.configBean;
}
}
XmlSqlMapperBuild.class
public class XmlSqlMapperBuild {
private ConfigBean configBean;
public XmlSqlMapperBuild(ConfigBean configBean){
this.configBean=configBean;
}
public void build(String path) throws DocumentException {
InputStream resourceAsStream = XmlSqlMapperBuild.class.getClassLoader().getResourceAsStream(path);
// 同样使用dom4j读取配置文件
SAXReader saxReader = new SAXReader();
Document document = saxReader.read(resourceAsStream);
// 获取根标签
Element rootElement = document.getRootElement();
// namespace唯一标识一个Mapper.xml
String namespace = rootElement.attributeValue("namespace");
List<Element> elements = rootElement.elements();
elements.forEach(node->{
SqlMpperBean sqlMpperBean = new SqlMpperBean();
// 要执行的方法id,唯一标识
sqlMpperBean.setId(node.attributeValue("id"));
sqlMpperBean.setNamespace(namespace);
// 获取传入参数类型
sqlMpperBean.setParamtertype(node.attributeValue("paramtertype"));
//获取返回参数类型
sqlMpperBean.setResulttype(node.attributeValue("resulttype"));
//获取sql
sqlMpperBean.setSqlText(node.getText());
//获取方法标签
sqlMpperBean.setType(node.getName());
//组成statementid,标识那个sqlmapper.xml中的那个方法
String key=namespace+"."+node.attributeValue("id");
this.configBean.addSqlMapperBeanMap(key,sqlMpperBean);
});
}
}
这样我们的xml配置文件就解析完成了,然后我们回过头来看,发现我们在SqlSessionFactoryBuild的build方法中除了调用解析配置文件方法,我们还实例化了一个DefaultSqlSession。
DefaultSqlSession是 SqlSessionFactory接口的实现,而SqlSessionFactory提供了我们要执行的方法,包括查询,插入等。
构建Session,调用执行方法
SQLSessionFactory
public interface SqlSessionFactory {
public <E> List<E> selectList(String statementid,Object...params);
public <T> T getMapper(Class<?> mapperClass);
public <T> T addOne(String statementid,Object...params);
}
DefaultSqlSession
public class DefaultSqlSession implements SqlSessionFactory {
private ConfigBean configBean;
public DefaultSqlSession(ConfigBean configBean){
this.configBean=configBean;
}
@Override
public <T> T getMapper(Class<?> mapperClass) {
// 使用动态代理
Object proxyInstance = Proxy.newProxyInstance(DefaultSqlSession.class.getClassLoader(), new Class[]{mapperClass}, new InvocationHandler() {
@Override
public Object invoke(Object o, Method method, Object[] objects) throws Throwable {
// 方法名
String name = method.getName();
// 该方法对应的类名
String namespace = method.getDeclaringClass().getName();
// 组成statementid
String key = namespace + "." + name;
// 获取对应sqlmapper,得到该方法对应的sql信息
SqlMpperBean sqlMpperBean = getSqlMpperBean(key);
// 根据该方法的标签名,选择对应的方法
switch (sqlMpperBean.getType()){
case "select":{
List<Object> objectList = selectList(key, objects);
return objectList;
}
case "insert":{
Object addOne = addOne(key, objects);
return addOne;
}
}
return new Exception("没有找到对应标签!请检查sql");
}
});
return (T)proxyInstance;
}
private SqlMpperBean getSqlMpperBean(String statementid){
Map<String, SqlMpperBean> sqlMpperBeanMap = this.configBean.getSqlMpperBeanMap();
SqlMpperBean sqlMpperBean = sqlMpperBeanMap.get(statementid);
return sqlMpperBean;
}
@Override
public <E> List<E> selectList(String statementid, Object... params) {
SqlMpperBean sqlMpperBean = getSqlMpperBean(statementid);
Executor executor = new Executor();
// 执行器执行该方法
List<Object> query = executor.query(this.configBean, sqlMpperBean, params);
return (List<E>) query;
}
@Override
public <T> T addOne(String statementid, Object... params) throws SQLException, ClassNotFoundException {
SqlMpperBean sqlMpperBean = getSqlMpperBean(statementid);
Executor executor = new Executor();
// 执行器执行该方法
Boolean aBoolean = executor.addOne(this.configBean, sqlMpperBean, params);
if(aBoolean){
return (T)aBoolean;
}else{
throw new RuntimeException("执行错误,检查sql代码是否正确");
}
}
}
执行器Executor
executor执行器是实际执行sql的类,对传入的参数进行解析,对放回的值进行封装
Executor.class
public class Executor {
// 解析入参
private PreparedStatement getprepare(ConfigBean configBean,SqlMpperBean sqlMpperBean,Object...params) {
// 获取数据库连接信息
DataSource dataSource = configBean.getDataSource();
Connection connection = dataSource.getConnection();
// 解析sql语句,对#{}进行替换成?
BoundSql boundSql = this.boundSQL(sqlMpperBean.getSqlText());
// boundsql.getsql 是解析完的sql语句
// preparedStatement 预处理对象,传入sql和对应参数就可以执行sql获取数据库返回值
PreparedStatement preparedStatement = connection.prepareStatement(boundSql.getSql());
// 获取参数路径
String paramtertype = sqlMpperBean.getParamtertype();
// 获取入参对应的类
Class<?> paramteClass = Class.forName(paramtertype);
// 获取解析完的要执行的入参字段名,比如id,name等
List<ParameterMapping> parameterMappingList = boundSql.getParameterMappingList();
if(!parameterMappingList.isEmpty()){
for (int i =0; i < parameterMappingList.size(); i++) {
ParameterMapping paramter = parameterMappingList.get(i);
String content = paramter.getContent();
try {
// 通过反射和字段名,获取到类中对应的属性
Field declaredField = paramteClass.getDeclaredField(content);
// 暴力访问
declaredField.setAccessible(true);
// 获取该属性在指定对象上面的值
Object o = declaredField.get(params[0]);
// 将参数存入preparedStatement
preparedStatement.setObject(i+1,o);
} catch (NoSuchFieldException | IllegalAccessException | SQLException e) {
e.printStackTrace();
}
}
}
return preparedStatement;
}
// 查询方法
public <E> List<E> query(ConfigBean configBean, SqlMpperBean sqlMpperBean,Object...params) {
PreparedStatement preparedStatement = getprepare(configBean, sqlMpperBean, params);
// 执行sql
ResultSet resultSet = preparedStatement.executeQuery();
// 封装返回结果
String resulttype = sqlMpperBean.getResulttype();
Class<?> resultClass = Class.forName(resulttype);
ArrayList<Object> objects = new ArrayList<>();
while (resultSet.next()){
Object instance = resultClass.newInstance();
ResultSetMetaData metaData = resultSet.getMetaData();
if (metaData != null) {
for (int j = 1; j <=metaData.getColumnCount(); j++) {
// 字段名
String columnName = metaData.getColumnName(j);
// 字段值
Object object = resultSet.getObject(columnName);
// 使用反射,来进行对实体的封装
// 获取属性
PropertyDescriptor propertyDescriptor = new PropertyDescriptor(columnName, resultClass);
// 得到对应的写方法
Method writeMethod = propertyDescriptor.getWriteMethod();
// 对属性进行赋值
writeMethod.invoke(instance, object);
}
}
objects.add(instance);
}
return (List<E>) objects;
}
// sql解析
private BoundSql boundSQL(String sql){
// 这里借助了mybatis的ParameterMappingTokenHandler和GenericTokenParser来解析sql
ParameterMappingTokenHandler parameterMappingTokenHandler = new ParameterMappingTokenHandler();
// 把{#}替换为?
GenericTokenParser genericTokenParser = new GenericTokenParser("#{", "}", parameterMappingTokenHandler);
// 解析完的sql
String parse = genericTokenParser.parse(sql);
// 入参名
List<ParameterMapping> parameterMappings = parameterMappingTokenHandler.getParameterMappings();
// 存入boundSql实体中,方便我们调用
BoundSql boundSql = new BoundSql(parse, parameterMappings);
return boundSql;
}
// 添加方法
public Boolean addOne(ConfigBean configBean, SqlMpperBean sqlMpperBean,Object...params) {
PreparedStatement getprepare = getprepare(configBean, sqlMpperBean, params);
try{
getprepare.executeUpdate();
return true;
}catch (Exception e){
System.out.println("请检查sql或数据已存在,请不要重复插入!");
return false;
}
}
}
客户端调用
首先我们新建一个Dao和实体
UserDao
public interface UserDao {
public List<User> queryAll();
public Boolean addOne(User user);
}
User
public class User implements Serializable {
private Integer id;
private String name;
}
最后我们写一个查询和添加测试一下
查询
@Test
public void test() throws PropertyVetoException, DocumentException {
InputStream resourceAsStream = mybatis_test.class.getClassLoader().getResourceAsStream("SqlConnectionMapper.xml");
SqlSessionFactoryBuild sqlSessionFactoryBuild = new SqlSessionFactoryBuild();
SqlSessionFactory sqlSessionFactory = sqlSessionFactoryBuild.build(resourceAsStream);
UserDao userDao = sqlSessionFactory.getMapper(UserDao.class);
List<User> users = userDao.queryAll();
System.out.println("--------" + users);
}
结果:
添加
@Test
public void addTest() throws PropertyVetoException, DocumentException {
InputStream resourceAsStream = mybatis_test.class.getClassLoader().getResourceAsStream("SqlConnectionMapper.xml");
SqlSessionFactoryBuild sqlSessionFactoryBuild = new SqlSessionFactoryBuild();
SqlSessionFactory build = sqlSessionFactoryBuild.build(resourceAsStream);
UserDao mapper = build.getMapper(UserDao.class);
User user = new User();
user.setId(3);
user.setName("wangwu");
Boolean aBoolean = mapper.addOne(user);
System.out.println(aBoolean);
}
结果:
最后一问
该项目中用了几种设计模式呢?