Mybatis 使用 GraalVM 构建本地映像
适用人群
- 熟悉使用 springboot + mybatis 构建应用
- 了解和使用过 graalvm 构建本地镜像
构建环境
工具 | 版本 |
---|---|
JDK | Oracle GraalVM 17.0.8 |
Maven | Apache Maven 3.9.2 |
MySQL | MySQL 8.0 |
Visual Studio | 2022 |
Windows Kits | 10 |
IntelliJ IDEA | IntelliJ IDEA 2023.2.1 (Ultimate Edition) |
项目构建
pom.xml 文件
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.1.3</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>3.0.2</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.graalvm.buildtools</groupId>
<artifactId>native-maven-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
application.yml 文件
server:
port: 9000
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://127.0.0.1:3306/user
username: root
password: 123456
logging:
level:
com.xxx: debug
com.xxx.mapper: trace
mybatis:
mapper-locations: classpath:mapper/*.xml
启动类配置
注意事项: 如果使用 @MapperScan , 需要配置 SqlSessionTemplate , 这里使用默认的 sqlSessionTemplate
@SpringBootApplication
@MapperScan(basePackages = "com.xxx.mapper", sqlSessionTemplateRef = "sqlSessionTemplate")
public class GraalvmApplication {
public static void main(String[] args) {
SpringApplication.run(GraalvmApplication.class, args);
}
}
DAO 层配置
sql 文件
create table users
(
username varchar(50) not null primary key,
password varchar(500) not null,
enabled tinyint(1) not null
);
mapper 接口
public interface UsersMapper {
Users selectByUsername(@Param("username") String username);
}
mapper xml
<resultMap type="com.xxx.entity.Users" id="UsersMap">
<result property="username" column="username" jdbcType="VARCHAR"/>
<result property="password" column="password" jdbcType="VARCHAR"/>
<result property="enabled" column="enabled" jdbcType="INTEGER"/>
</resultMap>
<select id="selectByUsername" resultMap="UsersMap">
select username, password, enabled
from users
where username = #{username}
</select>
Service 层配置
service 接口
public interface UsersService {
Users findByUsername(String username);
}
service 实现
@Service
@RequiredArgsConstructor
public class UsersServiceImpl implements UsersService {
private final UsersMapper usersMapper;
@Override
public Users findByUsername(String username) {
return this.usersMapper.selectByUsername(username);
}
}
Controller 层配置
@Slf4j
@RestController
@RequiredArgsConstructor
public class UsersController {
private final UsersService usersService;
@GetMapping("/string/{text}")
public String text(@PathVariable("text") String text) {
log.info("text: {}", text);
return "text: " + text;
}
@GetMapping("/users/{username}")
public Users users(@PathVariable("username") String username) {
return usersService.findByUsername(username);
}
}
实体类
@Data
public class Users implements Serializable {
private String username;
private String password;
private Integer enabled;
}
以上已完成 springboot + mybatis 的基础构建
项目直接运行情况如下:
. ____ _ __ _ _
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: Spring Boot :: (v3.1.3)
2023-08-30T17:36:32.710+08:00 INFO 4808 --- [ restartedMain] com.hyh.GraalvmApplication : Starting GraalvmApplication using Java 17.0.8 with PID 4808
2023-08-30T17:36:32.712+08:00 DEBUG 4808 --- [ restartedMain] com.hyh.GraalvmApplication : Running with Spring Boot v3.1.3, Spring v6.0.11
2023-08-30T17:36:32.712+08:00 INFO 4808 --- [ restartedMain] com.hyh.GraalvmApplication : No active profile set, falling back to 1 default profile: "default"
2023-08-30T17:36:32.756+08:00 INFO 4808 --- [ restartedMain] .e.DevToolsPropertyDefaultsPostProcessor : Devtools property defaults active! Set 'spring.devtools.add-properties' to 'false' to disable
2023-08-30T17:36:32.756+08:00 INFO 4808 --- [ restartedMain] .e.DevToolsPropertyDefaultsPostProcessor : For additional web related logging consider setting the 'logging.level.web' property to 'DEBUG'
2023-08-30T17:36:33.466+08:00 INFO 4808 --- [ restartedMain] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat initialized with port(s): 9000 (http)
2023-08-30T17:36:33.472+08:00 INFO 4808 --- [ restartedMain] o.apache.catalina.core.StandardService : Starting service [Tomcat]
2023-08-30T17:36:33.472+08:00 INFO 4808 --- [ restartedMain] o.apache.catalina.core.StandardEngine : Starting Servlet engine: [Apache Tomcat/10.1.12]
2023-08-30T17:36:33.509+08:00 INFO 4808 --- [ restartedMain] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring embedded WebApplicationContext
2023-08-30T17:36:33.509+08:00 INFO 4808 --- [ restartedMain] w.s.c.ServletWebServerApplicationContext : Root WebApplicationContext: initialization completed in 753 ms
2023-08-30T17:36:33.895+08:00 INFO 4808 --- [ restartedMain] o.s.b.d.a.OptionalLiveReloadServer : LiveReload server is running on port 35729
2023-08-30T17:36:33.920+08:00 INFO 4808 --- [ restartedMain] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port(s): 9000 (http) with context path ''
2023-08-30T17:36:33.928+08:00 INFO 4808 --- [ restartedMain] com.hyh.GraalvmApplication : Started GraalvmApplication in 1.548 seconds (process running for 2.032)
项目构建本地镜像
使用跟踪代理
- 获取项目的 jar 包
mvn clean package
- 使用跟踪代理运行项目
注意事项:- 不能调换 -agentlib 和 -jar 的位置
- 尽量测试完所有的接口
- 结束运行项目才会生成跟踪代理文件
java -agentlib:native-image-agent=config-output-dir=<跟踪代理文件输出路径> -jar <jar 包路径>
- 放置跟踪代理文件
将跟踪代理文件输出路径所有的 json 文件移动至项目 resources\META-INF\native-image 文件夹下,需要自行创建
创建配置类
import org.apache.commons.logging.LogFactory;
import org.apache.ibatis.annotations.DeleteProvider;
import org.apache.ibatis.annotations.InsertProvider;
import org.apache.ibatis.annotations.SelectProvider;
import org.apache.ibatis.annotations.UpdateProvider;
import org.apache.ibatis.cache.decorators.FifoCache;
import org.apache.ibatis.cache.decorators.LruCache;
import org.apache.ibatis.cache.decorators.SoftCache;
import org.apache.ibatis.cache.decorators.WeakCache;
import org.apache.ibatis.cache.impl.PerpetualCache;
import org.apache.ibatis.javassist.util.proxy.ProxyFactory;
import org.apache.ibatis.javassist.util.proxy.RuntimeSupport;
import org.apache.ibatis.logging.Log;
import org.apache.ibatis.logging.commons.JakartaCommonsLoggingImpl;
import org.apache.ibatis.logging.jdk14.Jdk14LoggingImpl;
import org.apache.ibatis.logging.log4j2.Log4j2Impl;
import org.apache.ibatis.logging.nologging.NoLoggingImpl;
import org.apache.ibatis.logging.slf4j.Slf4jImpl;
import org.apache.ibatis.logging.stdout.StdOutImpl;
import org.apache.ibatis.reflection.TypeParameterResolver;
import org.apache.ibatis.scripting.defaults.RawLanguageDriver;
import org.apache.ibatis.scripting.xmltags.XMLLanguageDriver;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.mapper.MapperFactoryBean;
import org.mybatis.spring.mapper.MapperScannerConfigurer;
import org.springframework.aot.hint.MemberCategory;
import org.springframework.aot.hint.RuntimeHints;
import org.springframework.aot.hint.RuntimeHintsRegistrar;
import org.springframework.beans.PropertyValue;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.BeanFactoryAware;
import org.springframework.beans.factory.aot.BeanFactoryInitializationAotContribution;
import org.springframework.beans.factory.aot.BeanFactoryInitializationAotProcessor;
import org.springframework.beans.factory.aot.BeanRegistrationExcludeFilter;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.config.ConfigurableBeanFactory;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.beans.factory.support.MergedBeanDefinitionPostProcessor;
import org.springframework.beans.factory.support.RegisteredBean;
import org.springframework.beans.factory.support.RootBeanDefinition;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.ImportRuntimeHints;
import org.springframework.core.ResolvableType;
import org.springframework.util.ClassUtils;
import org.springframework.util.ReflectionUtils;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.Stream;
@Configuration(proxyBeanMethods = false)
@ImportRuntimeHints(MyBatisNativeConfiguration.MyBaitsRuntimeHintsRegistrar.class)
public class MyBatisNativeConfiguration {
@Bean
MyBatisBeanFactoryInitializationAotProcessor myBatisBeanFactoryInitializationAotProcessor() {
return new MyBatisBeanFactoryInitializationAotProcessor();
}
@Bean
static MyBatisMapperFactoryBeanPostProcessor myBatisMapperFactoryBeanPostProcessor() {
return new MyBatisMapperFactoryBeanPostProcessor();
}
static class MyBaitsRuntimeHintsRegistrar implements RuntimeHintsRegistrar {
@Override
public void registerHints(RuntimeHints hints, ClassLoader classLoader) {
Stream.of(RawLanguageDriver.class,
XMLLanguageDriver.class,
RuntimeSupport.class,
ProxyFactory.class,
Slf4jImpl.class,
Log.class,
JakartaCommonsLoggingImpl.class,
Log4j2Impl.class,
Jdk14LoggingImpl.class,
StdOutImpl.class,
NoLoggingImpl.class,
SqlSessionFactory.class,
PerpetualCache.class,
FifoCache.class,
LruCache.class,
SoftCache.class,
WeakCache.class,
SqlSessionFactoryBean.class,
ArrayList.class,
HashMap.class,
TreeSet.class,
HashSet.class
).forEach(x -> hints.reflection().registerType(x, MemberCategory.values()));
Stream.of(
"org/apache/ibatis/builder/xml/*.dtd",
"org/apache/ibatis/builder/xml/*.xsd"
).forEach(hints.resources()::registerPattern);
}
}
static class MyBatisBeanFactoryInitializationAotProcessor
implements BeanFactoryInitializationAotProcessor, BeanRegistrationExcludeFilter {
private final Set<Class<?>> excludeClasses = new HashSet<>();
MyBatisBeanFactoryInitializationAotProcessor() {
excludeClasses.add(MapperScannerConfigurer.class);
}
@Override public boolean isExcludedFromAotProcessing(RegisteredBean registeredBean) {
return excludeClasses.contains(registeredBean.getBeanClass());
}
@Override
public BeanFactoryInitializationAotContribution processAheadOfTime(ConfigurableListableBeanFactory beanFactory) {
String[] beanNames = beanFactory.getBeanNamesForType(MapperFactoryBean.class);
if (beanNames.length == 0) {
return null;
}
return (context, code) -> {
RuntimeHints hints = context.getRuntimeHints();
for (String beanName : beanNames) {
BeanDefinition beanDefinition = beanFactory.getBeanDefinition(beanName.substring(1));
PropertyValue mapperInterface = beanDefinition.getPropertyValues().getPropertyValue("mapperInterface");
if (mapperInterface != null && mapperInterface.getValue() != null) {
Class<?> mapperInterfaceType = (Class<?>) mapperInterface.getValue();
if (mapperInterfaceType != null) {
registerReflectionTypeIfNecessary(mapperInterfaceType, hints);
hints.proxies().registerJdkProxy(mapperInterfaceType);
hints.resources()
.registerPattern(mapperInterfaceType.getName().replace('.', '/').concat(".xml"));
registerMapperRelationships(mapperInterfaceType, hints);
}
}
}
};
}
private void registerMapperRelationships(Class<?> mapperInterfaceType, RuntimeHints hints) {
Method[] methods = ReflectionUtils.getAllDeclaredMethods(mapperInterfaceType);
for (Method method : methods) {
if (method.getDeclaringClass() != Object.class) {
ReflectionUtils.makeAccessible(method);
registerSqlProviderTypes(method, hints, SelectProvider.class, SelectProvider::value, SelectProvider::type);
registerSqlProviderTypes(method, hints, InsertProvider.class, InsertProvider::value, InsertProvider::type);
registerSqlProviderTypes(method, hints, UpdateProvider.class, UpdateProvider::value, UpdateProvider::type);
registerSqlProviderTypes(method, hints, DeleteProvider.class, DeleteProvider::value, DeleteProvider::type);
Class<?> returnType = MyBatisMapperTypeUtils.resolveReturnClass(mapperInterfaceType, method);
registerReflectionTypeIfNecessary(returnType, hints);
MyBatisMapperTypeUtils.resolveParameterClasses(mapperInterfaceType, method)
.forEach(x -> registerReflectionTypeIfNecessary(x, hints));
}
}
}
@SafeVarargs
private <T extends Annotation> void registerSqlProviderTypes(
Method method, RuntimeHints hints, Class<T> annotationType, Function<T, Class<?>>... providerTypeResolvers) {
for (T annotation : method.getAnnotationsByType(annotationType)) {
for (Function<T, Class<?>> providerTypeResolver : providerTypeResolvers) {
registerReflectionTypeIfNecessary(providerTypeResolver.apply(annotation), hints);
}
}
}
private void registerReflectionTypeIfNecessary(Class<?> type, RuntimeHints hints) {
if (!type.isPrimitive() && !type.getName().startsWith("java")) {
hints.reflection().registerType(type, MemberCategory.values());
}
}
}
static class MyBatisMapperTypeUtils {
private MyBatisMapperTypeUtils() {
// NOP
}
static Class<?> resolveReturnClass(Class<?> mapperInterface, Method method) {
Type resolvedReturnType = TypeParameterResolver.resolveReturnType(method, mapperInterface);
return typeToClass(resolvedReturnType, method.getReturnType());
}
static Set<Class<?>> resolveParameterClasses(Class<?> mapperInterface, Method method) {
return Stream.of(TypeParameterResolver.resolveParamTypes(method, mapperInterface))
.map(x -> typeToClass(x, x instanceof Class ? (Class<?>) x : Object.class)).collect(Collectors.toSet());
}
private static Class<?> typeToClass(Type src, Class<?> fallback) {
Class<?> result = null;
if (src instanceof Class<?>) {
if (((Class<?>) src).isArray()) {
result = ((Class<?>) src).getComponentType();
} else {
result = (Class<?>) src;
}
} else if (src instanceof ParameterizedType) {
ParameterizedType parameterizedType = (ParameterizedType) src;
int index = (parameterizedType.getRawType() instanceof Class
&& Map.class.isAssignableFrom((Class<?>) parameterizedType.getRawType())
&& parameterizedType.getActualTypeArguments().length > 1) ? 1 : 0;
Type actualType = parameterizedType.getActualTypeArguments()[index];
result = typeToClass(actualType, fallback);
}
if (result == null) {
result = fallback;
}
return result;
}
}
static class MyBatisMapperFactoryBeanPostProcessor implements MergedBeanDefinitionPostProcessor, BeanFactoryAware {
private static final org.apache.commons.logging.Log LOG = LogFactory.getLog(
MyBatisMapperFactoryBeanPostProcessor.class);
private static final String MAPPER_FACTORY_BEAN = "org.mybatis.spring.mapper.MapperFactoryBean";
private ConfigurableBeanFactory beanFactory;
@Override
public void setBeanFactory(BeanFactory beanFactory) {
this.beanFactory = (ConfigurableBeanFactory) beanFactory;
}
@Override
public void postProcessMergedBeanDefinition(RootBeanDefinition beanDefinition, Class<?> beanType, String beanName) {
if (ClassUtils.isPresent(MAPPER_FACTORY_BEAN, this.beanFactory.getBeanClassLoader())) {
resolveMapperFactoryBeanTypeIfNecessary(beanDefinition);
}
}
private void resolveMapperFactoryBeanTypeIfNecessary(RootBeanDefinition beanDefinition) {
if (!beanDefinition.hasBeanClass() || !MapperFactoryBean.class.isAssignableFrom(beanDefinition.getBeanClass())) {
return;
}
if (beanDefinition.getResolvableType().hasUnresolvableGenerics()) {
Class<?> mapperInterface = getMapperInterface(beanDefinition);
if (mapperInterface != null) {
// Exposes a generic type information to context for prevent early initializing
beanDefinition
.setTargetType(ResolvableType.forClassWithGenerics(beanDefinition.getBeanClass(), mapperInterface));
}
}
}
private Class<?> getMapperInterface(RootBeanDefinition beanDefinition) {
try {
return (Class<?>) beanDefinition.getPropertyValues().get("mapperInterface");
}
catch (Exception e) {
LOG.debug("Fail getting mapper interface type.", e);
return null;
}
}
}
}
构建本地镜像
- 打开 x64 Native Tools Command Prompt for VS 2022(Visual Studio MSVC)
- 在项目的根目录运行一下命令
mvn -Pnative native:compile
完成以上步骤构建的本地镜像执行情况:
PS C:\Users\xxx\Desktop> .\graalvm.exe
. ____ _ __ _ _
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: Spring Boot :: (v3.1.3)
2023-08-30T18:09:44.170+08:00 INFO 9304 --- [ main] com.hyh.GraalvmApplication : Starting AOT-processed GraalvmApplication using Java 17.0.8 with PID 9304
2023-08-30T18:09:44.170+08:00 DEBUG 9304 --- [ main] com.hyh.GraalvmApplication : Running with Spring Boot v3.1.3, Spring v6.0.11
2023-08-30T18:09:44.170+08:00 INFO 9304 --- [ main] com.hyh.GraalvmApplication : No active profile set, falling back to 1 default profile: "default"
2023-08-30T18:09:44.226+08:00 INFO 9304 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat initialized with port(s): 9000 (http)
2023-08-30T18:09:44.226+08:00 INFO 9304 --- [ main] o.apache.catalina.core.StandardService : Starting service [Tomcat]
2023-08-30T18:09:44.226+08:00 INFO 9304 --- [ main] o.apache.catalina.core.StandardEngine : Starting Servlet engine: [Apache Tomcat/10.1.12]
2023-08-30T18:09:44.242+08:00 INFO 9304 --- [ main] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring embedded WebApplicationContext
2023-08-30T18:09:44.242+08:00 INFO 9304 --- [ main] w.s.c.ServletWebServerApplicationContext : Root WebApplicationContext: initialization completed in 72 ms
2023-08-30T18:09:44.339+08:00 INFO 9304 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port(s): 9000 (http) with context path ''
2023-08-30T18:09:44.339+08:00 INFO 9304 --- [ main] com.hyh.GraalvmApplication : Started GraalvmApplication in 0.218 seconds (process running for 0.242)
结尾
原生和本地镜像对比情况
大小 | 执行时间 | |
---|---|---|
原生 | 23.3 MB | 1.548 秒 |
本地镜像 | 102 MB | 0.218 秒 |