本文用于拉钩训练营笔记
说起持久层框架,大家第一时间想到的应该都是mybatis、hibernate,他们都是非常优秀的持久层框架。那为什么要使用框架而不使用原始的JDBC呢?下面是我们使用JDBC查询数据库的代码:
@Test
public void test() throws ClassNotFoundException{
String url = \"jdbc:mysql://localhost:3306/lin?characterEncoding=utf-8&serverTimezone=Asia/Shanghai\";
String user = \"root\";
String password = \"123456\";
String driverClass = \"com.mysql.cj.jdbc.Driver\";
String sql = \"select * from user where id = ?\";
//加载数据库驱动
Class.forName(driverClass);
try (Connection conn = DriverManager.getConnection(url, user, password);
PreparedStatement preparedStatement = conn.prepareStatement(sql);){
//设置sql参数,第一个参数为sql中参数的序号(从1开始),第二个参数为参数值
preparedStatement.setString(1, \"1\");
ResultSet resultSet = preparedStatement.executeQuery();
//遍历结果集,封装结果
while (resultSet.next()) {
int id = resultSet.getInt(\"id\");
String username = resultSet.getString(\"username\");
log.info(\"id: {}, username: {}\",id,username);
}
}catch (SQLException e){
e.printStackTrace();
}
}
不难看出上面代码的几个问题:
针对以上存在的几个问题提出解决方案:
针对之前JDBC存在的问题我们提出了解决方案,现在我们对照解决方案来设计框架:
使用端:
框架端:
接下来针对上面的方案开始实现我们的框架:
首先新建一个名为PersistenceFrameWork的项目:
其中app为使用端,framework为框架端。
先讲讲框架端,框架端要连接数据库,要解析xml,要打印日志,所以引入如下包:
<dependencies>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.16.18</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.22</version>
</dependency>
<dependency>
<groupId>c3p0</groupId>
<artifactId>c3p0</artifactId>
<version>0.9.1.2</version>
</dependency>
<dependency>
<groupId>dom4j</groupId>
<artifactId>dom4j</artifactId>
<version>1.6.1</version>
</dependency>
<dependency>
<groupId>jaxen</groupId>
<artifactId>jaxen</artifactId>
<version>1.1.6</version>
</dependency>
<!-- 日志处理 -->
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.17</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
<version>1.7.26</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>1.7.26</version>
</dependency>
</dependencies>
对于使用端,只需要引入框架端和打印日志以及测试就够了:
<dependencies>
<!-- 自定义持久层框架jar包 -->
<dependency>
<groupId>org.lin</groupId>
<artifactId>framework</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<!-- lombok,可自行选择 -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.16.18</version>
<scope>provided</scope>
</dependency>
<!-- 日志处理 -->
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.17</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
<version>1.7.26</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>1.7.26</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>5.0.6.RELEASE</version>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
<version>5.5.2</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
</dependency>
</dependencies>
我们需要读取使用端的数据库配置信息,需要使用端将配置文件路径传过来,我们再做解析:
对于使用端,在resources下面新建一个sqlConfig.xml
文件,提供数据库配置:
<?xml version=\"1.0\" encoding=\"UTF-8\" ?>
<configuration>
<dataSource>
<property name=\"driverClass\" value=\"com.mysql.cj.jdbc.Driver\" />
<property name=\"jdbcUrl\" value=\"jdbc:mysql://localhost:3306/lin?characterEncoding=utf-8&serverTimezone=Asia/Shanghai\" />
<property name=\"user\" value=\"root\" />
<property name=\"password\" value=\"123456\" />
</dataSource>
<mapper resource=\"UserMapper.xml\"/>
</configuration>
其中mapper节点为提供相关sql语句,这个后面再说。
然后就是框架端对配置文件的解析:
public class SqlSessionFactoryBuilder {
/**
* 读取配置文件,获取流
* @param path 文件路径
*/
private static InputStream getResourceAsSteam(String path) {
return SqlSessionFactoryBuilder.class.getClassLoader().getResourceAsStream(path);
}
/**
* 使用dom4j解析配置文件,并返回SqlSessionFactory
* @param path 配置文件
* @return SqlSessionFactory
*/
public SqlSessionFactory build(String path) throws DocumentException, PropertyVetoException {
// 1.解析配置文件封装configuration
XmlConfigBuilder xmlConfigBuilder = new XmlConfigBuilder();
SqlConfiguration configuration = xmlConfigBuilder.parseConfig(getResourceAsSteam(path));
// 2.创建SqlSessionFactory,并返回
return new DefaultSqlSessionFactory(configuration);
}
}
其中,XmlConfigBuilder
用于解析数据库配置文件,并且返回一个配置类:
public class XmlConfigBuilder {
private final SqlConfiguration configuration = new SqlConfiguration();
/**
* 使用dom4j解析配置文件,并封装到SqlConfiguration对象
* @param in 配置文件流
* @return SqlConfiguration
*/
@SuppressWarnings(\"unchecked\")
public SqlConfiguration parseConfig(InputStream in) throws DocumentException, PropertyVetoException {
// 1.解析sqlMapConfig.xml
// 读取sqlMapConfig.xml配置文件
Document document = new SAXReader().read(in);
// 获取根标签:即<configration>标签
Element rootElement = document.getRootElement();
// 获取property标签内容:即数据库配置信息
List<Element> list = rootElement.selectNodes(\"//property\");
// 封装到properties中
Properties properties = new Properties();
for (Element ele : list) {
String name = ele.attributeValue(\"name\");
String value = ele.attributeValue(\"value\");
properties.setProperty(name, value);
}
// 将数据库配置信息设置到c3p0的数据库连接池中
ComboPooledDataSource comboPooledDataSource = new ComboPooledDataSource();
comboPooledDataSource.setDriverClass(properties.getProperty(\"driverClass\"));
comboPooledDataSource.setJdbcUrl(properties.getProperty(\"jdbcUrl\"));
comboPooledDataSource.setUser(properties.getProperty(\"user\"));
comboPooledDataSource.setPassword(properties.getProperty(\"password\"));
configuration.setDataSource(comboPooledDataSource);
// 2.解析mapper.xml
// 获取mapper.xml的全路径--获取文件流--dom4j解析
List<Element> mapperList = rootElement.selectNodes(\"//mapper\");
for (Element ele : mapperList) {
String mapperPath = ele.attributeValue(\"resource\");
InputStream inputStream = ResourceFactory.getResourceAsSteam(mapperPath);
XmlMapperBuilder xmlMapperBuilder = new XmlMapperBuilder(configuration);
xmlMapperBuilder.parse(inputStream);
}
return configuration;
}
}
XmlMapperBuilder
是用于解析mapper.xml
文件,这个也放在后面说,返回的配置类包含数据库连接信息和mapper.xml
解析成为sql语句的信息:
@Data
public class SqlConfiguration {
private DataSource dataSource;
/**
* mapper信息:key:statementId:namespace+id;value:MappedStatement
*/
private Map<String, MappedStatement> mappedStatementMap = new HashMap<>();
}
使用端已经提供了数据库配置信息,这个时候就可以通过配置信息连接上数据库了。连上数据库之后的操作呢?当然是crud了!所以我们还得要使用端提供sql语句,我们希望的是:
使用端提供sql语句,执行sql的参数,执行完成的返回值,框架端拿到sql之后执行完成,将结果封装为相关对象然后返回给使用端。
这里使用端提供sql,我们统一将其写入到xml文件中,在resources中新建一个UserMapper.xml文件:
<?xml version=\"1.0\" encoding=\"UTF-8\" ?>
<Mapper namespace=\"com.lin.mapper.UserMapper\">
<!-- namespace和id组成statemeId,作为sql语句的唯一标识 -->
<select id=\"selectAll\" resultType=\"com.lin.pojo.User\">
select * from user
</select>
<select id=\"selectUserById\" paramterType=\"java.lang.Integer\" resultType=\"com.lin.pojo.User\">
select * from user where id = #{id}
</select>
<insert id=\"insert\" paramterType=\"com.lin.pojo.User\">
insert into user values(#{id}, #{username})
</insert>
<update id=\"update\" paramterType=\"com.lin.pojo.User\">
update user set username = #{username} where id = #{id}
</update>
<delete id=\"deleteById\" paramterType=\"java.lang.Integer\">
delete from user where id = #{id}
</delete>
</Mapper>
user类只有两个参数,对应数据库的user表:
@Data
public class User {
private Integer id;
private String username;
}
这个xml文件的命名空间为com.lin.mapper.UserMapper
,UserMapper
是一个接口,我们后面会通过动态代理的方式,在调用这个接口方法的时候,执行相关的sql然后封装返回结果:
public interface UserMapper {
List<User> selectAll();
User selectUserById(int id);
int insert(User user);
int update(User user);
int deleteById(int id);
}
使用端到这个时候做的事情已经结束了,接下来看框架端的了:
既然要解析mapper文件,那么就要把解析后的结果保存下来,所以需要有这么一个类,用于保存解析结果:
@Data
public class MappedStatement {
/**
* id
*/
private String id;
/**
* 返回值类型
*/
private String resultType;
/**
* 参数类型
*/
private String parameterType;
/**
* sql语句
*/
private String sql;
}
好了,接下来需要一个类来专门解析mapper.xml
文件,这个类也就是在上面XmlConfigBuilder
中用到的:
public class XmlMapperBuilder {
private SqlConfiguration configration;
/**
* 有参构造函数
* @param configration
*/
public XmlMapperBuilder(SqlConfiguration configration) {
this.configration = configration;
}
/**
* 解析mapper.xml,并将结果封装到configration的mappedStatementMap对象中
* @param in
* @throws DocumentException
*/
@SuppressWarnings(\"unchecked\")
public void parse(InputStream in) throws DocumentException {
// 读取mapper.xml配置文件
Document document = new SAXReader().read(in);
// 获取根标签:即<mapper>标签
Element rootElement = document.getRootElement();
// 获取mapper的namespace属性值
String namespace = rootElement.attributeValue(\"namespace\");
// 获取select、update、insert、delete标签内容
List<Element> list = rootElement.selectNodes(\"//select|//update|//insert|//delete\");
// 封装MappedStatement对象,并存放到configration中
for (Element ele : list) {
String id = ele.attributeValue(\"id\");
String resultType = ele.attributeValue(\"resultType\");
String paramterType = ele.attributeValue(\"paramterType\");
String sql = ele.getTextTrim();
MappedStatement mappedStatement = new MappedStatement();
mappedStatement.setId(id);
mappedStatement.setParameterType(paramterType);
mappedStatement.setResultType(resultType);
mappedStatement.setSql(sql);
// key为namepace.id
String key = namespace + \".\" + id;
configration.getMappedStatementMap().put(key, mappedStatement);
}
}
}
这个时候解析信息已经保存在SqlConfiguration
配置类中的map里面了,key为命名空间+方法名,这样我们就可以通过调用接口方法定位到要执行哪个sql操作了。但是注意,这个时候保存的sql还存在占位符的,后续还需要做处理。
创建Executor接口和实现,实现jdbc的增删改查:
public interface Executor {
<T> List<T> query(SqlConfiguration configuration, MappedStatement statement,Object... params) throws Exception;
int executeUpdate(SqlConfiguration configuration, MappedStatement statement,Object... params) throws Exception;
}
实现:
@Slf4j
public class DefaultExecutor implements Executor{
@Override
public <T> List<T> query(SqlConfiguration configuration, MappedStatement statement, Object... params) throws Exception {
PreparedStatement preparedStatement = preparedStatement(configuration, statement, params);
ResultSet resultSet = preparedStatement.executeQuery();
String resultType = statement.getResultType();
Class<?> classType = getClassType(resultType);
List<T> list = new ArrayList<>();
while (resultSet.next()){
ResultSetMetaData metaData = resultSet.getMetaData();
Object resultObject = classType.newInstance();
for (int i = 1; i <= metaData.getColumnCount(); i++) {
String columnName = metaData.getColumnName(i);
Object value = resultSet.getObject(columnName);
PropertyDescriptor descriptor = new PropertyDescriptor(columnName,classType);
Method writeMethod = descriptor.getWriteMethod();
//对带有指定参数的指定对象调用由此方法,此处相当于调用set方法
writeMethod.invoke(resultObject,value);
}
T transform = (T) resultObject;
list.add(transform);
}
return list;
}
@Override
public int executeUpdate(SqlConfiguration configuration, MappedStatement statement, Object... params) throws Exception {
PreparedStatement preparedStatement = preparedStatement(configuration, statement, params);
return preparedStatement.executeUpdate();
}
/**
* 转换sql,设置sql参数
*/
private PreparedStatement preparedStatement(SqlConfiguration configuration, MappedStatement statement, Object... params){
String sql = statement.getSql();
BoundSql boundSql = boundSql(sql);
log.debug(\"执行sql: {}\",boundSql.getSql());
try {
Connection connection = configuration.getDataSource().getConnection();
PreparedStatement preparedStatement = connection.prepareStatement(boundSql.getSql());
String parameterType = statement.getParameterType();
if (StringUtils.isBlank(parameterType)){
return preparedStatement;
}
Class<?> classType = getClassType(parameterType);
List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
StringBuilder parameterStr = new StringBuilder();
for (int i = 0; i < parameterMappings.size(); i++) {
ParameterMapping parameterMapping = parameterMappings.get(i);
String content = parameterMapping.getContent();
if (isCommonDataType(classType) || isWrapClass(classType)){
preparedStatement.setObject(i+1,params[0]);
parameterStr.append(content.concat(\"=\").concat(params[0].toString()).concat(\",\"));
}else{
Field field = classType.getDeclaredField(content);
//获取private的属性
field.setAccessible(true);
Object o = field.get(params[0]);
preparedStatement.setObject(i+1,o);
parameterStr.append(content.concat(\"=\").concat(o.toString()).concat(\" \"));
}
}
log.info(\"参数: {}\",parameterStr);
return preparedStatement;
}catch (Exception e){
e.printStackTrace();
throw new FrameWorkException(\"创建PreparedStatement失败\");
}
}
private BoundSql boundSql(String sql){
// 主要是存储下面解析出来的参数名
ParameterMappingTokenHandler parameterMappingTokenHandler = new ParameterMappingTokenHandler();
// 标记解析器:解析#{}占位符
GenericTokenParser genericTokenParser = new GenericTokenParser(\"#{\", \"}\", parameterMappingTokenHandler);
// 解析sql语句,并返回,此时#{***}已经转换成了?
String parseSql = genericTokenParser.parse(sql);
// #{***}中解析出来的参数名称
List<ParameterMapping> parameterMappings = parameterMappingTokenHandler.getParameterMappings();
return new BoundSql(parseSql, parameterMappings);
}
/**
* 根据全路径获取Class
*/
private Class<?> getClassType(String parameterType) {
try {
return Class.forName(parameterType);
} catch (ClassNotFoundException e) {
e.printStackTrace();
throw new FrameWorkException(\"无法获取参数类型,请检查xml文件的参数类型是否正确\");
}
}
/**
* 判断是否是基础数据类型,即 int,double,long等类似格式
*/
private boolean isCommonDataType(Class<?> clazz) {
return clazz.isPrimitive();
}
/**
* 判断是否为基本类型的包装类
*/
private boolean isWrapClass(Class<?> clazz) {
try {
return ((Class<?>) clazz.getField(\"TYPE\").get(null)).isPrimitive();
} catch (Exception e) {
return false;
}
}
}
这个类里面的代码比较多,我们重点关注preparedStatement
方法,主要做了几件事:
boundSql
,boundSql
主要作用就是将:select * from user where id = #{id}
这样的语句转换为select * from user where id = ?
,BoundSql
定义为:@Data
public class BoundSql {
/**
* 解析后的sql语句:select * from user where username = ?
*/
private String sql;
/**
* 原始sql语句#{***}中的参数名称集合
*/
private List<ParameterMapping> parameterMappings;
public BoundSql(String sql, List<ParameterMapping> parameterMappings) {
this.sql = sql;
this.parameterMappings = parameterMappings;
}
}
//ParameterMapping目前只定义了参数名,后面可拓展保存参数类型、数据库类型
@Data
public class ParameterMapping {
private String content;
public ParameterMapping(String content) {
this.content = content;
}
}
PreparedStatement
mappedStatement
中拿到参数的全路径,以及前面处理返回的参数变量名称集合parameterMappings
;循环遍历parameterMappings
设置参数。preparedStatement
最后拿着这个preparedStatement
做具体的查询/更新操作。
我们提供给使用端这样的Executor还是比较复杂,所以再做一次封装,封装一个SqlSession
:
public interface SqlSession {
<T> List<T> selectList(String statementId, Object... params) throws Exception;
<T> T selectOne(String statementId, Object... params) throws Exception;
int update(String statementId, Object... params) throws Exception;
<T> T getMapper(Class<?> mapperClass);
}
实现:
public class DefaultSqlSession implements SqlSession{
private SqlConfiguration configuration;
public DefaultSqlSession(SqlConfiguration configuration) {
this.configuration = configuration;
}
@Override
public <T> List<T> selectList(String statementId, Object... params) throws Exception {
MappedStatement mappedStatement = configuration.getMappedStatementMap().get(statementId);
DefaultExecutor defaultExecutor = new DefaultExecutor();
return defaultExecutor.query(configuration,mappedStatement,params);
}
@Override
public <T> T selectOne(String statementId, Object... params) throws Exception {
MappedStatement mappedStatement = configuration.getMappedStatementMap().get(statementId);
DefaultExecutor defaultExecutor = new DefaultExecutor();
List<Object> query = defaultExecutor.query(configuration, mappedStatement, params);
if (query != null && query.size() > 0){
return (T) query.get(0);
}
return null;
}
@Override
public int update(String statementId, Object... params) throws Exception {
MappedStatement mappedStatement = configuration.getMappedStatementMap().get(statementId);
DefaultExecutor defaultExecutor = new DefaultExecutor();
return defaultExecutor.executeUpdate(configuration,mappedStatement,params);
}
/**
* 使用动态代理给mapper接口生成代理类
* @param mapperClass
* @param <T>
* @return
*/
@Override
public <T> T getMapper(Class<?> mapperClass){
Object proxyInstance = Proxy.newProxyInstance(DefaultSqlSession.class.getClassLoader(), new Class[]{mapperClass}, new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
//这个地方获取到的是mapper类的全限定类名,我们的statementId为namespace+id。所以必须规定mapper.xml里的命名空间也是这个
String className = method.getDeclaringClass().getName();
String methodName = method.getName();
String statementId = className.concat(\".\").concat(methodName);
if (StringUtils.startsWith(methodName,\"select\") || StringUtils.startsWith(methodName,\"find\")){
//获取调用方法返回类型
Type type = method.getGenericReturnType();
//是否有泛型参数
if (type instanceof ParameterizedType){
return selectList(statementId,args);
}
return selectOne(statementId,args);
}else {
return update(statementId,args);
}
}
});
return (T) proxyInstance;
}
}
重点看getMapper
方法,这里使用了动态代理,在调用使用端mapper接口的时候,会根据方法名判断需要执行查询还是更新操作。至此,框架端的基本雏形已经有了,现在可以在使用端使用了!
在使用端新建一个测试类:
@Slf4j
public class Main {
private static SqlSession sqlSession;
@BeforeAll
public static void init() throws Exception{
SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(\"sqlConfig.xml\");
sqlSession = factory.openSqlSession();
}
@Test
void insertTest() throws Exception{
User user = new User();
user.setId(2);
user.setUsername(\"李四\");
UserMapper mapper = sqlSession.getMapper(UserMapper.class);
int count = mapper.update(user);
log.info(\"增加单个: {}\",count);
}
@Test
void sqlSessionTest() throws Exception{
User user = new User();
user.setId(1);
UserMapper mapper = sqlSession.getMapper(UserMapper.class);
User one = mapper.selectUserById(1);
log.info(\"查询单个: {}\",one.toString());
}
@Test
void selectListTest() throws Exception{
UserMapper mapper = sqlSession.getMapper(UserMapper.class);
List<User> list = mapper.selectAll();
list.forEach(item ->log.info(\"查询: {}\",item));
}
}
执行结果:
INFO [main] (DefaultExecutor.java:89) - 参数: username=李四 id=2
INFO [main] (Main.java:36) - 增加单个: 1
INFO [main] (DefaultExecutor.java:89) - 参数: id=1,
INFO [main] (Main.java:45) - 查询单个: User(id=1, username=张三)
INFO [main] (Main.java:52) - 查询: User(id=1, username=张三)
INFO [main] (Main.java:52) - 查询: User(id=2, username=李四)
整个框架的设计中,虽然框架只是初具雏形,但是其中涉及到的知识点挺多的。对后面理解mybatis
原理也有一定作用。
用到的知识点有:
代码已上传至Gitee:PresistenceFramework
版权说明 : 本文为转载文章, 版权归原作者所有 版权申明
原文链接 : https://blog.lingxiaomz.top/articleContent/?id=105989192970929343
内容来源于网络,如有侵权,请联系作者删除!