{"id":661393,"date":"2025-11-26T15:50:05","date_gmt":"2025-11-26T14:50:05","guid":{"rendered":"https:\/\/blog.jetbrains.com\/?post_type=idea&#038;p=661393"},"modified":"2025-11-26T16:23:22","modified_gmt":"2025-11-26T15:23:22","slug":"spring-data-aot","status":"publish","type":"idea","link":"https:\/\/blog.jetbrains.com\/zh-hans\/idea\/2025\/11\/spring-data-aot","title":{"rendered":"Master Spring Data AOT in IntelliJ IDEA"},"content":{"rendered":"\n<p>Spring\u2019s AOT engine has been <a href=\"https:\/\/spring.io\/blog\/2021\/12\/09\/new-aot-engine-brings-spring-native-to-the-next-level\" target=\"_blank\" rel=\"noopener\">around<\/a> since the Spring Native days, but Spring Data never really benefited from it \u2013 until now. Repository infrastructure is one of the most dynamic parts of the framework, and for years it relied heavily on proxies, reflection, and queries generated at runtime.<\/p>\n\n\n\n<p>That flexibility has a price: it slows startup and forces the system to do considerable runtime processing just to interpret something as simple as <code>findPersonByNameLike<\/code>.<\/p>\n\n\n\n<p>With the latest Spring Data release, repositories finally <a href=\"https:\/\/spring.io\/blog\/2025\/11\/25\/spring-data-ahead-of-time-repositories-part-2\" target=\"_blank\" rel=\"noopener\">get proper AOT support<\/a>. Method queries can now be pre-generated during the build process, so you don\u2019t have to wait until runtime.<\/p>\n\n\n\n<p>And here\u2019s where IntelliJ IDEA 2025.3 comes in: you can now inspect, navigate, and debug these AOT-generated repository classes in the IDE. This article focuses on the developer experience, exploring how to configure build tools and how IntelliJ IDEA makes the development process faster, easier, and less error-prone.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">AOT repository internals overview<\/h2>\n\n\n\n<p>Enabling AOT is an easy task. You can find a good manual in the official documentation for both:<\/p>\n\n\n\n<ul>\n<li><a href=\"https:\/\/docs.spring.io\/spring-boot\/gradle-plugin\/aot.html\" target=\"_blank\" rel=\"noopener\">Gradle<\/a>&nbsp;<\/li>\n\n\n\n<li><a href=\"https:\/\/docs.spring.io\/spring-boot\/maven-plugin\/aot.html\" target=\"_blank\" rel=\"noopener\">Maven<\/a>&nbsp;<\/li>\n<\/ul>\n\n\n\n<p><\/p>\n\n\n\n<p>After running the build, by default, AOT-generated sources appear in:<\/p>\n\n\n\n<ul>\n<li><code>build\/generated\/aotSources<\/code> for Gradle<\/li>\n\n\n\n<li><code>target\/spring-aot\/main\/classes\/<\/code> if you use Maven<\/li>\n<\/ul>\n\n\n\n<p><\/p>\n\n\n\n<p>For IntelliJ IDEA, two artifacts matter most:<\/p>\n\n\n\n<ul>\n<li>The generated source code<\/li>\n\n\n\n<li>The JSON metadata describing repository methods and their queries<\/li>\n<\/ul>\n\n\n\n<p><\/p>\n\n\n\n<p>To illustrate, let\u2019s assume our application exposes quotes, and we need a method to fetch quotes by author:<\/p>\n\n\n\n<p><code>List&lt;Quote&gt; findAllByAuthor(String author);<\/code><\/p>\n\n\n\n<p>Depending on whether you use Spring Data JPA or JDBC, the generated implementation varies.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Example: Spring Data JPA<\/h3>\n\n\n\n<p>For JPA repositories, IntelliJ IDEA shows the generated JPQL query above the method signature.<\/p>\n\n\n\n<figure class=\"wp-block-image size-full\"><img decoding=\"async\" loading=\"lazy\" width=\"1310\" height=\"154\" src=\"https:\/\/blog.jetbrains.com\/wp-content\/uploads\/2025\/11\/image-45.png\" alt=\"\" class=\"wp-image-661992\"\/><\/figure>\n\n\n\n<p>Clicking the  <img decoding=\"async\" loading=\"lazy\" width=\"36\" height=\"34\" class=\"wp-image-662120\" style=\"width: 36px;\" src=\"https:\/\/blog.jetbrains.com\/wp-content\/uploads\/2025\/11\/Screenshot-2025-11-26-at-1.56.20-PM.png\" alt=\"\"> icon takes you straight to the AOT-generated implementation:<\/p>\n\n\n\n<pre class=\"EnlighterJSRAW\">public List&lt;Quote&gt; findAllByAuthor(String author) {\n  String queryString = &quot;SELECT q FROM Quote q WHERE q.author = :author&quot;;\n  Query query = this.entityManager.createQuery(queryString);\n  query.setParameter(&quot;author&quot;, author);\n  \n  return (List&lt;Quote&gt;) query.getResultList();\n}<\/pre>\n\n\n\n<p>This is the actual code that will be executed at runtime when AOT mode is enabled.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Example: Spring Data JDBC<\/h3>\n\n\n\n<p>For Spring Data JDBC, the picture is a bit different. We can see pure SQL and a list of fields selected for mapping to the Java object. This is the repository code with the query above the method:&nbsp;<\/p>\n\n\n\n<figure class=\"wp-block-image size-full\"><img decoding=\"async\" loading=\"lazy\" width=\"1600\" height=\"130\" src=\"https:\/\/blog.jetbrains.com\/wp-content\/uploads\/2025\/11\/image-47.png\" alt=\"\" class=\"wp-image-662071\"\/><\/figure>\n\n\n\n<p>And this is the generated code:<\/p>\n\n\n\n<pre class=\"EnlighterJSRAW\">public List&lt;Quote&gt; findAllByAuthor(String author) {\n  Criteria criteria = Criteria.where(&quot;author&quot;).is(author);\n  StatementFactory.SelectionBuilder builder = getStatementFactory().select(Quote.class).filter(criteria);\n\n  RowMapper rowMapper = getRowMapperFactory().create(Quote.class);\n  List result = (List) builder.executeWith((sql, paramSource) -&gt; getJdbcOperations().query(sql, paramSource, new RowMapperResultSetExtractor&lt;&gt;(rowMapper)));\n  return (List&lt;Quote&gt;) convertMany(result, Quote.class);\n}<\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">How IntelliJ IDEA displays queries<\/h3>\n\n\n\n<p>IntelliJ IDEA treats the AOT output as part of the project\u2019s source set. This provides the following:<\/p>\n\n\n\n<ul>\n<li>Direct navigation into generated methods<\/li>\n\n\n\n<li>Code highlighting and analysis<\/li>\n\n\n\n<li>Linking compiled classes to their generated sources<\/li>\n<\/ul>\n\n\n\n<p><\/p>\n\n\n\n<p>Spring Data\u2019s AOT process also creates JSON metadata for every repository method. These files are a part of the project\u2019s <em>resources<\/em>, and they provide the information about the actual JPQL\/SQL query:&nbsp;<\/p>\n\n\n\n<p>For JPA:<\/p>\n\n\n\n<pre class=\"EnlighterJSRAW\">\n{\n  &quot;name&quot;: &quot;findAllByAuthor&quot;,\n  &quot;signature&quot;: &quot;public abstract java.util.List&lt;org.test.demo2gradleaot.hello.Quote&gt; org.test.demo2gradleaot.hello.QuoteRepository.findAllByAuthor(java.lang.String)&quot;,\n  &quot;query&quot;: {\n    &quot;query&quot;: &quot;SELECT q FROM Quote q WHERE q.author = :author&quot;\n  }\n}<\/pre>\n\n\n\n<p>And for JDBC:<\/p>\n\n\n\n<pre class=\"EnlighterJSRAW\">{\n  &quot;name&quot;: &quot;findAllByAuthor&quot;,\n  &quot;signature&quot;: &quot;public abstract java.util.List&lt;com.jetbrains.test.boot4.server.quote.Quote&gt; com.jetbrains.test.boot4.server.quote.QuoteRepository.findAllByAuthor(java.lang.String)&quot;,\n  &quot;query&quot;: {\n    &quot;query&quot;: &quot;SELECT \\&quot;quote\\&quot;.\\&quot;id\\&quot; AS \\&quot;id\\&quot;, \\&quot;quote\\&quot;.\\&quot;text\\&quot; AS \\&quot;text\\&quot;, \\&quot;quote\\&quot;.\\&quot;author\\&quot; AS \\&quot;author\\&quot;, \\&quot;quote\\&quot;.\\&quot;source\\&quot; AS \\&quot;source\\&quot; FROM \\&quot;quote\\&quot; WHERE \\&quot;quote\\&quot;.\\&quot;author\\&quot; = :author&quot;\n  }\n}<\/pre>\n\n\n\n<p>If the generated query needs refinement, you can use the <em>Inline Query<\/em> action to insert it into a <code>@Query<\/code> annotation&nbsp;in the Spring Data repository code.&nbsp;<\/p>\n\n\n\n<figure class=\"wp-block-image size-full is-resized\"><img decoding=\"async\" loading=\"lazy\" width=\"840\" height=\"292\" src=\"https:\/\/blog.jetbrains.com\/wp-content\/uploads\/2025\/11\/image1-3.png\" alt=\"\" class=\"wp-image-662131\" style=\"aspect-ratio:2.8767123287671232;width:679px;height:auto\"\/><\/figure>\n\n\n\n<p>The annotated query will then be used as is during AOT generation.<\/p>\n\n\n\n<p>Most importantly, AOT-generated repository code is fully debuggable. You can place breakpoints directly inside the generated method and observe the exact query being executed \u2013 without navigating proxies or JDBC driver internals.&nbsp;<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Running applications with AOT<\/h2>\n\n\n\n<p>By default, Spring Boot applications do not load AOT-generated classes. Neither <code>.\/gradlew bootRun<\/code> nor <code>.\/mvnw spring-boot:run<\/code> nor clicking on the green triangle icon in IntelliJ IDEA will load AOT-generated classes into the JVM. You need to explicitly instruct the runner to use them. To do this, you pass the <code>-Dspring.aot.enabled=true<\/code> flag for the <em>application\u2019s<\/em> JVM.&nbsp;<\/p>\n\n\n\n<p>Let\u2019s have a look at the required setup for both Gradle and Maven.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Gradle<\/h3>\n\n\n\n<p>The simplest way to pass the <code>spring.aot.enabled<\/code> flag is to modify the <code>build.gradle.kts<\/code> and add the following lines:<\/p>\n\n\n\n<pre class=\"EnlighterJSRAW\">tasks.named&lt;BootRun&gt;(&quot;bootRun&quot;) {\n    if (project.hasProperty(&quot;aot&quot;)) {\n        jvmArgs(&quot;-Dspring.aot.enabled=true&quot;)\n        systemProperty(&quot;spring.profiles.active&quot;, &quot;aot&quot;)\n    }\n}<\/pre>\n\n\n\n<p>This will make Gradle search for the project property aot, bind its value to the system property spring.aot.enabled for the running application, and load a profile with DB connection properties. To run the application with AOT code, use the following command:<\/p>\n\n\n\n<p><code>.\/gradlew bootRun -Paot<\/code><\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Maven<\/h3>\n\n\n\n<p>For Maven, you need to:<\/p>\n\n\n\n<ul>\n<li>Execute the package lifecycle phase to generate AOT classes before running the application.<\/li>\n\n\n\n<li>Pass the spring.aot.enabled flag to the app JVM.<\/li>\n<\/ul>\n\n\n\n<p><\/p>\n\n\n\n<p>It is usually a good idea to make a separate Maven profile to run the application with AOT classes:&nbsp;&nbsp;&nbsp;<\/p>\n\n\n\n<pre class=\"EnlighterJSRAW\">&lt;profile&gt;\n    &lt;id&gt;aot&lt;\/id&gt;\n    &lt;properties&gt;\n        &lt;spring-boot.run.profiles&gt;aot,localdb&lt;\/spring-boot.run.profiles&gt;\n        &lt;spring-boot.run.jvmArguments&gt;-Dspring.aot.enabled=true&lt;\/spring-boot.run.jvmArguments&gt;\n    &lt;\/properties&gt;\n    &lt;build&gt;\n        &lt;plugins&gt;\n            &lt;plugin&gt;\n                &lt;groupId&gt;org.springframework.boot&lt;\/groupId&gt;\n                &lt;artifactId&gt;spring-boot-maven-plugin&lt;\/artifactId&gt;\n                &lt;executions&gt;\n                    &lt;execution&gt;\n                        &lt;id&gt;process-aot&lt;\/id&gt;\n                        &lt;goals&gt;\n                            &lt;goal&gt;process-aot&lt;\/goal&gt;\n                        &lt;\/goals&gt;\n                    &lt;\/execution&gt;\n                &lt;\/executions&gt;\n            &lt;\/plugin&gt;\n        &lt;\/plugins&gt;\n    &lt;\/build&gt;\n&lt;\/profile&gt;<\/pre>\n\n\n\n<p>Now you can run the application using:<\/p>\n\n\n\n<p><code>.\/mvnw -Paot package spring-boot:run<\/code><\/p>\n\n\n\n<p>\ud83d\udca1 Note: Spring Boot\u2019s automatic Docker Compose integration doesn\u2019t currently work when the application is launched in AOT mode. If your project uses Docker Compose integration, start required services manually and configure the proper connection properties (for example, via a dedicated Spring profile). Please note the `localdb` profile string in the provided settings.&nbsp;<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Debugging AOT code with IntelliJ IDEA<\/h2>\n\n\n\n<p>IntelliJ IDEA can run and debug an application that was started by Gradle or Maven. Just create a proper run configuration, and you\u2019re good to go.&nbsp;<\/p>\n\n\n\n<p>For Gradle:<\/p>\n\n\n\n<figure class=\"wp-block-image size-full\"><img decoding=\"async\" loading=\"lazy\" width=\"1999\" height=\"1597\" src=\"https:\/\/blog.jetbrains.com\/wp-content\/uploads\/2025\/11\/image7-1.png\" alt=\"\" class=\"wp-image-662142\"\/><\/figure>\n\n\n\n<p>For Maven:<\/p>\n\n\n\n<figure class=\"wp-block-image size-full\"><img decoding=\"async\" loading=\"lazy\" width=\"1999\" height=\"1572\" src=\"https:\/\/blog.jetbrains.com\/wp-content\/uploads\/2025\/11\/image2-1.png\" alt=\"\" class=\"wp-image-662153\"\/><\/figure>\n\n\n\n<p>IntelliJ IDEA\u2019s native build system still has limitations when dealing with AOT. So, if you\u2019re aiming to debug AOT repositories, we advise you to build the code using the external build system, then run the application with IntelliJ IDEA, and pass system variables and profile values as required. The run configuration will look like this (assuming you\u2019re using Maven):<\/p>\n\n\n\n<figure class=\"wp-block-image size-full\"><img decoding=\"async\" loading=\"lazy\" width=\"1999\" height=\"1501\" src=\"https:\/\/blog.jetbrains.com\/wp-content\/uploads\/2025\/11\/image6-1.png\" alt=\"\" class=\"wp-image-662164\"\/><\/figure>\n\n\n\n<p>Please note the following in the screenshot above:<\/p>\n\n\n\n<ul>\n<li>The <em>Before launch<\/em> task<\/li>\n\n\n\n<li>The <em>JVM Args<\/em> field with the spring.aot.enabled variable<\/li>\n<\/ul>\n\n\n\n<p><\/p>\n\n\n\n<p>With either of these run configurations, you\u2019ll be able to start the application and debug AOT repositories, while also having access to Spring Debugger features.<\/p>\n\n\n\n<figure class=\"wp-block-image size-full\"><img decoding=\"async\" loading=\"lazy\" width=\"1999\" height=\"1033\" src=\"https:\/\/blog.jetbrains.com\/wp-content\/uploads\/2025\/11\/image8-1.png\" alt=\"\" class=\"wp-image-662175\"\/><\/figure>\n\n\n\n<p>Seeing a real query string and ongoing transaction \u2013 isn\u2019t it exciting?&nbsp;<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Limitations<\/h3>\n\n\n\n<p>They say that there\u2019s no such thing as a free lunch. And that&#8217;s certainly true when working with AOT, which is why we must bear several important points in mind.<\/p>\n\n\n\n<h4 class=\"wp-block-heading\">Spring Data JDBC dialect requirement<\/h4>\n\n\n\n<p>Spring Data JDBC currently requires a dialect bean for AOT mode:<\/p>\n\n\n\n<pre class=\"EnlighterJSRAW\">@Configuration\nclass AotConfiguration {\n&nbsp;&nbsp;&nbsp;&nbsp;@Bean\n&nbsp;&nbsp;&nbsp;&nbsp;JdbcDialect dialect() {\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;return JdbcPostgresDialect.INSTANCE;\n&nbsp;&nbsp;&nbsp;&nbsp;}\n}<\/pre>\n\n\n\n<p>This requirement will probably be removed in subsequent Spring Boot 4 updates (see issue <a href=\"https:\/\/github.com\/spring-projects\/spring-boot\/issues\/47781\" target=\"_blank\" rel=\"noopener\">#47781<\/a>). Now this workaround is mentioned in the <a href=\"https:\/\/docs.spring.io\/spring-data\/relational\/reference\/jdbc\/aot.html\" target=\"_blank\" rel=\"noopener\">documentation<\/a>, but it\u2019s easy to overlook.&nbsp;<\/p>\n\n\n\n<h4 class=\"wp-block-heading\">DevTools incompatibility<\/h4>\n\n\n\n<p>If you see the following strange error during startup:<\/p>\n\n\n\n<pre class=\"EnlighterJSRAW\">java.lang.IllegalAccessError: failed to access class org.springframework.boot.autoconfigure.AutoConfigurationPackages$BasePackages \nfrom class org.springframework.boot.autoconfigure.AutoConfigurationPackages__BeanDefinitions$BasePackages (org.springframework.boot.autoconfigure.AutoConfigurationPackages$BasePackages \nis in unnamed module of loader &#039;app&#039;; org.springframework.boot.autoconfigure.AutoConfigurationPackages__BeanDefinitions$BasePackages \nis in unnamed module of loader org.springframework.boot.devtools.restart.classloader.RestartClassLoader @2b4a127a)<\/pre>\n\n\n\n<p>There\u2019s a good chance that DevTools is on the classpath. At the moment, AOT and DevTools are not fully compatible, so just disable DevTools, and the application will start normally.&nbsp;&nbsp;<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Conclusion<\/h2>\n\n\n\n<p>Spring Data AOT repositories introduce several meaningful improvements:<\/p>\n\n\n\n<ul>\n<li><a href=\"https:\/\/spring.io\/blog\/2025\/05\/22\/spring-data-ahead-of-time-repositories#how-does-it-work-and-what-can-you-expect\" target=\"_blank\" rel=\"noopener\">Faster application startup<\/a>&nbsp;<\/li>\n\n\n\n<li>Reduced memory usage<\/li>\n\n\n\n<li>Better native image performance<\/li>\n\n\n\n<li>And most importantly: real visibility into behavior that used to be hidden behind proxies and reflection<\/li>\n<\/ul>\n\n\n\n<p><\/p>\n\n\n\n<p>With IntelliJ IDEA 2025.3, you can now see the exact SQL or JPQL queries used by your application, navigate directly into the generated repository methods, and debug them just like any other code. This removes much of the guesswork associated with Spring Data behavior and lets you focus on what matters most \u2013 building clean, reliable business logic.<\/p>\n","protected":false},"author":1511,"featured_media":662188,"comment_status":"closed","ping_status":"closed","template":"","categories":[89],"tags":[8972,276,3145],"cross-post-tag":[],"acf":[],"_links":{"self":[{"href":"https:\/\/blog.jetbrains.com\/zh-hans\/wp-json\/wp\/v2\/idea\/661393"}],"collection":[{"href":"https:\/\/blog.jetbrains.com\/zh-hans\/wp-json\/wp\/v2\/idea"}],"about":[{"href":"https:\/\/blog.jetbrains.com\/zh-hans\/wp-json\/wp\/v2\/types\/idea"}],"author":[{"embeddable":true,"href":"https:\/\/blog.jetbrains.com\/zh-hans\/wp-json\/wp\/v2\/users\/1511"}],"replies":[{"embeddable":true,"href":"https:\/\/blog.jetbrains.com\/zh-hans\/wp-json\/wp\/v2\/comments?post=661393"}],"version-history":[{"count":10,"href":"https:\/\/blog.jetbrains.com\/zh-hans\/wp-json\/wp\/v2\/idea\/661393\/revisions"}],"predecessor-version":[{"id":662232,"href":"https:\/\/blog.jetbrains.com\/zh-hans\/wp-json\/wp\/v2\/idea\/661393\/revisions\/662232"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/blog.jetbrains.com\/zh-hans\/wp-json\/wp\/v2\/media\/662188"}],"wp:attachment":[{"href":"https:\/\/blog.jetbrains.com\/zh-hans\/wp-json\/wp\/v2\/media?parent=661393"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/blog.jetbrains.com\/zh-hans\/wp-json\/wp\/v2\/categories?post=661393"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/blog.jetbrains.com\/zh-hans\/wp-json\/wp\/v2\/tags?post=661393"},{"taxonomy":"cross-post-tag","embeddable":true,"href":"https:\/\/blog.jetbrains.com\/zh-hans\/wp-json\/wp\/v2\/cross-post-tag?post=661393"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}