Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
a701b35
Add MyBatis native metadata and improve native-image test support
radovanradic Aug 21, 2026
07f56e9
Add branch `mybatis-native-tests` to GitHub Actions workflows
radovanradic Aug 21, 2026
774eff6
Merge remote-tracking branch 'origin/7.2.x' into mybatis-native-tests
radovanradic Aug 21, 2026
e8c2f5b
Enable binary compatibility checks for mybatis-processor starting fro…
radovanradic Aug 21, 2026
062595a
Remove `mybatis-native-tests` branch from GitHub Actions workflows
radovanradic Aug 21, 2026
bfd4084
Fix Sonar warnings.
radovanradic Aug 21, 2026
c997fee
Fix Sonar warnings.
radovanradic Aug 21, 2026
2d72f57
Ensure `Scan` can be used as a top-level record and add tests for equ…
radovanradic Aug 24, 2026
db2e21b
Merge branch '7.2.x' into mybatis-native-tests
radovanradic Aug 24, 2026
a20f91a
Refactor `Scan` equality checks to use pattern matching and fix relat…
radovanradic Aug 24, 2026
69240cc
Remove `javassist` dependency from MyBatis build configuration
radovanradic Aug 25, 2026
ec04ace
Generate aggregating MyBatis mapper registrations
radovanradic Sep 1, 2026
fe3fb55
Merge remote-tracking branch 'origin/7.2.x' into mybatis-native-tests
radovanradic Sep 3, 2026
0496d2c
Refactor MyBatis mapper registration logic for improved readability a…
radovanradic Sep 3, 2026
df59552
Ensure `intercept` proceeds when conditions are unmet in MyBatisMappe…
radovanradic Sep 3, 2026
31c1ff1
Merge branch '7.2.x' into mybatis-native-tests
radovanradic Sep 22, 2026
d03473b
Remove runtime-based MyBatisMapperScan functionality and introduce co…
radovanradic Sep 22, 2026
6d116c9
Switch from `ByteCodeGenerator` to `ByteCodeWriter` for improved depe…
radovanradic Sep 22, 2026
cbb7fbc
Update MyBatis documentation for kotlin and groovy processor configur…
radovanradic Sep 22, 2026
58eb446
Fix Sonar: Refactor MyBatis mapper scan to extract registration logic…
radovanradic Sep 23, 2026
237ccf3
Merge remote-tracking branch 'origin/7.2.x' into mybatis-native-tests
radovanradic Sep 23, 2026
57ba5c8
Refactor MyBatis documentation and examples to clarify `@Named("defau…
radovanradic Sep 23, 2026
50c8f42
Refactor MyBatis mapper scan: implement `MapperScanSupport` for share…
radovanradic Sep 23, 2026
fc272e7
Refactor MyBatis mapper scan to support package-based mapper grouping…
radovanradic Sep 23, 2026
803a872
Update GraalVM distribution to version 25.4 in GitHub Actions workflow
radovanradic Sep 23, 2026
728e326
Refactor MyBatis mapper scan: improve registration name calculation a…
radovanradic Sep 23, 2026
b7cc751
Merge remote-tracking branch 'origin/7.3.x' into mybatis-native-tests
radovanradic Sep 29, 2026
82a9182
Enable the processor binary compatibility checks after its first rele…
radovanradic Sep 29, 2026
db4b11d
Load the mapper scan registrations with the application context class…
radovanradic Sep 29, 2026
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
28 changes: 28 additions & 0 deletions mybatis-processor/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
plugins {
id("io.micronaut.build.internal.sql-module")
}

dependencies {
implementation(projects.micronautMybatis)
implementation(mn.micronaut.core.processor)
implementation(mn.micronaut.sourcegen.model)
implementation(mn.micronaut.sourcegen.bytecode.writer)

testImplementation(mn.micronaut.inject.java)
// Runs the GraalTypeElementVisitor in the tests to verify the generated reflection configuration
testImplementation(mn.micronaut.graal)
testImplementation(mnTest.junit.jupiter.api)
testImplementation(mnTest.junit.platform.launcher)

testRuntimeOnly(mnTest.junit.jupiter.engine)
}

tasks.withType<Test> {
useJUnitPlatform()
}

micronautBuild {
binaryCompatibility {
enabledAfter("7.3.0")
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
/*
* Copyright 2017-2026 original 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 io.micronaut.configuration.mybatis.processor;

import io.micronaut.configuration.mybatis.MyBatisMapperScan;
import io.micronaut.core.annotation.AnnotationClassValue;
import io.micronaut.core.annotation.AnnotationValue;
import io.micronaut.core.annotation.Internal;
import io.micronaut.inject.ast.ClassElement;
import io.micronaut.inject.ast.ElementQuery;
import io.micronaut.inject.ast.MethodElement;
import io.micronaut.inject.ast.ParameterElement;

import java.lang.annotation.Annotation;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;

/**
* Shared logic of the {@link MyBatisMapperScan} visitors.
*/
@Internal
final class MapperScanSupport {

private MapperScanSupport() {
}

/**
* Reads the members of a {@link MyBatisMapperScan} annotation.
*
* @param element The annotated element
* @return The scan, or {@code null} if the element is not annotated
*/
static Scan readScan(ClassElement element) {
AnnotationValue<MyBatisMapperScan> annotation = element.getAnnotation(MyBatisMapperScan.class);
if (annotation == null) {
return null;
}
List<String> mappers = new ArrayList<>();
for (AnnotationClassValue<?> mapper : annotation.annotationClassValues("mappers")) {

Check warning on line 55 in mybatis-processor/src/main/java/io/micronaut/configuration/mybatis/processor/MapperScanSupport.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use a stream instead of this loop.

See more on https://sonarcloud.io/project/issues?id=micronaut-projects_micronaut-sql&issues=AaDu6L3MZLIL3J8sjQqc&open=AaDu6L3MZLIL3J8sjQqc&pullRequest=2086
mappers.add(mapper.getName());
}
return new Scan(
element,
List.of(annotation.stringValues("value")),
mappers,
annotation.stringValue("datasource").orElse("default"),
annotation.booleanValue("nativeImageMetadata").orElse(true)
);
}

/**
* Collects the element and its nested types when they are interfaces. Nested interfaces are included
* because MyBatis runtime package scanning registers them as well.
*
* @param element The element
* @param interfaces The collected interfaces
*/
static void collectInterfaces(ClassElement element, List<ClassElement> interfaces) {
if (element.isInterface() && !element.isAssignable(Annotation.class)) {
interfaces.add(element);
}
for (ClassElement inner : element.getEnclosedElements(ElementQuery.ALL_INNER_CLASSES)) {
collectInterfaces(inner, interfaces);
}
}

/**
* @param typeName A type name
* @param packageName A package name
* @return Whether the type is in the package or one of its sub-packages
*/
static boolean isInPackage(String typeName, String packageName) {
int lastDot = typeName.lastIndexOf('.');
String typePackage = lastDot > 0 ? typeName.substring(0, lastDot) : "";
return typePackage.equals(packageName) || typePackage.startsWith(packageName + ".");
}

/**
* Collects the result and parameter types of the mapper methods, unwrapping containers. These are the
* types MyBatis instantiates and populates reflectively.
*
* @param mapper The mapper interface
* @param reflectiveTypes The collected type names
*/
static void collectReflectiveTypes(ClassElement mapper, Set<String> reflectiveTypes) {
for (MethodElement method : mapper.getEnclosedElements(ElementQuery.ALL_METHODS)) {
addReflectiveType(method.getGenericReturnType(), reflectiveTypes);
for (ParameterElement parameter : method.getParameters()) {
addReflectiveType(parameter.getGenericType(), reflectiveTypes);
}
}
}

private static void addReflectiveType(ClassElement type, Set<String> reflectiveTypes) {
if (type == null || type.isPrimitive() || type.isEnum()) {
return;
}
if (type.isArray()) {
addReflectiveType(type.fromArray(), reflectiveTypes);
return;
}
if (type.isAssignable(Iterable.class)
|| type.isAssignable(Map.class)
|| type.isAssignable(Optional.class)
|| type.isAssignable("java.util.stream.Stream")
|| type.isAssignable("org.reactivestreams.Publisher")) {
for (ClassElement typeArgument : type.getTypeArguments().values()) {
addReflectiveType(typeArgument, reflectiveTypes);
}
return;
}
String name = type.getName();
if (name.startsWith("java.") || name.startsWith("javax.") || name.startsWith("jakarta.")
|| name.startsWith("kotlin.") || name.startsWith("groovy.")
|| name.startsWith("org.apache.ibatis.")) {
return;
}
reflectiveTypes.add(name);
}

/**
* The members of a {@link MyBatisMapperScan} annotation.
*
* @param element The annotated element
* @param packages The scanned packages
* @param mappers The explicitly listed mappers
* @param datasource The datasource name
* @param nativeImageMetadata Whether to generate GraalVM metadata
*/
record Scan(ClassElement element,
List<String> packages,
List<String> mappers,
String datasource,
boolean nativeImageMetadata) {

boolean covers(String typeName) {
return packages.stream().anyMatch(packageName -> isInPackage(typeName, packageName));
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
/*
* Copyright 2017-2026 original 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 io.micronaut.configuration.mybatis.processor;

import io.micronaut.configuration.mybatis.MyBatisMapperScan;
import io.micronaut.configuration.mybatis.processor.MapperScanSupport.Scan;
import io.micronaut.core.annotation.AnnotationClassValue;
import io.micronaut.core.annotation.Internal;
import io.micronaut.core.annotation.ReflectionConfig;
import io.micronaut.core.annotation.TypeHint;
import io.micronaut.inject.ast.ClassElement;
import io.micronaut.inject.visitor.TypeElementVisitor;
import io.micronaut.inject.visitor.VisitorContext;

import java.util.ArrayList;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;

/**
* Adds the GraalVM native image metadata of the mappers registered by {@link MyBatisMapperScan} as
* {@link ReflectionConfig} values to the annotated type: MyBatis implements mapper interfaces with
* {@link java.lang.reflect.Proxy}, and instantiates and populates their result and parameter types reflectively.
*
* <p>The metadata itself is generated by the Micronaut {@code GraalTypeElementVisitor} from those values,
* exactly like for {@code @ReflectiveAccess}. That visitor is {@link VisitorKind#ISOLATING} and runs at
* order {@code -200}, so this visitor must be isolating as well (isolating and aggregating visitors run in
* different annotation processors on javac) and ordered before it, see {@link #getOrder()}.</p>
*
* <p>Like {@link MyBatisMapperScanVisitor}, the visitor visits every class because it has to see the
* unannotated mapper interfaces. Visitors process all classes before the next visitor runs, so the values
* added here are complete by the time the Graal visitor reaches the annotated type.</p>
*/
@Internal
public final class MyBatisMapperScanReflectionVisitor implements TypeElementVisitor<Object, Object> {

/**
* {@code io.micronaut.graal.reflect.GraalTypeElementVisitor#POSITION}.
*/
private static final int GRAAL_TYPE_ELEMENT_VISITOR_POSITION = -200;

private static final TypeHint.AccessType[] REFLECTIVE_TYPE_ACCESS = {
TypeHint.AccessType.ALL_DECLARED_CONSTRUCTORS,
TypeHint.AccessType.ALL_PUBLIC_CONSTRUCTORS,
TypeHint.AccessType.ALL_DECLARED_METHODS,
TypeHint.AccessType.ALL_PUBLIC_METHODS,
TypeHint.AccessType.ALL_DECLARED_FIELDS,
TypeHint.AccessType.ALL_PUBLIC_FIELDS
};

private final Map<String, ClassElement> interfaceTypes = new LinkedHashMap<>();
private final Map<Scan, Set<String>> scans = new LinkedHashMap<>();

@Override
public VisitorKind getVisitorKind() {
return VisitorKind.ISOLATING;
}

@Override
public int getOrder() {
return GRAAL_TYPE_ELEMENT_VISITOR_POSITION + 100;
}

@Override
public void visitClass(ClassElement element, VisitorContext context) {
List<ClassElement> interfaces = new ArrayList<>();
MapperScanSupport.collectInterfaces(element, interfaces);
for (ClassElement anInterface : interfaces) {
interfaceTypes.put(anInterface.getName(), anInterface);
for (Map.Entry<Scan, Set<String>> entry : scans.entrySet()) {
if (entry.getKey().covers(anInterface.getName())) {
register(entry.getKey(), entry.getValue(), anInterface);
}
}
}

Scan scan = MapperScanSupport.readScan(element);
if (scan == null || !scan.nativeImageMetadata()) {
return;
}
Set<String> registered = new HashSet<>();
scans.put(scan, registered);
for (String mapper : scan.mappers()) {
context.getClassElement(mapper).ifPresent(mapperElement -> register(scan, registered, mapperElement));
}
for (ClassElement anInterface : interfaceTypes.values()) {
if (scan.covers(anInterface.getName())) {
register(scan, registered, anInterface);
}
}
}

@Override
public void finish(VisitorContext visitorContext) {
interfaceTypes.clear();
scans.clear();
}

private static void register(Scan scan, Set<String> registered, ClassElement mapper) {
if (!registered.add(mapper.getName())) {
return;
}
ClassElement element = scan.element();
element.annotate(ReflectionConfig.class, builder -> builder
.member("type", new AnnotationClassValue<>(mapper.getName()))
.member("accessType", TypeHint.AccessType.DYNAMIC_PROXY));
Set<String> reflectiveTypes = new TreeSet<>();
MapperScanSupport.collectReflectiveTypes(mapper, reflectiveTypes);
for (String reflectiveType : reflectiveTypes) {
if (registered.add(reflectiveType)) {
element.annotate(ReflectionConfig.class, builder -> builder
.member("type", new AnnotationClassValue<>(reflectiveType))
.member("accessType", REFLECTIVE_TYPE_ACCESS));
}
}
}
}
Loading
Loading