r/java 1d ago

LarkBatis: A build-time MyBatis compiled to plain Java

https://larkbatis.github.io/latest/

My team is moving away from reflection and runtime proxies for cloud-native setups. MyBatis is heavily used in our stack, so I rewrote its core as an annotation processor: LarkBatis.

It resolves SQL, dynamic parameters (#{}), type handlers, and result mapping at compile time—generating plain Java code. At runtime, it executes direct JDBC calls with zero proxying, zero OGNL, and zero reflection.

Programming model: Standard MyBatis (mapper interfaces, XML, dynamic SQL).

Runtime: ~1,500 LOC, JDK 17+, JDBC-only dependency (GraalVM native-image works out of the box - I haven't test).

37 Upvotes

27 comments sorted by

6

u/fuckedupkid_yo 1d ago

This is nice, finally an alternative to jooq (not that it's bad, but i know even partial migration to it is a risk a lot of teams would not take)

I know at least one of my seniors who would absolutely love this (dude lives and breathes mybatis 😂)

5

u/catmewo 1d ago

Thanks. jOOQ solves a bigger problem than I'm trying to. But I'm a fan of MyBatis because of its simplicity.
Anyway, PRs are welcome!

3

u/woj-tek 1d ago

Hmm… it kinda looks like JDBI? How does it compare?

3

u/_predator_ 1d ago

I understood the value proposition to be that you're already neck-deep into MyBatis and just want to get rid of the reflection overhead, i.e. migrating to an entirely different lib with different idioms is off the table.

5

u/repeating_bears 1d ago

Nice. I've thought for years that Java relies too much on reflection. I like codegen from an annotation processor as an alternative 

I do a lot of java codegen and I'd strongly recommend to ditch javapoet. I started out with that and it seems like the default choice, but I don't think it really deserves it's popularity 

A template language works better because the template code you write and the output code that's generated is much more similar than the indirection of a fluent API/DSL thing.  I settled on Velocity because IntelliJ's support for it is pretty good. You get syntax highlighting, method calls in templates show up when you do "find usages", etc 

The main thing Velocity is missing is something to manage imports, avoid name clashes, etc. I wrote a lib to do that, wasn't difficult. I meant to open source it at some point but it would need some polish 

2

u/NovaX 1d ago

imho it depends on the use-case. The majority of the time the templates are small and simple, which is why that approach has been the default for decades. When templates become large with many conditionals they are difficult to read and maintain. It then its much nicer to structure around a rules engine to build up based on the needed capabilities. Both approaches some thought towards the design structure or they are big balls of mud, but that's the author's fault and not the approaches. Caffeine uses JavaPoet and I think Velocity would be a significantly worse approach, but I'd also say the reverse would be true for many other projects.

2

u/repeating_bears 1d ago

When templates become large with many conditionals they are difficult to read and maintain

Javapoet is always difficult to read and maintain because it's a level of indirection.

Java codegen is basically the primary thing I work on these days in some substantial frameworks and so far I haven't hit a point of unmaintainability. If you need lots of conditionals in a template, the approach is probably wrong. Just like if you need lots of conditionals in a method, you probably did something wrong. In a method you'd lean on polymorphism. In a template there are similar things you can do.

But after writing that comment I did wonder whether a hybrid approach might be viable. i.e. to primarily use templates, and to use JavaPoet to generate smaller fragments, e.g. single methods. If Javapoet would allow you to extract enough context, it might work.

2

u/NovaX 1d ago

Usually unorganized templates become a rats nest of conditionals depending on what the caller's input specification is. If feature/capability may need fields, methods, or a common method may need adjustments based on what is available. Writing that in a classic template approach becomes ugly when the number of specification options increases. If instead all of the details are collected into a rule evaluation, applying fields/methods/customizations is cohesive and composable. Those cases make JavaPoet less difficult to read, as it has a higher floor but lower ceiling.

The problem with both approaches is simply most usages do not put effort into code structure and neither approach teaches how to organize for code generation. Templates are wonderful for the common, simple cases and should be the default. Its simply that once the structure is laid out then either work well, but without both can turn into horrible messes and usually the mess is what developers are exposed to.

1

u/repeating_bears 1d ago

You are creating a false dichotomy of a template with many conditionals vs Javapoet with, as you called it, a rule evalulation. There is nothing unique to Javapoet that lets you structure your code like that. You could structure Velocity code with rule evalutations for the parts that need that level of "polymorpism" (and that's what we do!)

1

u/OddEstimate1627 1d ago edited 1d ago

I do a lot of code generation as well and significantly prefer JavaPoet for some use cases.

Templates are good for some things, but IMO they become much harder to read and maintain once you run into a lot of variations.

Try generating something like a Protobuf API purely from templates (that's the use case JavaPoet was originally designed for).

1

u/repeating_bears 1d ago

Part of what we generate is a proprietary binary format for de/serialization of entities, so conceptually quite similar to Protobuf actually 

Doesn't use lots of nested conditions. We structure to avoid it

1

u/NovaX 1d ago

No, I gave an example of how JavaPoet can be wrangled and examples of where Templates can become a mess. I repeatedly said both make sense and both need structure, which by default is what developers struggle to figure out. I've tried to show both can make sense and its not so black-and-white.

1

u/repeating_bears 1d ago

"Both make sense" is an opinion not a fact.

Javapoet code is one level of abstraction beyond what you're generating, which is inherently bad. Any benefits would have to be extreme to make up for that downside. I haven't seen you name any benefit. You just said templates can be confusing if you use lots of conditionals, but you are not obligated to use lots of conditions.

I've yet to see any Javapoet code which isn't an overabstracted mess. That includes my own honest attempts, that Caffeine example, and everything else I've seen.

2

u/NovaX 1d ago

Fair enough to disagree, just your opinions are not facts regardless of how strongly you try to assert them.

1

u/repeating_bears 1d ago

I've not been giving many opinions. The 3rd paragraph there was, I suppose.

Benefits of templates: looks like the code you're generating, syntax highlighting, ctrl F works some of the time (as opposed to never), IDE integration (find usages, auto complete)

Disadvantage of templates: some people use lots of conditions (not mandatory)

Benefits of javapoet: as yet unspecified

Oh it manages the imports for you, I guess. It's not hard to replicate though

1

u/catmewo 1d ago

Thanks for your suggestion. I'm using Javapoet after researching on Github. However, It looks like I have to check Velocity right now

2

u/TheKingOfSentries 1d ago

My personal favorite for templating is Jstachio because it's also a compile time templating library

1

u/repeating_bears 1d ago edited 1d ago

It doesn't have to be Velocity. I think any template language could work, but that one seemed to have good IDE support.

Here's an example of the template file from some random class in one of my projects. It needs a little extra wiring than just this to put variables in the "context" but it's very simple

#imports()
import com.example.EventMetadata;
import org.jspecify.annotations.NullMarked;
#end

/// Fired after [$Entity]s are created.
@NullMarked
final class CreateEvent implements $Interface {
    Create(EventMetadata metadata, $Entity entity) {
        super(metadata, entity);
    }

    @Override
    public int entityTypeId() {
        return $entity.id();
    }

    @Override
    public $Key.box() key() {
        return item().${entity.key().name()}();
    }

    @Override
    public Class<?> entityClass() {
        return ${Entity}.class;
    }
}

And then here's a snippet from your codebase (not an equivalent class)

```java TypeSpec.Builder type = TypeSpec.classBuilder(result.readerSimpleName()) .addModifiers(Modifier.PUBLIC, Modifier.FINAL) .addAnnotation(EmitSupport.generatedAnnotation()) .addJavadoc("Row reader for {@link $T} — generated by LarkBatis, do not edit.\n" + "One reader per result class; the manual escape hatch\n" + "reuses {@link #READER}.\n", resultClass);

type.addField(FieldSpec.builder( ParameterizedTypeName.get(EmitSupport.ROW_READER, resultClass), "READER", Modifier.PUBLIC, Modifier.STATIC, Modifier.FINAL) .initializer("$T::read", readerClass) .build()); ```

I think it should be pretty clear which of these styles is nicer to work with

2

u/quantum-fudge 1d ago edited 1d ago

I'm very confused by the naming convention, which probably comes from MyBatis and not your project. Why are DAOs (things that perform CRUD operations) called mappers? It's rather these Row classes (e.g.  UserRow ) that seem to be doing what I'd call mapping - translating a ResultSet to a domain objects.

Again, this is probably just my lack of familiarity with MyBatis' nomenclature, but I had to ask.

Cool project btw :)

1

u/TomKavees 1d ago

The repo does not have license information, but assuming it would be actually open source, what are your thoughts on moving this under the main MyBatis project?

2

u/catmewo 1d ago

Hi, it's Apache 2.0. There's a LICENSE file in the repo, but the docs site never mentions it, which is on me. I'll add it.

On moving it under the main MyBatis project, I don't think that's possible for now. A lot of MyBatis features depend on reflection.
So I think the shape of this would have to be something like a MyBatis 4 that intentionally drops support for that class of features. If that ever happens, I'd be glad to contribute to it.

1

u/bowbahdoe 1d ago

Can this find setters in a superclass? I mostly ask to know if it would be compatible with github.com/bowbahdoe/magic-bean

(It will be a bit until i get around to testing it)

1

u/bowbahdoe 1d ago

Also...is this largely AI? Because the docs read like it

1

u/kaumein 4h ago

Really cool project. I’ve been working on a very similar idea with kaumei-jdbc (https://kaumei-jdbc.kaumei.io), but from the other direction.

LarkBatis starts with the MyBatis programming model and compiles it down to plain JDBC.

With kaumei-jdbc, I started from plain JDBC itself and built around compile-time code generation, type safety, statically generated conversion, no reflection, no runtime proxies, and a very small runtime.

So there’s quite a bit of overlap in the underlying idea, but the starting point and focus are different.

I especially want to take a closer look at how you handle generated keys — there are a few ideas there that look interesting for my own implementation.

Nice to see more work in this direction.

PS: kaumei-jdbc 0.2.0 just landed

1

u/vips7L 1d ago edited 1d ago

“Works with native image but we haven’t ran a build or tested it” OK let’s actually test the claims we (or your LLM) makes. 

Compile time everything is great. For those that like ORMs Ebean has a similar feature: https://ebean.io/docs/query/query-beans

-1

u/Round_Patient_7420 1d ago

Nice!! It seems similar to Spring Data.