1.1分析jdbc操作
持久层:Java-EE三层体系结构中与数据库交互的一层,往往称为dao层
早期与数据库交互会直接使用jdbc,但是jdbc存在一些问题,于是就出现了持久层框架,
这些持久层框架是对jdbc的封装,规避了jdbc存在的问题。
1.下面是JDBC连接数据库,查询代码
public static void main(String[] args) {
Connection connection = null;
PreparedStatement preparedStatement = null;
ResultSet resultSet = null;
try {
// 加载数据库驱动
Class.forName("com.mysql.jdbc.Driver");
// 通过驱动管理类获取数据库链接
connection =
DriverManager.getConnection("jdbc:mysql://localhost:3306/mybatis?
characterEncoding=utf-8", "root", "root");
// 定义sql语句?表示占位符
String sql = "select * from user where username = ?";
// 获取预处理statement
preparedStatement = connection.prepareStatement(sql);
// 设置参数,第⼀个参数为sql语句中参数的序号(从1开始),第⼆个参数为设置的参数值
preparedStatement.setString(1, "tom");
// 向数据库发出sql执⾏查询,查询出结果集
resultSet = preparedStatement.executeQuery();
// 遍历查询结果集
while (resultSet.next()) {
int id = resultSet.getInt("id");
String username = resultSet.getString("username");
// 封装User
user.setId(id);
user.setUsername(username);
}
System.out.println(user);
}
} catch (Exception e) {
e.printStackTrace();
} finally {
// 释放资源
if (resultSet != null) {
try {
resultSet.close();
} catch (SQLException e) {
e.printStackTrace();
} }
if (preparedStatement != null) {
try {
preparedStatement.close();
} catch (SQLException e) {
e.printStackTrace();
} }
if (connection != null) {
try {
connection.close();
} catch (SQLException e) {
e.printStackTrace();
}
} }
JDBC源码分析以及问题解决方案
自定持久层框架(解决和规避jdbc存在问题)设计思路:
1.使用端(项目):引入自定义持久层框架的jar包
提供两部分配置信息:数据库配置信息,sql配置信息:sql语句,参数类型,返回值类型
使用配置文件来提供这两部分配置信息:
(1)sqlMapConfig.xml:寸法数据库的配置信息,存入mapper.xml的全路径
(2)mapper.xml:存放sql配置信息
2.自定义持久层框架本身:(工程):本质就是对jdbc代码进行封装
(1)加载配置文件:根据配置文件的路径,加载成字节输入流,存储在内存中
创建Resources类 方法:InputStream getResourceAsStream(String path)
(2)创建两个javaBean:(容器对象):存放的就是对配置文件解析出来的内容
Configuration:核心配置类:存放sqlMapConfig.xml解析出来的内容
MappedStatement:映射配置类:存放mapper.xml解析出来的内容
(3)解析配置文件:dom4j(xml解析技术)
创建类:SqlSessionFactoryBuilder 方法:build(InputStream in)
第一:使用dom4j解析配置文件,将解析出来的内容封装到容器对象中
第二:创建sqlSessionFactory对象;生产sqlSession:会话对象(工厂模式)
(4)创建SqlSessionFactory接口及实现类DefaultSqlSessionFactory
第一:openSession():生产sqlSession
(5)创建SqlSession接口及实现类DefaultSession
定义对数据库的crud操作:selectList()
selectOne()
update()
delete()
(6)创建Executor接口及实现类SimpleExecutor实现类
query(Configuation,MappedStatement,Object....params):执行的就是JDBC代码
3.创建使用端项目,创建一个maven项目IPersistence_test,创建三个xml文件
sqlMapConfig.xml,以下所有的标签都是自定义的
<configuration> <!-- 数据库配置信息--> <dataSource> <property name="dirverClass" value="com.mysql.jdbc.Driver"></property> <property name="jdbcUrl" value="jdbc:mysql:///zdy_mybatis"></property> <property name="username" value="root"></property> <property name="password" value="123456"></property> </dataSource> <!--存放mapper.xml的全路径--> <mapper resource="UserMapper.xml"></mapper> </configuration>
UserMapper.xml
<mapper namespace="user"> <!--sql的唯一表示:namespace.id来组成: statementId--> <select id="selectList" resultType="com.ch.pojo.User"> select * from user </select> <!-- User user=new User() user.setId(1); user.setUsername("zhangsan")--> <select id="selectOne" resultType="com.ch.pojo.User" parameterType="com.ch.pojo.User"> select * from user where id=#{id} and username=#{username} </select> </mapper>
ProductMapper.xml
<mapper namespace="product"> <select id="selectList"> select * from product </select> <select id="selectOne"> select * from product where id=? and username=? </select> </mapper>
创建User实体类
package com.ch.pojo;
public class User {
private int id;
private String username;
public int getId() {
return id;
}
public void setId(int 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 + '\'' +
'}';
}
}
4.自定义持久层框架本身,创建Maven项目IPersistence
引入相关依赖,mysql连接,数据库连接池,还有dom4j
<dependencies> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>5.1.17</version> </dependency> <dependency> <groupId>c3p0</groupId> <artifactId>c3p0</artifactId> <version>0.9.1.2</version> </dependency> <dependency> <groupId>log4j</groupId> <artifactId>log4j</artifactId> <version>1.2.12</version> </dependency> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.10</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> </dependencies>
(1)加载配置文件:根据配置文件的路径,加载成字节输入流,存储在内存中
创建Resources类 方法:InputStream getResourceAsStream(String path)
package com.ch.io;
import java.io.InputStream;
public class Resources {
//根据配置文件的路径,将配置文件加载成字节输入流,存储在内存中
public static InputStream getResourcesAsStream(String path){
InputStream resourceAsStream = Resources.class.getClassLoader().getResourceAsStream(path);
return resourceAsStream;
}
}
(2)创建两个javaBean:(容器对象):存放的就是对配置文件解析出来的内容
Configuration:核心配置类:存放sqlMapConfig.xml解析出来的内容
MappedStatement:映射配置类:存放mapper.xml解析出来的内容
package com.ch.pojo;
import javax.sql.DataSource;
import java.util.HashMap;
import java.util.Map;
public class Configuration {
private DataSource dataSource;
/*
key:statementid value:封装好的mappedStatement对象
*/
Map<String,MappedStatement>mappedStatementMap=new HashMap<String, MappedStatement>();
public DataSource getDataSource() {
return dataSource;
}
public void setDataSource(DataSource dataSource) {
this.dataSource = dataSource;
}
public Map<String, MappedStatement> getMappedStatementMap() {
return mappedStatementMap;
}
public void setMappedStatementMap(Map<String, MappedStatement> mappedStatementMap) {
this.mappedStatementMap = mappedStatementMap;
}
}
package com.ch.pojo;
public class MappedStatement {
//id标识
private String id;
//返回值类型
private String resultType;
//参数值类型
private String parameterType;
//sql语句
private String sql;
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 getParameterType() {
return parameterType;
}
public void setParameterType(String parameterType) {
this.parameterType = parameterType;
}
public String getSql() {
return sql;
}
public void setSql(String sql) {
this.sql = sql;
}
}
(3)解析配置文件:dom4j(xml解析技术)
创建类:SqlSessionFactoryBuilder 方法:build(InputStream in)
第一:使用dom4j解析配置文件,将解析出来的内容封装到容器对象中
第二:创建sqlSessionFactory对象;生产sqlSession:会话对象(工厂模式)
package com.ch.sqlSession;
import com.ch.config.XMLConfigBuilder;
import com.ch.pojo.Configuration;
import org.dom4j.DocumentException;
import java.beans.PropertyVetoException;
import java.io.InputStream;
public class SqlSessionFactoryBuilder {
public SqlSessionFactory build(InputStream in) throws DocumentException, PropertyVetoException {
//第一:使用dom4j解析配置文件,将解析出来的内容封装到Configuration中
XMLConfigBuilder xmlConfigBuilder = new XMLConfigBuilder();
Configuration configuration=xmlConfigBuilder.parseConfig(in);
//第二:创建sqlSessionFactory对象;工厂类:生产sqlSession对象
DefaultSqlSessionFactory defaultSqlSessionFactory = new DefaultSqlSessionFactory(configuration);
return defaultSqlSessionFactory;
}
}
package com.ch.config;
import com.ch.io.Resources;
import com.ch.pojo.Configuration;
import com.mchange.v2.c3p0.ComboPooledDataSource;
import org.dom4j.Document;
import org.dom4j.DocumentException;
import org.dom4j.Element;
import org.dom4j.io.SAXReader;
import java.beans.PropertyVetoException;
import java.io.InputStream;
import java.util.List;
import java.util.Properties;
public class XMLConfigBuilder {
private Configuration configuration;
public XMLConfigBuilder(){
this.configuration=new Configuration();
}
/*
该方法就是想配置文件进行解析,封装成Configuration
*/
public Configuration parseConfig(InputStream inputStream) throws DocumentException, PropertyVetoException {
Document document = new SAXReader().read(inputStream);
/* <configuration>*/
Element rootElement=document.getRootElement();
List<Element> list = rootElement.selectNodes("//property");
Properties properties = new Properties();
for (Element element:list) {
String name = element.attributeValue("name");
String value = element.attributeValue("value");
properties.setProperty(name,value);
}
ComboPooledDataSource comboPooledDataSource = new ComboPooledDataSource();
comboPooledDataSource.setDriverClass(properties.getProperty("dirverClass"));
comboPooledDataSource.setJdbcUrl(properties.getProperty("jdbcUrl"));
comboPooledDataSource.setUser(properties.getProperty("username"));
comboPooledDataSource.setPassword(properties.getProperty("password"));
configuration.setDataSource(comboPooledDataSource);
/* mapper.xml解析:拿到路径---字节输入流---dom4j进行解析*/
List<Element> mapperList= rootElement.selectNodes("//mapper");
for (Element element:mapperList) {
String mapperPath = element.attributeValue("resource");
InputStream resourcesAsStream = Resources.getResourcesAsStream(mapperPath);
XMLMapperBuilder xmlMapperBuildr = new XMLMapperBuilder(configuration);
xmlMapperBuildr.parse(resourcesAsStream);
}
return configuration;
}
}
package com.ch.config;
import com.ch.pojo.Configuration;
import com.ch.pojo.MappedStatement;
import org.dom4j.Document;
import org.dom4j.DocumentException;
import org.dom4j.Element;
import org.dom4j.io.SAXReader;
import java.io.InputStream;
import java.util.List;
public class XMLMapperBuilder {
private Configuration configuration;
public XMLMapperBuilder(Configuration configuration){
this.configuration=configuration;
}
public void parse(InputStream inputStream) throws DocumentException {
Document document = new SAXReader().read(inputStream);
Element rootElement = document.getRootElement();
String namesapce = rootElement.attributeValue("namespace");
List<Element> list = rootElement.selectNodes("//select");
for (Element element:list) {
String id = element.attributeValue("id");
String resultType = element.attributeValue("resultType");
String parameterType = element.attributeValue("parameterType");
String sqlText = element.getTextTrim();
MappedStatement mappedStatement = new MappedStatement();
mappedStatement.setId(id);
mappedStatement.setResultType(resultType);
mappedStatement.setParameterType(parameterType);
mappedStatement.setSql(sqlText);
String key=namesapce+"."+id;
System.out.println("key的值"+key);
configuration.getMappedStatementMap().put(key,mappedStatement);
}
}
}
package com.ch.sqlSession;
import java.util.List;
public interface SqlSession {
//查询所有
public <E> List<E>selectLsit(String statementId,Object... params) throws Exception;
//根据条件条件查询单个
public <T> T selectOne(String statementId,Object... params) throws Exception;
}
(4)创建SqlSessionFactory接口及实现类DefaultSqlSessionFactory
第一:openSession():生产sqlSession
package com.ch.sqlSession;
public interface SqlSessionFactory {
public SqlSession openSession();
}
package com.ch.sqlSession;
import com.ch.pojo.Configuration;
public class DefaultSqlSessionFactory implements SqlSessionFactory {
private Configuration configuration;
public DefaultSqlSessionFactory(Configuration configuration) {
this.configuration=configuration;
}
public SqlSession openSession() {
return new DefaultSqlSession(configuration);
}
}
package com.ch.sqlSession;
import java.util.List;
public interface SqlSession {
//查询所有
public <E> List<E>selectLsit(String statementId,Object... params) throws Exception;
//根据条件条件查询单个
public <T> T selectOne(String statementId,Object... params) throws Exception;
}
package com.ch.sqlSession;
import com.ch.pojo.Configuration;
import com.ch.pojo.MappedStatement;
import java.util.List;
public class DefaultSqlSession implements SqlSession {
private Configuration configuration;
public DefaultSqlSession(Configuration configuration){
this.configuration=configuration;
}
public <E> List<E> selectLsit(String statementId, Object... params) throws Exception {
//将要去完成simpleExecutor里的query方法的调用
SimpleExecutor simpleExecutor = new SimpleExecutor();
MappedStatement mappedStatement = configuration.getMappedStatementMap().get(statementId);
List<Object> list = simpleExecutor.query(configuration, mappedStatement, params);
return (List<E>) list;
}
public <T> T selectOne(String statementId, Object... params) throws Exception {
List<Object> objects = selectLsit(statementId, params);
if (objects.size()==1){
return (T) objects.get(0);
}else {
throw new RuntimeException("查询结果为空或者返回结果过多");
}
}
}
(5)创建SqlSession接口及实现类DefaultSession
定义对数据库的crud操作:selectList()
selectOne()
package com.ch.sqlSession;
import com.ch.pojo.Configuration;
import com.ch.pojo.MappedStatement;
import java.sql.SQLException;
import java.util.List;
public interface Executor {
public <E> List<E> query(Configuration configuration, MappedStatement mappedStatement,Object... params) throws SQLException, Exception;
}
package com.ch.sqlSession;
import com.ch.config.BoundSql;
import com.ch.pojo.Configuration;
import com.ch.pojo.MappedStatement;
import com.ch.utils.GenericTokenParser;
import com.ch.utils.ParameterMapping;
import com.ch.utils.ParameterMappingTokenHandler;
import com.ch.utils.TokenHandler;
import java.beans.PropertyDescriptor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.ResultSetMetaData;
import java.util.ArrayList;
import java.util.List;
public class SimpleExecutor implements Executor{ //user
public <E> List<E> query(Configuration configuration, MappedStatement mappedStatement, Object... params) throws Exception {
//1.注册驱动 获取连接
// Class.forName(configuration.getDataSource().)
Connection connection = configuration.getDataSource().getConnection();
//获取sql语句: select * from user where id=#{id} and username=#{username}
//转换sql语句:select * from user where id=? and username=?,还
String sql=mappedStatement.getSql();
BoundSql boundSql=getBoundSql(sql);
//3.获取预处理对象:preparedStatement
PreparedStatement preparedStatement = connection.prepareStatement(boundSql.getSqlText());
//4.设置参数
//String resultType = mappedStatement.getResultType();
String parameterType = mappedStatement.getParameterType();
Class<?> parametertypeClass=getClassType(parameterType);
List<ParameterMapping> parameterMappingList = boundSql.getParameterMappingList();
for (int i = 0; i <parameterMappingList.size() ; i++) {
ParameterMapping parameterMapping = parameterMappingList.get(i);
String content = parameterMapping.getContent();
//反射
Field declaredField = parametertypeClass.getDeclaredField(content);
//暴力访问
declaredField.setAccessible(true);
Object o = declaredField.get(params[0]);
preparedStatement.setObject(i+1,o);
}
//5.执行sql
ResultSet resultSet = preparedStatement.executeQuery();
String resultType = mappedStatement.getResultType();
Class<?> resulttypeClass = getClassType(resultType);
ArrayList<Object> objects = new ArrayList();
//6.封装返回结果集
while (resultSet.next()){
//元数据
Object o = resulttypeClass.newInstance();
ResultSetMetaData metaData = resultSet.getMetaData();
for (int i = 1; i < metaData.getColumnCount()+1; i++) {
//字段名
String columnName = metaData.getColumnName(i);
//字段值
Object value = resultSet.getObject(columnName);
//使用反射或者内省,根据数据库表和实体的对应关系,完成封装
PropertyDescriptor propertyDescriptor = new PropertyDescriptor(columnName, resulttypeClass);
Method writeMethod = propertyDescriptor.getWriteMethod();
writeMethod.invoke(o,value);
}
objects.add(o);
}
return (List<E>) objects;
}
private Class<?> getClassType(String parameterType) throws ClassNotFoundException {
if (parameterType!=null){
Class<?> aClass = Class.forName(parameterType);
return aClass;
}
return null;
}
/*完成对#{}的解析工作 1.将#{}使用?进行代替,2.解析出#{}里面的值进行存储
@param sql
* */
private BoundSql getBoundSql(String sql) {
//标记处理类,配合标记解析器来完成对占位符的解析
ParameterMappingTokenHandler parameterMappingTokenHandler = new ParameterMappingTokenHandler();
GenericTokenParser genericTokenParser = new GenericTokenParser("#{", "}", parameterMappingTokenHandler);
//解析出来的sql
String parseSql = genericTokenParser.parse(sql);
//#{}解析出来的参数名称
List<ParameterMapping> parameterMappings = parameterMappingTokenHandler.getParameterMappings();
BoundSql boundSql = new BoundSql(parseSql,parameterMappings);
return boundSql;
}
}
将IPersistence打包,在IPersistence_test项目中引入对IPersistence的依赖
<dependencies> <dependency> <groupId>com.ch</groupId> <artifactId>IPersistence</artifactId> <version>1.0-SNAPSHOT</version> </dependency> </dependencies>
测试
package com.ch.test;
import com.ch.io.Resources;
import com.ch.pojo.User;
import com.ch.sqlSession.SqlSession;
import com.ch.sqlSession.SqlSessionFactory;
import com.ch.sqlSession.SqlSessionFactoryBuilder;
import org.dom4j.DocumentException;
import org.junit.Test;
import java.beans.PropertyVetoException;
import java.io.InputStream;
import java.util.List;
public class IPersistenceTest {
@Test
public void test() throws Exception {
InputStream resourcesAsStream = Resources.getResourcesAsStream("sqlMapConfig.xml");
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(resourcesAsStream);
SqlSession sqlSession=sqlSessionFactory.openSession();
User user = new User();
user.setId(1);
user.setUsername("zhangsan");
/* User user2 = sqlSession.selectOne("user.selectOne", user);
System.out.println("查询到的值"+user2);*/
List<User> users = sqlSession.selectLsit("user.selectList", user);
for (int i = 0; i <users.size() ; i++) {
System.out.println(users.get(i));
}
}
}
运行结果
key的值user.selectList
key的值user.selectOne
User{id=1, username='zhangsan'}
User{id=2, username='lisi'}Process finished with exit code 0
再把测试接口封装到DAO层
package com.ch.dao;
import com.ch.pojo.User;
import org.dom4j.DocumentException;
import java.beans.PropertyVetoException;
import java.util.List;
public interface IUserDao {
//查询所有用户
public List<User>findAll() throws Exception;
//根据条件进行用户查询
public User findByCondition(User user) throws Exception;
}
package com.ch.dao;
import com.ch.io.Resources;
import com.ch.pojo.User;
import com.ch.sqlSession.SqlSession;
import com.ch.sqlSession.SqlSessionFactory;
import com.ch.sqlSession.SqlSessionFactoryBuilder;
import org.dom4j.DocumentException;
import java.beans.PropertyVetoException;
import java.io.InputStream;
import java.util.List;
public class UserDaoImpl implements IUserDao{
public List<User> findAll() throws Exception {
InputStream resourcesAsStream = Resources.getResourcesAsStream("sqlMapConfig.xml");
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(resourcesAsStream);
SqlSession sqlSession=sqlSessionFactory.openSession();
List<User> users = sqlSession.selectLsit("user.selectList");
return users;
}
public User findByCondition(User user) throws Exception {
InputStream resourcesAsStream = Resources.getResourcesAsStream("sqlMapConfig.xml");
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(resourcesAsStream);
SqlSession sqlSession=sqlSessionFactory.openSession();
User users = sqlSession.selectOne("user.selectList",user);
return user;
}
}
观察自定义持久层框架还存在一些问题:
1.Dao层使用自定义持久层框架,存在代码重复,整个操作的过程模板重复(加载配置文件,创建sqlSessionFactory,以及生产sqlSession)
2.存在硬编码问题,statementId
解决思路:
使用代理模式生成Dao层接口的代理实现类
在IPersistence项目的SqlSession中加入以下接口
public <T> T getMapper(Class<?> mapperClass);
具体实现
功能扩展-----动态代理invoke方法实现
proxy:当前代理对象的引用
method:当前被调用方法的引用
args:传递参数
修改原先的UserMapper.xml,使得与接口名和方法名保持一致,
<mapper namespace="com.ch.dao.IUserDao"> <!--sql的唯一表示:namespace.id来组成: statementId--> <select id="findAll" resultType="com.ch.pojo.User"> select * from user </select> <!-- User user=new User() user.setId(1); user.setUsername("zhangsan")--> <select id="findByCondition" resultType="com.ch.pojo.User" parameterType="com.ch.pojo.User"> select * from user where id=#{id} and username=#{username} </select> </mapper>
@Override
public <T> T getMapper(Class<?> mapperClass) {
//使用JDK动态代理来为Dao接口生成代理对象,并返回
Object proxyInstance = Proxy.newProxyInstance(DefaultSqlSession.class.getClassLoader(), new Class[]{mapperClass}, new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
//底层都还是去执行JDBC代码 //根据不同情况。来调用selectList或者selectOne
//准备参数:statementId:namespace.id=接口全限定名.方法名
String methodName = method.getName();
String className = method.getDeclaringClass().getName();
String statementId=className+"."+methodName;
//准备参数2,params:args
//获取被调用方法的返回值类型
Type genericReturnType = method.getGenericReturnType();
//判断是否进行了泛型类型参数化
if (genericReturnType instanceof ParameterizedType){
List<Object> objects = selectLsit(statementId, args);
return objects;
}
return selectOne(statementId,args);
}
});
return (T) proxyInstance;
}
测试代码
package com.ch.test; import com.ch.dao.IUserDao; import com.ch.io.Resources; import com.ch.pojo.User; import com.ch.sqlSession.SqlSession; import com.ch.sqlSession.SqlSessionFactory; import com.ch.sqlSession.SqlSessionFactoryBuilder; import org.dom4j.DocumentException; import org.junit.Test; import java.beans.PropertyVetoException; import java.io.InputStream; import java.util.List; public class IPersistenceTest { @Test public void test() throws Exception { InputStream resourcesAsStream = Resources.getResourcesAsStream("sqlMapConfig.xml"); SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(resourcesAsStream); SqlSession sqlSession=sqlSessionFactory.openSession(); User user = new User(); user.setId(1); user.setUsername("zhangsan"); /* User user2 = sqlSession.selectOne("user.selectOne", user); System.out.println("查询到的值"+user2);*/ /* List<User> users = sqlSession.selectLsit("user.selectList", user); for (int i = 0; i <users.size() ; i++) { System.out.println(users.get(i)); }*/ IUserDao userDao = sqlSession.getMapper(IUserDao.class); // userDao.findAll() User user2 = userDao.findByCondition(user); List<User> all = userDao.findAll(); System.out.println("用户信息"+user2); System.out.println("用户信息全部"+all); } }
结果:
key的值com.ch.dao.IUserDao.findAll
key的值com.ch.dao.IUserDao.findByCondition
用户信息User{id=1, username='zhangsan'}
用户信息全部[User{id=1, username='zhangsan'}, User{id=2, username='lisi'}]
项目下载地址:https://github.com/Moshubai1129/IPersistence_test
https://github.com/Moshubai1129/IPersistence
下一篇:https://blog.csdn.net/moshubai/article/details/115447910