前言
mybatis框架是我学习的第一个框架
框架即是许多工具类的集合
让我们更关心于代码的逻辑 需求实现的方法 省去了很多繁琐的重复的步骤
Mybatis
Mybatis是一个优秀的基于Java的持久层框架,它内部封装了Jdbc,使开发者只需要关注sql语句本身,而不需要花费精力去处理加载驱动、创建连接、创建statement等繁杂的过程。
public static void main(String[] args) throws ClassNotFoundException, SQLException {
//注册驱动
Class.forName("com.mysql.jdbc.Driver");
//获取连接
Connection connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/itpy_mybatis", "root", "root");
//准备sql 获取prepareStatement
PreparedStatement statement = connection.prepareStatement("select * from user");
//执行sql 获取resultSet
ResultSet resultSet = statement.executeQuery();
//处理结果
ArrayList<User> users = new ArrayList<>();
while (resultSet.next()){
User user = new User();
Object id = resultSet.getObject("id");
user.setId((Integer) id);
Object username = resultSet.getObject("username");
user.setUsername((String) username);
Object gender = resultSet.getObject("gender");
user.setGender((String) gender);
Object birthday = resultSet.getObject("birthday");
user.setBirthday((java.util.Date) birthday);
Object address = resultSet.getObject("address");
user.setAddress((String) address);
users.add(user);
}
//关闭资源
connection.close();
statement.close();
System.out.println(users);
}
显然若仅仅使用jdbc编程步骤十分繁琐且不便于修改
并且大部分代码都是固定的 只需要修改其中某些部分而已
故我们尝试封装代码来减少代码编写长度
尝试自定义编写框架
1.使用工厂模式以及构建者模式构建框架
首先创建一个SqlSession接口
其中提供了各种增删改查的抽象方法以供实现
public interface SqlSession {
<T>List<T> findAll(String statement);
}
SqlSession接口的默认实现类
public class DefaultSqlSession implements SqlSession {
@Override
public <T> List<T> findAll(String statement) {
return null;
}
}
创建SqlSession接口的实现类需要用到工厂类
提供openSession方法来创建SqlSession接口的实现类
public class SqlSessionFactory {
public SqlSession openSession(){
DefaultSqlSession DefaultSqlSession = new DefaultSqlSession(configuration);
return DefaultSqlSession;
}
}
SqlSessionFactoryBuilder用来构建工厂类
许多必须完成的操作可用放在此类中 如配置文件的解析及结果对象的封装
public class SqlSessionFactoryBuilder {
//默认选项
public SqlSessionFactory build(){
return null;
}
//文件改了位置
public SqlSessionFactory build(String path){
return null;
}
//直接给一个流
public SqlSessionFactory build(InputStream inputStream){
return null;
}
private void loadMapperXml(String location, HashMap<String, Sqlmap> sqls) {
}
private DataSource loadDataSource(Document document) {
return null;
}
}
框架的工作流程大致是获取配置文件中的dataSource连接池对象以及sql语句
在实现类中从连接池获取连接调用prepareStatement方法传入sql语句进行执行
再对查询到的结果封装
故关键就是获取DataSource对象和sql语句
所有我们把它们封装成一个实体类configuration的两个属性 以便作为参数传输
public class Configuration {
private DataSource dataSource;
private Map<String, Sqlmap> sqlMaps;
public Map<String, Sqlmap> getSqlMaps() {
return sqlMaps;
}
public void setSqlMaps(Map<String, Sqlmap> sqlMaps) {
this.sqlMaps = sqlMaps;
}
public DataSource getDataSource() {
return dataSource;
}
public void setDataSource(DataSource dataSource) {
this.dataSource = dataSource;
}
}
public class Sqlmap {
private String sql;
private Class clazz;
public Sqlmap(String sql, Class clazz) {
this.sql = sql;
this.clazz = clazz;
}
public Sqlmap() {
}
public Class getClazz() {
return clazz;
}
public void setClazz(Class clazz) {
this.clazz = clazz;
}
public String getSql() {
return sql;
}
public void setSql(String sql) {
this.sql = sql;
}
}
配置文件:
SqlMapConfig.xml:
<?xml version="1.0" encoding="UTF-8" ?>
<configuration>
<environments default="development">
<environment id="development">
<transactionManager type="JDBC" />
<dataSource type="POOLED">
<property name="driver" value="com.mysql.jdbc.Driver" />
<property name="url" value="jdbc:mysql://127.0.0.1:3306/itpy_mybatis?characterEncoding=utf8" />
<property name="username" value="root" />
<property name="password" value="root" />
</dataSource>
</environment>
</environments>
<mappers>
<mapper resource="UserMapper.xml"></mapper>
<mapper resource="ProductMapper.xml"></mapper>
</mappers>
</configuration>
UserMapper.xml:
<?xml version="1.0" encoding="utf-8" ?>
<mapper namespace="user">
<select id="findAll1" resultType="com.itpy.domain.User">
select * from user limit 0,2
</select>
<select id="findAll2" resultType="com.itpy.domain.User">
select * from user limit 2,2
</select>
</mapper>
2.代码补完
SqlSessionFactoryBuilder是最先被创建出来的 所以我们在这个类中封装解析配置文件的操作
public class SqlSessionFactoryBuilder {
//默认选项
public SqlSessionFactory build(){
InputStream resourceAsStream = SqlSessionFactoryBuilder.class.getClassLoader().getResourceAsStream("SqlMapConfig.xml");
return build(resourceAsStream);
}
//文件改了位置
public SqlSessionFactory build(String path){
InputStream resourceAsStream = SqlSessionFactoryBuilder.class.getClassLoader().getResourceAsStream(path);
return build(resourceAsStream);
}
//直接给一个流
public SqlSessionFactory build(InputStream inputStream){
//开始解析xml配置文件
SAXReader saxReader = new SAXReader();
try {
Document document = saxReader.read(inputStream);
//数据库连接池的相关解析
DataSource dataSource = loadDataSource(document);
//sql语句相关解析
Map<String, Sqlmap> sqls = loadSqls(document);
Configuration configuration = new Configuration();
configuration.setDataSource(dataSource);
configuration.setSqlMaps(sqls);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactory();
sqlSessionFactory.setConfiguration(configuration);
return sqlSessionFactory;
} catch (DocumentException e) {
e.printStackTrace();
}finally {
try {
inputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
return null;
}
private Map<String,Sqlmap> loadSqls(Document document) {
List<Element> list = document.selectNodes("//mapper");
HashMap<String, Sqlmap> sqls = new HashMap<>();
for (Element element : list) {
//获取元素的resource属性 代表另外的配置文件位置
String location = element.attributeValue("resource");
loadMapperXml(location,sqls);
}
return sqls;
}
private void loadMapperXml(String location, HashMap<String, Sqlmap> sqls) {
InputStream resourceAsStream = SqlSessionFactoryBuilder.class.getClassLoader().getResourceAsStream(location);
SAXReader saxReader = new SAXReader();
Document document = null;
try {
document = saxReader.read(resourceAsStream);
//获取namespace
String namespace = document.getRootElement().attributeValue("namespace");
List<Element> list = document.selectNodes("//select");
for (Element element : list) {
String id = element.attributeValue("id");
String key = namespace+"."+id;
String resultType = element.attributeValue("resultType");
Class<?> aClass = Class.forName(resultType);
String sql = element.getTextTrim();
sqls.put(key,new Sqlmap(sql,aClass));
}
} catch (DocumentException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
throw new RuntimeException("找不到类型",e.getException());
}
}
private DataSource loadDataSource(Document document) {
List<Element> list = document.selectNodes("//property");
HikariConfig config = new HikariConfig();
for (Element element : list) {
String name = element.attributeValue("name");
if ("driver".equals(name)){
String value = element.attributeValue("value");
config.setDriverClassName(value);
}
if ("url".equals(name)){
String value = element.attributeValue("value");
config.setJdbcUrl(value);
}
if ("username".equals(name)){
String value = element.attributeValue("value");
config.setUsername(value);
}
if ("password".equals(name)){
String value = element.attributeValue("value");
config.setPassword(value);
}
}
HikariDataSource dataSource = new HikariDataSource(config);
return dataSource;
}
}
数据库连接池解析loadDataSource时 没有使用c3p0连接池 使用的是HikariCP连接池
解析完成后 Configuration 在创建SqlSessionFactory类时也应传给SqlSessionFactory
故SqlSessionFactory类:
public class SqlSessionFactory {
private Configuration configuration;
public Configuration getConfiguration() {
return configuration;
}
public void setConfiguration(Configuration configuration) {
this.configuration = configuration;
}
public SqlSession openSession(){
DefaultSqlSession DefaultSqlSession = new DefaultSqlSession(configuration);
return DefaultSqlSession;
}
}
同样DefaultSqlSession的构造 也应传入此对象
DefaultSqlSession的作用主要是从连接池获取连接执行sql语句并把结果封装
public class DefaultSqlSession implements SqlSession {
private Configuration configuration;
public DefaultSqlSession(Configuration configuration) {
this.configuration = configuration;
}
@Override
public <T> List<T> findAll(String statement) {
//获取连接 使用数据连接池方案
try {
Connection connection = configuration.getDataSource().getConnection();
//获取prepareStatement(包含sql)
//获取sql语句
Sqlmap sqlmap = configuration.getSqlMaps().get(statement);
PreparedStatement preparedStatement = connection.prepareStatement(sqlmap.getSql());
ResultSet resultSet = preparedStatement.executeQuery();
//拿到我们想要封装成的类型
Class clazz = sqlmap.getClazz();
//处理结果 封装
List list = handleResultset(clazz,resultSet);
return list;
} catch (Exception throwables) {
throwables.printStackTrace();
}
return null;
}
private List handleResultset(Class clazz, ResultSet resultSet) throws SQLException, IllegalAccessException, InstantiationException {
List list = new ArrayList();
//想要一个clazz类型的list集合
//遍历resultset 封装到集合中
//获取元信息
ResultSetMetaData metaData = resultSet.getMetaData();
int columnCount = metaData.getColumnCount();
//想办法获取所有列的名字
List<String> columnNames = new ArrayList<>();
for (int i = 1; i <= columnCount; i++) {
columnNames.add(metaData.getColumnName(i));
}
while (resultSet.next()){
Object o = clazz.newInstance();
for (String columnName : columnNames) {
Object columnValue = resultSet.getObject(columnName);
ReflectorUtil.set(o,columnName,columnValue);
}
list.add(o);
}
return list;
}
}
其中ReflectorUtil是一个工具类 作用是把查询到的列名和其对应的内容拼装成一个实体类
public class ReflectorUtil {
private static Map<Class,BeanMethodInfo> cache=new HashMap<>();
private static BeanMethodInfo getBeanMethodInfo(Object o){
Class<?> oClass = o.getClass();
BeanMethodInfo beanMethodInfo = cache.get(oClass);
if (beanMethodInfo==null){
beanMethodInfo = new BeanMethodInfo(o.getClass());
cache.put(oClass,beanMethodInfo);
}
return beanMethodInfo;
}
public static void set(Object o,String propertyName,Object propertyValue){
BeanMethodInfo beanMethodInfo = getBeanMethodInfo(o);
Method method = beanMethodInfo.setterMethod(propertyName);
try {
method.invoke(o,propertyValue);
} catch (Exception e) {
throw new RuntimeException("set--"+propertyName+"--property failed!",e);
}
}
public static Object get(Object o,String propertyName){
BeanMethodInfo beanMethodInfo = getBeanMethodInfo(o);
Method method = beanMethodInfo.getterMethod(propertyName);
try {
return method.invoke(o);
} catch (Exception e) {
throw new RuntimeException("get--"+propertyName+"--property failed!",e);
}
}
private static final class BeanMethodInfo{
private Class clazz;
private Map<String,Method> setterMethods=new HashMap<>();
private Map<String,Method> getterMethods=new HashMap<>();
public BeanMethodInfo(Class clazz) {
this.clazz = clazz;
addGetterMethods();
addSetterMethods();
}
public Method setterMethod(String property){
return setterMethods.get(property);
}
public Method getterMethod(String property){
return getterMethods.get(property);
}
private String methodToProperty(String name) {
if(name.startsWith("is")) {
name = name.substring(2);
} else {
if(!name.startsWith("get") && !name.startsWith("set")) {
throw new RuntimeException("Error parsing property name \'" + name + "\'. Didn\'t start with \'is\', \'get\' or \'set\'.");
}
name = name.substring(3);
}
if(name.length() == 1 || name.length() > 1 && !Character.isUpperCase(name.charAt(1))) {
name = name.substring(0, 1).toLowerCase() + name.substring(1);
}
return name;
}
private void addSetterMethods(){
Method[] methods = clazz.getMethods();
int len = methods.length;
for(int i = 0; i < len; i++) {
Method method = methods[i];
String name = method.getName();
if(name.startsWith("set") && name.length() > 3 && method.getParameterTypes().length == 1) {
String property = methodToProperty(name);
setterMethods.put(property,method);
}
}
}
private void addGetterMethods(){
Method[] methods = clazz.getMethods();
int len = methods.length;
for(int i = 0; i < len; i++) {
Method method = methods[i];
String name = method.getName();
if(name.startsWith("get") && name.length() > 3 || name.startsWith("is") && name.length() > 2) {
String property = methodToProperty(name);
getterMethods.put(property,method);
}
}
}
}
}
这样我们就自定义完成了一个简单的框架 有利于我们理解mybatis框架的原理
public class AppTest {
@Test
public void test(){
SqlSessionFactoryBuilder builder = new SqlSessionFactoryBuilder();
SqlSessionFactory sqlSessionFactory = builder.build();
SqlSession sqlSession = sqlSessionFactory.openSession();
List<User> list = sqlSession.findAll("user.findAll1");
System.out.println(list);
}
}