Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

✨ Support annotation based on MyBatis for Spring Boot 3 Native #995

Open
wants to merge 8 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
82 changes: 82 additions & 0 deletions .github/workflows/native.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
#
# Copyright 2015-2024 the original author or authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#

name: Spring Boot Native Support Samples

on: [ push, pull_request ]

jobs:
test-ubuntu:
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ ubuntu-latest, macOS-latest ]
java: [ 17.0.9, 21.0.2, 22.0.2 ]
fail-fast: false
max-parallel: 5
name: Test GraalVM JDK ${{ matrix.java }}, ${{ matrix.os }}

steps:
- uses: actions/checkout@v4

- name: Ubuntu Set up JDK
if: ${{ matrix.os == 'ubuntu-latest' }}
run: |
JAVA_HOME=$RUNNER_WORKSPACE/.graalvm
echo $JAVA_HOME
mkdir -p $JAVA_HOME
echo "JAVA_HOME=$JAVA_HOME" >> $GITHUB_ENV
curl -L -o graalvm.tar.gz https://github.com/graalvm/graalvm-ce-builds/releases/download/jdk-${{ matrix.java }}/graalvm-community-jdk-${{ matrix.java }}_linux-x64_bin.tar.gz
tar -zxvf graalvm.tar.gz -C $JAVA_HOME --strip-components=1
ls -lh $JAVA_HOME
mvn -v

- name: MacOS Set up JDK
if: ${{ matrix.os == 'macOS-latest' }}
run: |
JAVA_HOME=$RUNNER_WORKSPACE/.graalvm
echo $JAVA_HOME
mkdir -p $JAVA_HOME
curl -L -o graalvm.tar.gz https://github.com/graalvm/graalvm-ce-builds/releases/download/jdk-${{ matrix.java }}/graalvm-community-jdk-${{ matrix.java }}_macos-x64_bin.tar.gz
tar -zxvf graalvm.tar.gz -C $JAVA_HOME --strip-components=1
JAVA_HOME=$RUNNER_WORKSPACE/.graalvm/Contents/Home
echo $JAVA_HOME
echo "JAVA_HOME=$JAVA_HOME" >> $GITHUB_ENV
ls -lh $JAVA_HOME
mvn -v

- name: Cache modules
uses: actions/cache@v4
with:
path: |
~/.m2/repository
key: ${{ runner.os }}-${{ matrix.java }}

- name: Ubuntu Prerequisites
if: ${{ matrix.os == 'ubuntu-latest' }}
run: sudo apt-get install build-essential zlib1g-dev

#- name: MacOS Prerequisites
# if: ${{ matrix.os == 'macOS-latest' }}
# run: xcode-select --install

- name: Test with Spring Boot Native Latest
run: ./mvnw -V clean compile -Pnative native:compile -am -pl mybatis-spring-boot-samples/mybatis-spring-boot-sample-graalvm-annotation,mybatis-spring-boot-samples/mybatis-spring-boot-sample-graalvm-xml

- name: Run Native Latest
run: |
mybatis-spring-boot-samples/mybatis-spring-boot-sample-graalvm-annotation/target/mybatis-spring-boot-sample-graalvm-annotation
mybatis-spring-boot-samples/mybatis-spring-boot-sample-graalvm-xml/target/mybatis-spring-boot-sample-graalvm-xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
/*
* Copyright 2015-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.mybatis.spring.boot.autoconfigure;

import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import org.apache.ibatis.reflection.TypeParameterResolver;
import org.mybatis.spring.mapper.MapperFactoryBean;
import org.mybatis.spring.mapper.MapperScannerConfigurer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.aot.hint.MemberCategory;
import org.springframework.aot.hint.RuntimeHints;
import org.springframework.beans.PropertyValue;
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.ConfigurableListableBeanFactory;
import org.springframework.beans.factory.support.RegisteredBean;
import org.springframework.core.env.Environment;
import org.springframework.core.io.Resource;
import org.springframework.core.io.ResourceLoader;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.util.ReflectionUtils;
import org.springframework.util.ResourceUtils;
import org.springframework.util.StringUtils;

/**
* @since 3.0.4
*/
class MyBatisBeanFactoryInitializationAotProcessor
implements BeanFactoryInitializationAotProcessor, BeanRegistrationExcludeFilter {

private static final Logger logger = LoggerFactory.getLogger(MyBatisBeanFactoryInitializationAotProcessor.class);

private static final ResourceLoader RESOURCE_RESOLVER = new PathMatchingResourcePatternResolver();

private static final String CONFIG_LOCATION = MybatisProperties.MYBATIS_PREFIX + ".config-location";

private static final String MAPPER_LOCATIONS = MybatisProperties.MYBATIS_PREFIX + ".mapper-locations";

private static final Set<Class<?>> EXCLUDE_CLASSES = new HashSet<>();

static {
EXCLUDE_CLASSES.add(MapperScannerConfigurer.class);
}

@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();

Environment environment = beanFactory.getBean(Environment.class);
configLocation(environment, hints);
mapperLocations(environment, hints);

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);
registerMapperRelationships(mapperInterfaceType, hints);
}
}
}
};
}

@Override
public boolean isExcludedFromAotProcessing(RegisteredBean registeredBean) {
return EXCLUDE_CLASSES.contains(registeredBean.getBeanClass());
}

private void configLocation(Environment environment, RuntimeHints hints) {
String configLocation = environment.getProperty(CONFIG_LOCATION);
if (StringUtils.hasText(configLocation)) {
Resource resource = RESOURCE_RESOLVER.getResource(configLocation);
if (resource.exists()) {
Stream.of(resource).forEach(hints.resources()::registerResource);
} else {
logger.error("{}: {} does not exist", CONFIG_LOCATION, configLocation);
}
}
}

private void mapperLocations(Environment environment, RuntimeHints hints) {
String[] mapperLocations = environment.getProperty(MAPPER_LOCATIONS, String[].class);
if (mapperLocations != null) {
for (String mapperLocation : mapperLocations) {
Stream.of(mapperLocation.replace(ResourceUtils.CLASSPATH_URL_PREFIX, ""))
.forEach(hints.resources()::registerPattern);
}
}
}

private void registerMapperRelationships(Class<?> mapperInterfaceType, RuntimeHints hints) {
Method[] methods = ReflectionUtils.getAllDeclaredMethods(mapperInterfaceType);
for (Method method : methods) {
if (method.getDeclaringClass() != Object.class) {
ReflectionUtils.makeAccessible(method);
Class<?> returnType = MyBatisMapperTypeUtils.resolveReturnClass(mapperInterfaceType, method);
registerReflectionTypeIfNecessary(returnType, hints);
MyBatisMapperTypeUtils.resolveParameterClasses(mapperInterfaceType, method)
.forEach(x -> registerReflectionTypeIfNecessary(x, hints));
}
}
}

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) {
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;
}

}

private void registerReflectionTypeIfNecessary(Class<?> type, RuntimeHints hints) {
if (!type.isPrimitive() && !type.getName().startsWith("java")) {
hints.reflection().registerType(type, MemberCategory.values());
}
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
/*
* Copyright 2015-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.mybatis.spring.boot.autoconfigure;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.mybatis.spring.mapper.MapperFactoryBean;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.BeanFactoryAware;
import org.springframework.beans.factory.config.ConfigurableBeanFactory;
import org.springframework.beans.factory.config.ConstructorArgumentValues;
import org.springframework.beans.factory.support.MergedBeanDefinitionPostProcessor;
import org.springframework.beans.factory.support.RootBeanDefinition;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.ResolvableType;
import org.springframework.util.ClassUtils;

/**
* The {@link MergedBeanDefinitionPostProcessor} for customizing a {@link MapperFactoryBean}.
*
* @author Stéphane Nicoll
* @author Kazuki Shimizu
* @author xuxiaowei
*
* @since 3.0.4
*/
@Configuration
class MyBatisMapperFactoryBeanPostProcessor implements MergedBeanDefinitionPostProcessor, BeanFactoryAware {

private static final Log LOG = LogFactory.getLog(MyBatisMapperFactoryBeanPostProcessor.class);

private static final String MAPPER_FACTORY_BEAN = MapperFactoryBean.class.getName();

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
ConstructorArgumentValues constructorArgumentValues = new ConstructorArgumentValues();
constructorArgumentValues.addGenericArgumentValue(mapperInterface);
beanDefinition.setConstructorArgumentValues(constructorArgumentValues);
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;
}
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
[
{ "name": "org.apache.ibatis.logging.slf4j.Slf4jImpl", "allDeclaredConstructors": true, "allDeclaredMethods": true },
{ "name": "org.apache.ibatis.logging.commons.JakartaCommonsLoggingImpl", "allDeclaredConstructors": true, "allDeclaredMethods": true },
{ "name": "org.apache.ibatis.logging.log4j2.Log4j2Impl", "allDeclaredConstructors": true, "allDeclaredMethods": true },
{ "name": "org.apache.ibatis.logging.log4j.Log4jImpl", "allDeclaredConstructors": true, "allDeclaredMethods": true },
{ "name": "org.apache.ibatis.logging.jdk14.Jdk14LoggingImpl", "allDeclaredConstructors": true, "allDeclaredMethods": true },
{ "name": "org.apache.ibatis.logging.nologging.NoLoggingImpl", "allDeclaredConstructors": true, "allDeclaredMethods": true },
{ "name": "org.apache.ibatis.plugin.Interceptor", "allDeclaredConstructors": true, "allDeclaredMethods": true },
{ "name": "org.apache.ibatis.javassist.util.proxy.ProxyFactory", "allDeclaredConstructors": true, "allDeclaredMethods": true },
{ "name": "org.apache.ibatis.scripting.xmltags.XMLLanguageDriver", "methods": [{ "name": "<init>", "parameterTypes": [] }] },
{ "name": "org.apache.ibatis.scripting.defaults.RawLanguageDriver", "methods": [{ "name": "<init>", "parameterTypes": [] }] },
{ "name": "org.mybatis.spring.SqlSessionFactoryBean", "allDeclaredConstructors": true, "allDeclaredMethods": true },
{ "name": "java.util.ArrayList", "methods": [{ "name": "<init>", "parameterTypes": [] }] },
{ "name": "java.util.HashMap", "methods": [{ "name": "<init>", "parameterTypes": [] }] },
{ "name": "java.util.TreeSet", "methods": [{ "name": "<init>", "parameterTypes": [] }] },
{ "name": "java.util.HashSet", "methods": [{ "name": "<init>", "parameterTypes": [] }] },
{ "name": "org.apache.ibatis.session.Configuration", "methods": [{ "name": "<init>", "parameterTypes": [] }] },
{ "name": "org.mybatis.spring.boot.autoconfigure.SpringBootVFS", "methods": [{ "name": "<init>", "parameterTypes": [] }] }
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"resources": {
"includes": [
{ "pattern": "org/apache/ibatis/builder/xml/mybatis-3-config.dtd" },
{ "pattern": "org/apache/ibatis/builder/xml/mybatis-3-mapper.dtd" },
{ "pattern": "org/apache/ibatis/builder/xml/mybatis-config.xsd" },
{ "pattern": "org/apache/ibatis/builder/xml/mybatis-mapper.xsd" }
]
}
}
Loading