1. JDBC 的使用问题
代码示例:
public class JDBCTest {
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", "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 user = new 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();
}
}
}
}
}
下面剖开代码,逐个分析其中问题:
(1)加载驱动,获取连接
存在问题1:数据库配置信息存在硬编码问题。
优化思路:使用配置文件!
存在问题2:频繁创建、释放数据库连接问题。
优化思路:使用数据连接池!
(2)定义sql、设置参数、执行查询
存在问题3:SQL语句、设置参数、获取结果集参数均存在硬编码问题 。
优化思路:使用配置文件!
(3)遍历查询结果集
存在问题4:手动封装返回结果集,较为繁琐。
优化思路:使用 Java 反射、内省!
针对 JDBC 各个环节中存在的不足,现在我们整理出对应的优化思路,统一汇总:
存在问题 | 优化思路 |
---|---|
数据库配置信息存在硬编码问题 | 使用配置文件 |
频繁创建、释放数据库连接问题 | 使用数据连接池 |
SQL语句、设置参数、获取结果集参数均存在硬编码问题 | 使用配置文件 |
手动封装返回结果集,较为繁琐 | 使用 Java 反射、内省 |
2. 自定义框架分析
JDBC 是个人作战,凡事亲力亲为,低效而高险,自己加载驱动,自己建连接,自己做所有事。而持久层框架好比是多工种协作,分工明确,执行高效,有专门负责解析注册驱动建立连接的,有专门管理数据连接池的,有专门执行sql语句的,有专门做预处理参数的,有专门装配结果集的 …
框架的作用,就是为了帮助我们减去繁重开发细节与冗余代码,使我们能更加专注于业务应用开发。
- 配置数据源(地址/数据名/用户名/密码)===> 全局配置文件
- 编写SQL与参数准备(SQL语句/参数类型/返回值类型)===> SQL 映射文件
持久层框架对比:
- Mybatis:SQL 映射框架;半自动化的持久层框架
- Hibernate:ORM 框架;全自动化的持久层框架
框架,除了思考本身的工程设计,还需要考虑到实际项目端的使用场景,干系方涉及两端:使用端(实际项目)、持久层框架本身。手写持久层框架基本思路图如下所示。
核心接口/类重点说明:
分工协作 | 角色定位 | 类名定义 |
---|---|---|
负责读取配置文件 | 资源辅助类 | Resources |
负责存储数据库连接信息 | 数据库资源类 | Configuration |
负责存储 SQL 映射定义、存储结果集映射定义 | SQL 与结果集资源类 | MappedStatement |
负责解析配置文件,创建会话工厂 SqlSessionFactory | 会话工厂构建者 | SqlSessionFactoryBuilder |
负责创建会话 SqlSession | 会话工厂 | SqlSessionFactory |
指派执行器 Executor | 会话 | SqlSession |
负责执行 SQL (配合指定资源 MappedStatement) | 执行器 | Executor |
正常来说项目只对应一套数据库环境,一般对应一个 SqlSessionFactory 实例对象,我们使用单例模式只创建一个 SqlSessionFactory 实例。如果需要配置多套数据库环境,那需要做一些拓展,例如 Mybatis 中通过 environments 等配置就可以支持多套测试/生产数据库环境进行切换。
2.1 项目使用端
(1)调用框架 API,需引入自定义持久层框架的 jar 包
(2)提供两部分配置信息
- sqlMapConfig.xml : 数据库配置信息(地址/数据名/用户名/密码),以及 mapper.xml 的全路径;
- mapper.xml : SQL 配置信息,存放 SQL 语句、参数类型、返回值类型相关信息。
2.2 持久层框架本身
3. 框架设计
3.1 使用端设计
(1)引入 ipersistent 即自定义持久化框架的依赖
(2)创建 sqlMapConfig.xml
<configuration>
<!-- 1.配置数据库信息 -->
<dataSource>
<property name="driverClassName" value="com.mysql.jdbc.Driver"></property>
<property name="url" value="jdbc:mysql:///ipersistent?useSSL=false&characterEncoding=UTF-8"></property>
<property name="username" value="root"></property>
<property name="password" value="root"></property>
</dataSource>
<!-- 2.引入映射配置文件 -->
<mappers>
<mapper resource="mapper/UserMapper.xml"></mapper>
</mappers>
</configuration>
(2)创建 mapper/UserMapper.xml
<mapper namespace="com.itheima.dao.IUserDao">
<!-- 唯一标识:namespace.id statementId -->
<!-- 查询所有 -->
<!--
规范:接口的全路径要和namespace的值保持一致
接口中的方法名要和id的值保持一致
-->
<select id="findAll" resultType="com.itheima.pojo.User">
select * from user
</select>
<!-- 按条件进行查询 -->
<!--
User user = new User();
user.setId(1);
user.setUserName("tom");
-->
<select id="findByCondition" resultType="com.itheima.pojo.User" parameterType="com.itheima.pojo.User">
select * from user where id = #{id} and username = #{username}
</select>
</mapper>
(3)创建 User 实体
package com.itheima.pojo;
public class User {
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 + '\'' +
'}';
}
}
(4)创建 Dao
package com.itheima.dao;
import com.itheima.pojo.User;
import java.util.List;
public interface IUserDao {
/**
* 查询所有
*/
List<User> findAll() throws Exception;
/**
* 根据多条件查询
*/
User findByCondition(User user) throws Exception;
}
3.2 框架类图
4. 框架实现
4.1 配置装载类
a. Resources
package com.itheima.io;
public class Resources {
/**
* 根据配置文件的路径,加载配置文件成字节输入流,存到内存中
*/
public static InputStream getResourceAsSteam(String path) {
InputStream resourceAsStream = Resources.class.getClassLoader().getResourceAsStream(path);
return resourceAsStream;
}
}
b. Configuration
package com.itheima.pojo;
/**
* 全局配置类:存放核心配置文件解析出来的内容
*/
public class Configuration {
/**
* 数据源对象
*/
private DataSource dataSource;
/**
* [key] statementId:namespace.id
* [val] 封装好的MappedStatement对象
*/
private Map<String, MappedStatement> mappedStatementMap = new HashMap();
// 省略 Getter/Setter ...
}
c. MappedStatement
package com.itheima.pojo;
/**
* 映射配置类:存放mapper.xml解析内容
*/
public class MappedStatement {
/**
* 唯一标识(statementId:namespace.id)
*/
private String statementId;
/**
* 返回值类型
*/
private String resultType;
/**
* 参数值类型
*/
private String parameterType;
/**
* SQL语句
*/
private String sql;
/**
* 判断当前是什么操作的一个属性
*/
private String sqlCommandType;
// 省略 Getter/Setter ...
}
4.2 装载配置
a. SqlSessionFactoryBuilder
package com.itheima.sqlSession;
public class SqlSessionFactoryBuilder {
/**
* 1.解析配置文件,封装容器对象
* 2.创建SqlSessionFactory工厂对象
*/
public SqlSessionFactory build(InputStream inputStream) throws DocumentException {
// 1. ===> 解析配置文件,封装容器对象 (XMLConfigBuilder 专门解析核心配置文件的解析类)
XMLConfigBuilder xmlConfigBuilder = new XMLConfigBuilder();
Configuration configuration = xmlConfigBuilder.parse(inputStream);
// 2. 创建SqlSessionFactory工厂对象
return new DefaultSqlSessionFactory(configuration);
}
}
b. XMLConfigBuilder
package com.itheima.config;
public class XMLConfigBuilder {
private Configuration configuration;
public XMLConfigBuilder() {
this.configuration = new Configuration();
}
/**
* 使用dom4j+xpath解析配置文件,封装Configuration对象
* 1. dataSource数据源对象
* 2. MappedStatement集合
*/
public Configuration parse(InputStream inputStream) throws DocumentException {
Document document = new SAXReader().read(inputStream);
Element rootElement = document.getRootElement();
List<Element> list = rootElement.selectNodes("//property");
// <property name="driverClassName" value="com.mysql.jdbc.Driver"></property>
Properties properties = new Properties();
for (Element element : list) {
String name = element.attributeValue("name");
String value = element.attributeValue("value");
properties.setProperty(name, value);
}
// 创建数据源对象
DruidDataSource druidDataSource = new DruidDataSource();
druidDataSource.setDriverClassName(properties.getProperty("driverClassName"));
druidDataSource.setUrl(properties.getProperty("url"));
druidDataSource.setUsername(properties.getProperty("username"));
druidDataSource.setPassword(properties.getProperty("password"));
// 创建好的数据源对象存入Configuration对象中
configuration.setDataSource(druidDataSource);
// ---------------------- 解析映射配置文件 ----------------------
// <mapper resource="mapper/UserMapper.xml"></mapper>
List<Element> mapperList = rootElement.selectNodes("//mapper");
for (Element element : mapperList) {
// a. 获取映射配置文件的路径
String mapperPath = element.attributeValue("resource");
InputStream resourceAsSteam = Resources.getResourceAsSteam(mapperPath);
// b. 根据路径进行映射配置文件的加载解析 (XMLMapperBuilder 专门解析映射配置文件的对象)
XMLMapperBuilder xmlMapperBuilder = new XMLMapperBuilder(configuration);
// c. 解析结果封装 MappedStatement 存入 Configuration.mappedStatementMap
xmlMapperBuilder.parse(resourceAsSteam);
}
return configuration;
}
}
c. XMLMapperBuilder
package com.itheima.config;
public class XMLMapperBuilder {
private Configuration configuration;
public XMLMapperBuilder(Configuration configuration) {
this.configuration = configuration;
}
/**
* 解析映射配置文件 -> MappedStatement -> Configuration.mappedStatementMap
*/
public void parse(InputStream resourceAsSteam) throws DocumentException {
// 1. 解析映射配置文件
Document document = new SAXReader().read(resourceAsSteam);
Element rootElement = document.getRootElement();
/*
* <select id="selectOne" resultType="com.itheima.pojo.User" parameterType="com.itheima.pojo.User">
* select * from user where id = #{id} and username = #{username}
* </select>
*/
List<Element> selectList = rootElement.selectNodes("//select");
String namespace = rootElement.attributeValue("namespace");
for (Element element : selectList) {
String id = element.attributeValue("id");
String resultType = element.attributeValue("resultType");
String parameterType = element.attributeValue("parameterType");
String sql = element.getTextTrim();
// 2. 封装MappedStatement对象
MappedStatement mappedStatement = new MappedStatement();
// statementId = namespace.id
String statementId = namespace + "." + id;
mappedStatement.setStatementId(statementId);
mappedStatement.setResultType(resultType);
mappedStatement.setParameterType(parameterType);
mappedStatement.setSql(sql);
mappedStatement.setSqlCommandType("select");
// 3. 将封装好的MappedStatement封装到configuration中的map集合中
configuration.getMappedStatementMap().put(statementId, mappedStatement);
}
}
}
4.3 创建 SqlSessionFactory
SqlSessionFactoryBuilder.build 操作的第一步是解析配置文件,封装 Configuration;第二步就是以 configuration 作为入参创建 SqlSessionFactory 工厂对象。
a. SqlSessionFactory
package com.itheima.sqlSession;
public interface SqlSessionFactory {
/**
* 1. 生产 SqlSession 对象
* 2. 创建 Executor 对象
*/
SqlSession openSession();
}
b. DefaultSqlSessionFactory
package com.itheima.sqlSession;
public class DefaultSqlSessionFactory implements SqlSessionFactory {
private Configuration configuration;
public DefaultSqlSessionFactory(Configuration configuration) {
this.configuration = configuration;
}
@Override
public SqlSession openSession() {
// 1.创建执行器对象
Executor simpleExecutor = new SimpleExecutor();
// 2.生产SqlSession对象
DefaultSqlSession defaultSqlSession = new DefaultSqlSession(configuration, simpleExecutor);
return defaultSqlSession;
}
}
4.4 创建 Executor
a. Executor
package com.itheima.executor;
public interface Executor {
<E> List<E> query(Configuration configuration, MappedStatement mappedStatement, Object param) throws Exception;
void close();
}
b. BoundSql
package com.itheima.config;
import com.itheima.utils.ParameterMapping;
import java.util.List;
public class BoundSql {
private String finalSql;
private List<ParameterMapping> parameterMappingList;
public BoundSql(String finalSql, List<ParameterMapping> parameterMappingList) {
this.finalSql = finalSql;
this.parameterMappingList = parameterMappingList;
}
public String getFinalSql() {
return finalSql;
}
public void setFinalSql(String finalSql) {
this.finalSql = finalSql;
}
public List<ParameterMapping> getParameterMappingList() {
return parameterMappingList;
}
public void setParameterMappingList(List<ParameterMapping> parameterMappingList) {
this.parameterMappingList = parameterMappingList;
}
}
c. SimpleExecutor
package com.itheima.executor;
public class SimpleExecutor implements Executor {
private Connection connection = null;
private PreparedStatement preparedStatement = null;
private ResultSet resultSet = null;
@Override
public <E> List<E> query(Configuration configuration, MappedStatement mappedStatement, Object param) throws Exception {
// 1. 加载驱动,获取数据库连接
connection = configuration.getDataSource().getConnection();
// 2. 获取PreparedStatement预编译对象
// 获取要执行的sql语句
/* 自定义的占位符
select * from user where id = #{id} and username = #{username}
替换: select * from user where id = ? and username = ?
解析替换的过程中:#{id}里面的值保存下来
*/
String sql = mappedStatement.getSql();
BoundSql boundSql = getBoundSql(sql);
String finalSql = boundSql.getFinalSql();
preparedStatement = connection.prepareStatement(finalSql);
// 3.设置参数
// com.itheima.pojo.User
String parameterType = mappedStatement.getParameterType();
if (parameterType != null) {
Class<?> parameterTypeClass = Class.forName(parameterType);
List<ParameterMapping> parameterMappingList = boundSql.getParameterMappingList();
for (int i = 0; i < parameterMappingList.size(); i++) {
ParameterMapping parameterMapping = parameterMappingList.get(i);
// id || username
String paramName = parameterMapping.getContent();
// 反射
Field declaredField = parameterTypeClass.getDeclaredField(paramName);
// 暴力访问
declaredField.setAccessible(true);
Object value = declaredField.get(param);
// 赋值占位符
preparedStatement.setObject(i + 1, value);
}
}
// 4.执行sql发起查询
resultSet = preparedStatement.executeQuery();
// 5.处理返回结果集
ArrayList<E> list = new ArrayList<>();
while (resultSet.next()) {
// 元数据信息包含了字段名、字段值
ResultSetMetaData metaData = resultSet.getMetaData();
String resultType = mappedStatement.getResultType();
Class<?> resultTypeClass = Class.forName(resultType);
Object o = resultTypeClass.newInstance();
for (int i = 1; i <= metaData.getColumnCount(); i++) {
// 字段名 id username
String columnName = metaData.getColumnName(i);
// 字段的值
Object value = resultSet.getObject(columnName);
// 问题:现在要封装到哪一个实体中
// 属性描述器:通过API方法获取某个属性的读写方法
PropertyDescriptor propertyDescriptor = new PropertyDescriptor(columnName, resultTypeClass);
Method writeMethod = propertyDescriptor.getWriteMethod();
// 参数1:实例对象 参数2:要设置的值
writeMethod.invoke(o, value);
}
list.add((E) o);
}
return list;
}
/**
* 1. #{} 占位符替换成?
* 2. 解析替换的过程中 将 #{} 里的值保存下来
*
* @param sql
* @return
*/
private BoundSql getBoundSql(String sql) {
// 1. 创建标记处理器:配合标记解析器完成标记的处理解析工作
ParameterMappingTokenHandler parameterMappingTokenHandler = new ParameterMappingTokenHandler();
// 2. 创建标记解析器
GenericTokenParser genericTokenParser = new GenericTokenParser("#{", "}", parameterMappingTokenHandler);
// #{} 占位符替换成 ?
String finalSql = genericTokenParser.parse(sql);
// 解析替换的过程中,将 #{} 里的值保存下来
List<ParameterMapping> parameterMappings = parameterMappingTokenHandler.getParameterMappings();
BoundSql boundSql = new BoundSql(finalSql, parameterMappings);
return boundSql;
}
/**
* 释放资源
*/
@Override
public void close() {
// 释放资源
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();
}
}
}
}
4.5 创建 SqlSession
a. SqlSession
package com.itheima.sqlSession;
public interface SqlSession {
/**
* 生成代理对象
*/
<T> T getMapper(Class<?> mapperClass);
/**
* 查询多个结果
* sqlSession.selectList() 定位到要执行的sql语句,从而执行
* select * from user where username like '% ? %'
*/
<E> List<E> selectList(String statementId, Object param) throws Exception;
/**
* 查询单个结果
*/
<T> T selectOne(String statementId, Object param) throws Exception;
/**
* 清除资源
*/
void close();
}
b. DefaultSqlSession
package com.itheima.sqlSession;
public class DefaultSqlSession implements SqlSession {
private Configuration configuration;
private Executor executor;
public DefaultSqlSession(Configuration configuration, Executor executor) {
this.configuration = configuration;
this.executor = executor;
}
@Override
public <T> T getMapper(Class<?> mapperClass) {
// 使用JDK动态代理生成基于接口的代理对象
Object proxy = Proxy.newProxyInstance(DefaultSqlSession.class.getClassLoader(),
new Class[]{mapperClass}, (object, method, args) -> {
// 具体的逻辑 :执行底层的JDBC
// 通过调用 SqlSession 里的方法来完成方法调用
// 参数的准备:1.statementId: com.itheima.dao.IUserDao.findAll 2.param
// [问题1] 如何获取statementId?
// => findAll
String methodName = method.getName();
// => com.itheima.dao.IUserDao
String className = method.getDeclaringClass().getName();
// => 组装
String statementId = className + "." + methodName;
// [问题2] 要调用sqlSession中增删改查的什么方法呢?
MappedStatement mappedStatement = configuration.getMappedStatementMap().get(statementId);
// => sqlCommandType: select/update/delete/insert
String sqlCommandType = mappedStatement.getSqlCommandType();
switch (sqlCommandType) {
case "select":
// 执行查询方法调用
// [问题3] 该调用selectList还是selectOne?
Type genericReturnType = method.getGenericReturnType();
// => 判断是否实现了 泛型类型参数化
// => ParameterizedType represents a parameterized type such as Collection<String>.
if (genericReturnType instanceof ParameterizedType) {
if (args != null) {
return selectList(statementId, args[0]);
}
return selectList(statementId, null);
}
return selectOne(statementId, args[0]);
case "update":
// TODO 执行update方法调用
break;
case "delete":
// TODO 执行delete方法调用
break;
case "insert":
// TODO 执行insert方法调用
break;
}
return null;
});
return (T) proxy;
}
@Override
public <E> List<E> selectList(String statementId, Object param) throws Exception {
// 将查询操作委派给底层的执行器
// query(): 执行底层的JDBC 1.数据库配置信息 2.sql配置信息
MappedStatement mappedStatement = configuration.getMappedStatementMap().get(statementId);
List<E> list = executor.query(configuration, mappedStatement, param);
return list;
}
@Override
public <T> T selectOne(String statementId, Object param) throws Exception {
List<Object> list = this.selectList(statementId, param);
if (list.size() == 1) {
return (T) list.get(0);
} else if (list.size() > 1) {
throw new RuntimeException("result.size > 1!");
} else {
return null;
}
}
@Override
public void close() {
executor.close();
}
}
4.6 工具类
a. GenericTokenParser
package com.itheima.utils;
/**
* @author Clinton Begin
*/
public class GenericTokenParser {
private final String openToken; // 开始标记
private final String closeToken; // 结束标记
private final TokenHandler handler; // 标记处理器
public GenericTokenParser(String openToken, String closeToken, TokenHandler handler) {
this.openToken = openToken;
this.closeToken = closeToken;
this.handler = handler;
}
/**
* 解析${}和#{}
*
* @param text
* @return 该方法主要实现了配置文件、脚本等片段中占位符的解析、处理工作,并返回最终需要的数据。
* 其中,解析工作由该方法完成,处理工作是由处理器handler的handleToken()方法来实现
*/
public String parse(String text) {
// 验证参数问题,如果是null,就返回空字符串。
if (text == null || text.isEmpty()) {
return "";
}
// 下面继续验证是否包含开始标签,如果不包含,默认不是占位符,直接原样返回即可,否则继续执行。
int start = text.indexOf(openToken, 0);
if (start == -1) {
return text;
}
// 把text转成字符数组src,并且定义默认偏移量offset=0、存储最终需要返回字符串的变量builder,
// text变量中占位符对应的变量名expression。判断start是否大于-1(即text中是否存在openToken),如果存在就执行下面代码
char[] src = text.toCharArray();
int offset = 0;
final StringBuilder builder = new StringBuilder();
StringBuilder expression = null;
while (start > -1) {
// 判断如果开始标记前如果有转义字符,就不作为openToken进行处理,否则继续处理
if (start > 0 && src[start - 1] == '\\') {
builder.append(src, offset, start - offset - 1).append(openToken);
offset = start + openToken.length();
} else {
// 重置expression变量,避免空指针或者老数据干扰。
if (expression == null) {
expression = new StringBuilder();
} else {
expression.setLength(0);
}
builder.append(src, offset, start - offset);
offset = start + openToken.length();
int end = text.indexOf(closeToken, offset);
while (end > -1) {////存在结束标记时
if (end > offset && src[end - 1] == '\\') {
// 如果结束标记前面有转义字符时
// this close token is escaped. remove the backslash and continue.
expression.append(src, offset, end - offset - 1).append(closeToken);
offset = end + closeToken.length();
end = text.indexOf(closeToken, offset);
} else {
// 不存在转义字符,即需要作为参数进行处理
expression.append(src, offset, end - offset);
offset = end + closeToken.length();
break;
}
}
if (end == -1) {
// close token was not found.
builder.append(src, start, src.length - start);
offset = src.length;
} else {
// 首先根据参数的key(即expression)进行参数处理,返回?作为占位符
builder.append(handler.handleToken(expression.toString()));
offset = end + closeToken.length();
}
}
start = text.indexOf(openToken, offset);
}
if (offset < src.length) {
builder.append(src, offset, src.length - offset);
}
return builder.toString();
}
}
b. TokenHandler
接口
package com.itheima.utils;
public interface TokenHandler {
String handleToken(String content);
}
实现类
package com.itheima.utils;
import java.util.ArrayList;
import java.util.List;
public class ParameterMappingTokenHandler implements TokenHandler {
private List<ParameterMapping> parameterMappings = new ArrayList<ParameterMapping>();
// context 是参数名称 #{id} #{username}
public String handleToken(String content) {
parameterMappings.add(buildParameterMapping(content));
return "?";
}
private ParameterMapping buildParameterMapping(String content) {
ParameterMapping parameterMapping = new ParameterMapping(content);
return parameterMapping;
}
public List<ParameterMapping> getParameterMappings() {
return parameterMappings;
}
public void setParameterMappings(List<ParameterMapping> parameterMappings) {
this.parameterMappings = parameterMappings;
}
}
c. ParameterMapping
public class ParameterMapping {
private String content;
public ParameterMapping(String content) {
this.content = content;
}
public String getContent() {
return content;
}
public void setContent(String content) {
this.content = content;
}
}
标签:24,持久,String,自定义,private,id,return,configuration,public
From: https://www.cnblogs.com/liujiaqi1101/p/18153420