文章

H2

使用h2做测试很常用,也很简单。不过使用h2写demo也很方便,这种情况下,就需要了解更多h2相关的内容。

  1. 依赖
  2. URL
  3. 存储形式
  4. 模式
    1. Embedded mode
      1. embeded + in memory
      2. embeded + 文件
    2. server mode
      1. tcp server
      2. web server - h2 console
  5. 兼容mysql
  6. 非测试场景的h2使用
    1. 原理
    2. spring security

依赖

1
2
3
4
5
6
7
<!-- https://mvnrepository.com/artifact/com.h2database/h2 -->
<dependency>
    <groupId>com.h2database</groupId>
    <artifactId>h2</artifactId>
    <version>${version}</version>
    <scope>test</scope>
</dependency>

默认是test,说明h2默认是测试时用的,而不是用在正常环境中。如果用非test启动服务的话,会报错找不到org.h2.Driver。

h2的访问使用JDBC,具体的连接池可以用hikari等。这就是统一接口的好处啊!

URL

h2的url大全:http://www.h2database.com/html/features.html#database_url

存储形式

  • 内存:不持久化,关闭JVM即销毁;
  • 文件:持久化,关闭JVM后文件还在;

从url就能指定使用哪种方式:

  • jdbc:h2:mem:<databaseName>:内存存储;
  • jdbc:h2:[file:][<path>]<databaseName>:文件存储;

模式

https://www.h2database.com/html/features.html#connection_modes

  • embedded:h2和应用程序绑定在一起;
  • server:一个独立的h2数据库服务,使用TCP/IP从远端访问数据库;

Embedded mode

h2是否是嵌入模式和是否使用内存存储是两码事。嵌入只是说h2和程序猿自己开发的应用链接在一起,成为一个可执行文件。至于数据库的数据文件存在哪儿,内存还是文件,都行。

参考:https://stackoverflow.com/a/48833387/7676237

所以两两组合,可以组合出四种方式的h2:

  • embedded in-memory
  • embedded persistent
  • client/server in-memory
  • client/server persistent

embeded + in memory

https://www.h2database.com/html/features.html#in_memory_databases

in-memory如果不指定db名,比如jdbc:h2:mem:只能使用一个线程访问该数据库,如果有另一个线程,访问的是新的数据库。类似于Java的匿名类。如果想多个线程访问同一个数据库,需要给数据库url加上数据库名称。比如jdbc:h2:mem:pokemon。有了名字大家就都可以访问它了。

使用in memory很方便,但是不能跨进程连接。毕竟两个进程的jdbc:h2:mem:db1指的不是同一款内存:

Sometimes multiple connections to the same in-memory database are required. In this case, the database URL must include a name. Example: jdbc:h2:mem:db1. Accessing the same database using this URL only works within the same virtual machine and class loader environment.

如果想远程访问另一个进程的in memory h2,就需要使用tcp连接,提供那个进程的ip和port:

To access an in-memory database from another process or from another computer, you need to start a TCP server in the same process as the in-memory database was created. The other processes then need to access the database over TCP/IP or TLS, using a database URL such as: jdbc:h2:tcp://localhost/mem:db1.

默认当没有连接时,数据库就关了。 如果是in memory数据库,内容就丢失了。如果相当等jvm关闭时才关闭数据库,拼接;DB_CLOSE_DELAY=-1,比如jdbc:h2:mem:pokemon;DB_CLOSE_DELAY=-1

embeded + 文件

两个以文件作为存储的h2服务也不能同时访问同一份文件,会报错:Database may be already in use。

所以一个h2服务只要是嵌入模式,其实无论使用哪种存储方式,都不能让另一个进程远程访问它的数据。必须使用server模式,再开启一个tcp服务,通过tcp给别人传数据。

server mode

tcp server

server mode就是一个进程启动h2(一般是以embeded的形式启动),并提供tcp访问,别的进程无论是本机的还是外网的,都可以通过tcp获取数据。

如何开启一个server模式的h2:http://www.h2database.com/html/tutorial.html#using_server

一个在spring boot中开启h2 tcp server的示例:https://stackoverflow.com/a/55847035/7676237

web server - h2 console

h2不止有tcp server,供远程访问,还有一个内置的web server,可以使用http访问。也就是说可以拿浏览器直接访问,所以它又被称为h2 console。

它其实就是一个servlet:

1
2
3
4
5
6
7
8
package org.h2.server.web;

/**
 * This servlet lets the H2 Console be used in a standard servlet container
 * such as Tomcat or Jetty.
 */
public class WebServlet extends HttpServlet {
}

这个servlet被放在tomcat容器里,处理http请求。

兼容mysql

https://www.h2database.com/html/features.html#compatibility

mode设为mysql。同时,因为mysql忽略大小写,所以可以把case insensitive也加上:

1
jdbc:h2:~/test;MODE=MySQL;DATABASE_TO_LOWER=TRUE;CASE_INSENSITIVE_IDENTIFIERS=TRUE

另外mysql的文本比较也忽略大小写(???),所以h2需要使用SET IGNORECASE TRUE让自己也忽略大小写,从而让=, LIKE, REGEXP达到和mysql一样的效果。

非测试场景的h2使用

h2一般被拿来做test用,但是有时候写一些简单的示例小程序,也可以把他作为数据库来用。此时就不再把scope声明为test,可以使用默认的compile,或者runtime也行,毕竟编码的时候用不到它,运行的时候有就行了:

1
2
3
4
5
        <dependency>
            <groupId>com.h2database</groupId>
            <artifactId>h2</artifactId>
            <scope>runtime</scope>
        </dependency>

springboot使用h2非常简单,用户名和密码可设可不设:

1
2
3
4
5
6
7
8
# embedded in-memory; close only when jvm exit; use MySQL mode
spring.datasource.url=jdbc:h2:mem:pokemon;DB_CLOSE_DELAY=-1;MODE=MySQL
# embedded local file
spring.datasource.driverClassName=org.h2.Driver
#spring.datasource.username=sa
#spring.datasource.password=password
# sql schema
spring.datasource.schema=classpath:sql/USER-schema.sql,classpath:sql/USER-data.sql

但不方便的地方在于,这是一个embeded模式启动的数据库,我们在开发的时候,没法让别的进程连上去查看其中的内容。

牛逼的是,spring boot的auto configuration功能搞了个H2ConsoleAutoConfiguration,可以给springboot的h2开启web server(即h2 console),让我们可以方便地使用浏览器查看h2数据库的内容:

1
2
3
4
5
6
7
8
9
10
h2:
  # h2 web consloe 是一个数据库GUI管理应用,程序运行时,会自动启动h2 web consloe
  console:
    # 开启 h2 web console ,默认开启
    enabled: true
    # 可以通过 url/h2-console 访问 h2 web
    path: /h2-console
    settings:
      # 配置后 h2 web console 就可以在远程访问,否则只能在本机访问
      web-allow-others: true

配置非常简单,enable=true,然后就可以通过http://host:port/h2-console访问springboot启动的内嵌数据库了。

原理

看一下这个autoconfig的内容就明白了:

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
/**
 * {@link EnableAutoConfiguration Auto-configuration} for H2's web console.
 *
 * @author Andy Wilkinson
 * @author Marten Deinum
 * @author Stephane Nicoll
 * @since 1.3.0
 */
@Configuration(proxyBeanMethods = false)
@ConditionalOnWebApplication(type = Type.SERVLET)
@ConditionalOnClass(WebServlet.class)
@ConditionalOnProperty(prefix = "spring.h2.console", name = "enabled", havingValue = "true", matchIfMissing = false)
@AutoConfigureAfter(DataSourceAutoConfiguration.class)
@EnableConfigurationProperties(H2ConsoleProperties.class)
public class H2ConsoleAutoConfiguration {

	private static final Log logger = LogFactory.getLog(H2ConsoleAutoConfiguration.class);

	@Bean
	public ServletRegistrationBean<WebServlet> h2Console(H2ConsoleProperties properties,
			ObjectProvider<DataSource> dataSource) {
		String path = properties.getPath();
		String urlMapping = path + (path.endsWith("/") ? "*" : "/*");
		ServletRegistrationBean<WebServlet> registration = new ServletRegistrationBean<>(new WebServlet(), urlMapping);
		H2ConsoleProperties.Settings settings = properties.getSettings();
		if (settings.isTrace()) {
			registration.addInitParameter("trace", "");
		}
		if (settings.isWebAllowOthers()) {
			registration.addInitParameter("webAllowOthers", "");
		}
		dataSource.ifAvailable((available) -> {
			try (Connection connection = available.getConnection()) {
				logger.info("H2 console available at '" + path + "'. Database available at '"
						+ connection.getMetaData().getURL() + "'");
			}
			catch (SQLException ex) {
				// Continue
			}
		});
		return registration;
	}

}

首先,条件注解声明,spring.h2.console.enabled为true才生效。

然后构造了一个ServletRegistrationBean

1
ServletRegistrationBean<WebServlet> registration = new ServletRegistrationBean<>(new WebServlet(), urlMapping);

把设置的path(默认就是/h2-console)和h2 console对应的servlet绑定。

ServletRegistrationBean,看它的名字就知道,是用来注册servlet的bean。既然注册了这个servlet,那springboot就有这个servlet的功能了。

最后http请求访问这个path,就会由这个servlet来服务了。

spring security

h2有自己的登录界面。如果springboot集成了spring security,需要先登录验证spring security,再登录h2 console。两套登录,没必要。另外登陆完h2之后,所有静态资源无法加载。(暂时不清楚为啥)

所以记得把spring security对h2-console这个path的校验关掉:https://stackoverflow.com/a/59560136/7676237

登录h2 console的信息:

  • url: jdbc:h2:mem:pokemon依然是mem格式的连接。因为实际上是让和h2同一个JVM里的servlet线程去连接h2,他们的mem指的是同一个JVM的mem
  • username/password: 就是springboot配置里的

参阅:

  • https://www.baeldung.com/spring-boot-h2-database
  • https://stackoverflow.com/a/28950817/7676237
  • https://stackoverflow.com/a/43862903/7676237
本文由作者按照 CC BY 4.0 进行授权