无spring环境Mybatis整合

  

背景

mirai-console

  最近在学习写mirai console插件,mirai本身是kotlin开发,对于我真是既友好又不友好…不友好的是kotlin真的太抽象了,阅读起来有一定障碍,友好的是有问题至少能用java填坑..

数据存储

  mirai console提供的数据存储PluginData 本身算是为kotlin设计的,java使用有些不便(而且虽然已经在计划中,但目前还不支持数据库存储)。数据的存储 以及一些查询等等…我觉得还是数据库更胜一筹的,于是乎还得整合数据库(当然为了插件的便捷性,数据库依然是sqlite)

mybatis

  为什么是mybatis不是jpa,不是hibernate?sql归sql,代码归代码,这是我喜欢mybatis的原因。另一个主要原因是:用惯了…

无spring

  其实在准备写此插件之前,已经写过一个mirai+spring的项目了WMagicBotR,那个项目就是 spring+sqlite+mybtis,不得不说spring确实是为开发人员省下很多心,特别是springboot 一键搞定。
  然而这些便利是有代价的:

  • 打包后jar体积庞大(可以通过阉割spring解决)
  • 不熟悉的kotlin+不熟悉的构建工具gradle,使得一开始使用springboot的过程中,打包后的jar要不打包失败,要不打包后无法被识别为插件很是心累…

  springboot默认需要一个main方法启动类,而作为mirai console插件,他本身是不该主动启动的,应该在插件被加载的时候才启动。这里就有一个问题需要想明白,这个项目是spring作主还是mirai做主?上一个项目其实是spring做主的,使用mirai获得的bot对象作为一个bean被spring管理着,哪里需要bot在哪里注入就行。但是作为console插件,整个环境下不应该有bot对象,而是通过mcl等其他mirai前端来调用插件的方法,调用者(bot)是不固定的。
  失去了 在任意地方获取bot对象 整个需求后,作为一个功能性插件,对于spring的需求也便弱化了…

需求与坑

  现在总结一下需要做的事,非spring+mybatis+sqlite,这里面还有一些隐藏的小问题(坑),比如说:

  • mybatis+sqlite,sqlite文件存放的位置不是固定的url,这就不该使用常规的mybatis-config.xml配置文件了
  • mapper的注册方式有4种,package,url,class,resource用哪一种?为了可以让mapper.xml一家人整整齐齐的待在resourses文件夹下,得用resource模式
  • 可惜 不用config.xml配置文件的方式…貌似不能用resource注册mapper
  • 开发环境的resource,和打成jar包的resource,他一样吗?
      几个问题一环扣一环..接下来就开始一点点解决吧

正文

   相关详细代码会在本文最后贴出,源码见WMagicBotRP

sqlite数据初始化

  数据初始化的流程比较好理解,检查数据文件是否存在,如果不存在则创建文件。这边就不用mybatis了,为什么呢?如果没有初始化的话,db文件都不存在,相当于mybatis没有数据库连接的url

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// 初始化数据文件
private void createDB() {
//SQLite没有datetime值的显式数据类型,这里自己转换一下格式写入
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
try {
Class.forName("org.sqlite.JDBC");
} catch (ClassNotFoundException e1) {
logger.error(e1.getMessage());
}
String url = "jdbc:sqlite:" + Path.getPath() + dbFileName;
try (Connection connection = DriverManager.getConnection(url);
Statement statement = connection.createStatement();) {

statement.executeUpdate("DROP TABLE IF EXISTS DBInfo;");

String createDBsql = "create table DBInfo("
+ "id INTEGER PRIMARY KEY autoincrement,"
+ "name varchar(255) ,"
+ "version varchar(20),"
+ "createDate varchar(20),"
+ "updateDate varchar(20));";
statement.executeUpdate(createDBsql);

String initsql = "INSERT INTO DBInfo (name,version,createDate,updateDate) VALUES ("
+ "'MagicBot',"
+ "'0.0.1',"
+ "'" + sdf.format(new Date()) + "',"
+ "'')";
statement.executeUpdate(initsql);
} catch (Exception e) {
logger.error(e.getMessage());
}
}

mybatis的配置

先看看config.xml

  上面提过,因为需要动态的获取db文件的地址,这里的配置就不再使用mybatis-config.xml文件了,但是原理是差不多的。所以先来看一下使用配置文件的方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// mybatis官方提供的配置
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>

<environments default="development">
<environment id="development">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<property name="driver" value="${driver}"/>
<property name="url" value="${url}"/>
<property name="username" value="${username}"/>
<property name="password" value="${password}"/>
</dataSource>
</environment>
</environments>

//4种注册mapper的方式就不展开了,有需要可以自己去找一下相关资料
<mappers>
<mapper resource="mapper/DemoMapper.xml"/>
<mapper url="file:///path/to/your/file/DemoMapper.xml"/>
<package name="com.examle.mappers"/>
<mapper class="com.examle.mappers.DemoMapper"/>
</mappers>
</configuration>

// 这种方式是读取配置文件的构造方式
InputStream inputStream = Resources.getResourceAsStream("mybatis-config.xml");
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);

创建Configuration

  可以看到最外层标签configuration下包含environments以及mappers注册,其中environments中指定了数据源dataSource和事务管理transactionManager。照猫画虎,先手动构建configuration

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 先构建Configuration对象
Configuration config = new Configuration();

// 接着构建environment对象,不过再此之前先创建 dataSource
SQLiteConnectionPoolDataSource pool = new SQLiteConnectionPoolDataSource();
// db文件地址的动态就体现在这里了
pool.setUrl("jdbc:sqlite:" + Path.getPath() + "data.db");
// 有了dataSource就可以创建environment了
Environment environment = new Environment.Builder("development")
.dataSource(pool)
.transactionFactory(new JdbcTransactionFactory())
.build();

//将environment设置给configuration
config.setEnvironment(environment);

// mapper的注册
...

SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(config);

  第十行,设置transactionFactory的时候,有2种选择JdbcTransactionFactory 以及ManagedTransactionFactory,这就对应了xml配置文件中的 type=”JDBC” 和 type=”MANAGED”,前者的事物由connection对象的具体方法控制,后者则是把事物交给了容器(tomcat,weblogic,grassfish等),作为插件,我们选择JDBC

mapper的注册

  在需求与坑那部分,提到需要使用resource方式来注册mapper,但是…麻烦来了,在Configuration中提供的mapper注册方式里,没有resource模式

1
2
3
4
5
6
7
8
9
10
// Configuration 提供的方式,可以看到是class模式和package模式
public void addMappers(String packageName, Class<?> superType) {
mapperRegistry.addMappers(packageName, superType);
}
public void addMappers(String packageName) {
mapperRegistry.addMappers(packageName);
}
public <T> void addMapper(Class<T> type) {
mapperRegistry.addMapper(type);
}

  这就产生了一点小小的困难,不过没有提供不代表没办法注册,他自己是怎么注册的呢?去Configuration源码中看看

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// 使用配置文件build调用的方法
public SqlSessionFactory build(InputStream inputStream) {
return build(inputStream, null, null);
}
// 使用配置文件build调用的方法
public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {
try {
XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
return build(parser.parse());
} catch (Exception e) {
throw ExceptionFactory.wrapException("Error building SqlSession.", e);
} finally {
ErrorContext.instance().reset();
try {
if (inputStream != null) {
inputStream.close();
}
} catch (IOException e) {
// Intentionally ignore. Prefer previous error.
}
}
}
// 自己手动创建Configuration调用的方法
public SqlSessionFactory build(Configuration config) {
return new DefaultSqlSessionFactory(config);
}

  可以看到第8行使用了XMLConfigBuilder去解析获取的xml文件,在跟着进去找找,在文件靠下方可以找到以下代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
private void mapperElement(XNode parent) throws Exception {
if (parent != null) {
for (XNode child : parent.getChildren()) {
if ("package".equals(child.getName())) {
String mapperPackage = child.getStringAttribute("name");
configuration.addMappers(mapperPackage);
} else {
String resource = child.getStringAttribute("resource");
String url = child.getStringAttribute("url");
String mapperClass = child.getStringAttribute("class");
if (resource != null && url == null && mapperClass == null) {
ErrorContext.instance().resource(resource);
try(InputStream inputStream = Resources.getResourceAsStream(resource)) {
XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
mapperParser.parse();
}
} else if (resource == null && url != null && mapperClass == null) {
ErrorContext.instance().resource(url);
try(InputStream inputStream = Resources.getUrlAsStream(url)){
XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, url, configuration.getSqlFragments());
mapperParser.parse();
}
} else if (resource == null && url == null && mapperClass != null) {
Class<?> mapperInterface = Resources.classForName(mapperClass);
configuration.addMapper(mapperInterface);
} else {
throw new BuilderException("A mapper element may only specify a url, resource or class, but not more than one.");
}
}
}
}
}

  这熟悉的package、resource、url、class让人振奋,这段代码中12-16行正是我们所需要的,到此解决了Configuration没有提供resource注册方式的麻烦(有兴趣可以深入再看看)

1
2
3
4
5
6
7
8
9
10
11
12
// 我们把找到的代码封装为一个方法 注册xml mapper
private static void magicMapperLoader(Configuration configuration,List<String> xmlFiles) {
for (String xmlPath : xmlFiles) {
ErrorContext.instance().resource(xmlPath);
try (InputStream inputStream = Resources.getResourceAsStream(xmlPath)) {
XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, xmlPath, configuration.getSqlFragments());
mapperParser.parse();
} catch (Exception e) {
e.printStackTrace();
}
}
}

mapper的扫描

  单个mapper已经可以注册,接下来将我们需要的mapper作为参数导入即可完成注册。这没有什么难度,mapper统一存放在项目的resources资源mapper目录下,获取mapper所有的文件即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public static List<String> getFileXmlRes() {
String path = "mapper";
List<String> filenames = new ArrayList<>();
try (
InputStream in = Thread.currentThread().getContextClassLoader().getResourceAsStream(path);
BufferedReader br = new BufferedReader(new InputStreamReader(in));
) {
String resource;
while ((resource = br.readLine()) != null) {
if (resource.endsWith("xml")) {
filenames.add(path + "/" + resource);
}
}
} catch (IOException e) {
e.printStackTrace();
}
return filenames;
}

  到这里一切都很顺利,运行测试已经通过,然而打包后就出现了一些小小的问题…上面遍历的方法是基于文件系统的。而打包后所有的文件都在jar包内部,资源文件不再属于文件系统,只能指定资源名称将资源作为流读取出来,无法做到遍历获取。
  这时候就得针对jar包来做一个专门的读取资源文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public static List<String> getJarXmlRes() {
List<String> list = new ArrayList<>();
String jar = MyBatisUtil.class.getProtectionDomain().getCodeSource().getLocation().getFile();
try (JarFile jf = new JarFile(jar);) {
Enumeration<JarEntry> es = jf.entries();
while (es.hasMoreElements()) {
String resName = es.nextElement().getName();
if (resName.startsWith("mapper") && resName.endsWith("xml")) {
list.add(resName);
}
}
} catch (IOException e) {
e.printStackTrace();
}
return list;
}

  实际情况下根据运行环境的不同,应该使用对应的读取方式。针对这个做一下优化,就完成了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
private static void magicMapperLoader(Configuration configuration) {
List<String> xmlFiles;
// 根据不同运行环境来扫描xml资源
String protocol = MyBatisUtil.class.getResource("").getProtocol();
if (Objects.equals(protocol, "jar")) {
xmlFiles = getJarXmlRes();
} else {
xmlFiles = getFileXmlRes();
}
for (String xmlPath : xmlFiles) {
ErrorContext.instance().resource(xmlPath);
try (InputStream inputStream = Resources.getResourceAsStream(xmlPath)) {
XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, xmlPath, configuration.getSqlFragments());
mapperParser.parse();
} catch (Exception e) {
e.printStackTrace();
}
}
}

SqlSession的获取

1
2
3
4
5
6
// datasource,enviroment等配置
...
// mapper的注册
magicMapperLoader(config);
SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(config);
SqlSession session = factory.openSession(true);

   获取session的时候要注意一下:autocommit参数默认是false的,不想每次执行完sql自己commit的话,还是设置为自动commit比较方便一些,当然这属于仁者见仁智者见智。

使用mybatis

  具体的mapper接口和xml文件就不具体说明了,有没有spring都一样,该怎么写怎么写。就简单说一下没有@Autowised 如何使用定义的接口

1
2
3
4
5
6
7
8
9
// mapper接口,对应的xml就不写了大家都懂
@Mapper
public interface DemoMapper {
String demoSql();
}

SqlSession session = ...//session建议使用全局的单例
DemoMapper mapper = session.getMapper(DemoMapper.class);
String result = mapper.demoSql();

结语

  框架真是带来了很多便利,那么,代价是什么呢?

附代码

  更多请见WMagicBotRP

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
import org.apache.ibatis.builder.xml.XMLMapperBuilder;
import org.apache.ibatis.executor.ErrorContext;
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.mapping.Environment;
import org.apache.ibatis.session.Configuration;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;
import org.apache.ibatis.transaction.jdbc.JdbcTransactionFactory;
import org.sqlite.javax.SQLiteConnectionPoolDataSource;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.List;
import java.util.Objects;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;

public class MyBatisUtil {

private static SqlSessionFactory factory = null;

public static SqlSession getSqlSession() {
return factory.openSession(true);
}

static {
try {
// datasource
SQLiteConnectionPoolDataSource pool = new SQLiteConnectionPoolDataSource();
pool.setUrl("jdbc:sqlite:" + Path.getPath() + DBInitHelper.dbFileName);
// environment
Environment environment = new Environment.Builder("development")
.dataSource(pool)
.transactionFactory(new JdbcTransactionFactory())
.build();
// config
Configuration config = new Configuration();
config.setEnvironment(environment);
// 注册mapper
magicMapperLoader(config);
// build factory
factory = new SqlSessionFactoryBuilder().build(config);
} catch (Exception e) {
e.printStackTrace();
}
}


// 注册xml mapper
private static void magicMapperLoader(Configuration configuration) {
List<String> xmlFiles;

// 根据不同运行环境来扫描xml资源
String protocol = MyBatisUtil.class.getResource("").getProtocol();
if (Objects.equals(protocol, "jar")) {
xmlFiles = getJarXmlRes();
} else {
xmlFiles = getFileXmlRes();
}

for (String xmlPath : xmlFiles) {
ErrorContext.instance().resource(xmlPath);
try (InputStream inputStream = Resources.getResourceAsStream(xmlPath)) {
XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, xmlPath, configuration.getSqlFragments());
mapperParser.parse();
} catch (Exception e) {
e.printStackTrace();
}
}
}

// 加载所有 resources 中 mapper下的文件
// jar包运行时无法使用file来遍历 使用这个
public static List<String> getJarXmlRes() {
List<String> list = new ArrayList<>();
String jar = MyBatisUtil.class.getProtectionDomain().getCodeSource().getLocation().getFile();
try (JarFile jf = new JarFile(jar);) {
Enumeration<JarEntry> es = jf.entries();
while (es.hasMoreElements()) {
String resName = es.nextElement().getName();
if (resName.startsWith("mapper") && resName.endsWith("xml")) {
list.add(resName);
}
}
} catch (IOException e) {
e.printStackTrace();
}
return list;
}


// idea开发时使用 ,获取resources/mapper下所有xml文件
public static List<String> getFileXmlRes() {
String path = "mapper";
List<String> filenames = new ArrayList<>();
try (
InputStream in = getResourceAsStream(path);
BufferedReader br = new BufferedReader(new InputStreamReader(in));
) {
String resource;
while ((resource = br.readLine()) != null) {
if (resource.endsWith("xml")) {
filenames.add(path + "/" + resource);
}
}
} catch (IOException e) {
e.printStackTrace();
}
return filenames;
}

private static InputStream getResourceAsStream(String resource) {
final InputStream in = Thread.currentThread().getContextClassLoader().getResourceAsStream(resource);
return in == null ? MyBatisUtil.class.getResourceAsStream(resource) : in;
}

}