├── .gitignore ├── .mvn └── wrapper │ ├── MavenWrapperDownloader.java │ ├── maven-wrapper.jar │ └── maven-wrapper.properties ├── README.md ├── mvnw ├── mvnw.cmd ├── pom.xml └── src ├── main ├── java │ └── demo │ │ ├── DemoApplication.java │ │ ├── DemoController.java │ │ ├── DemoModel.java │ │ ├── VisitsRepository.java │ │ └── package-info.java └── resources │ ├── application.properties │ └── templates │ ├── index.mustache │ └── layout.mustache └── test └── java └── demo └── DemoApplicationTests.java /.gitignore: -------------------------------------------------------------------------------- 1 | HELP.md 2 | target/ 3 | jte-classes/ 4 | !.mvn/wrapper/maven-wrapper.jar 5 | !**/src/main/**/target/ 6 | !**/src/test/**/target/ 7 | 8 | ### STS ### 9 | .apt_generated 10 | .classpath 11 | .factorypath 12 | .project 13 | .settings 14 | .springBeans 15 | .sts4-cache 16 | 17 | ### IntelliJ IDEA ### 18 | .idea 19 | *.iws 20 | *.iml 21 | *.ipr 22 | 23 | ### NetBeans ### 24 | /nbproject/private/ 25 | /nbbuild/ 26 | /dist/ 27 | /nbdist/ 28 | /.nb-gradle/ 29 | build/ 30 | !**/src/main/**/build/ 31 | !**/src/test/**/build/ 32 | 33 | ### VS Code ### 34 | .vscode/ 35 | -------------------------------------------------------------------------------- /.mvn/wrapper/MavenWrapperDownloader.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2007-present the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | import java.net.*; 17 | import java.io.*; 18 | import java.nio.channels.*; 19 | import java.util.Properties; 20 | 21 | public class MavenWrapperDownloader { 22 | 23 | private static final String WRAPPER_VERSION = "0.5.6"; 24 | /** 25 | * Default URL to download the maven-wrapper.jar from, if no 'downloadUrl' is provided. 26 | */ 27 | private static final String DEFAULT_DOWNLOAD_URL = "https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/" 28 | + WRAPPER_VERSION + "/maven-wrapper-" + WRAPPER_VERSION + ".jar"; 29 | 30 | /** 31 | * Path to the maven-wrapper.properties file, which might contain a downloadUrl property to 32 | * use instead of the default one. 33 | */ 34 | private static final String MAVEN_WRAPPER_PROPERTIES_PATH = 35 | ".mvn/wrapper/maven-wrapper.properties"; 36 | 37 | /** 38 | * Path where the maven-wrapper.jar will be saved to. 39 | */ 40 | private static final String MAVEN_WRAPPER_JAR_PATH = 41 | ".mvn/wrapper/maven-wrapper.jar"; 42 | 43 | /** 44 | * Name of the property which should be used to override the default download url for the wrapper. 45 | */ 46 | private static final String PROPERTY_NAME_WRAPPER_URL = "wrapperUrl"; 47 | 48 | public static void main(String args[]) { 49 | System.out.println("- Downloader started"); 50 | File baseDirectory = new File(args[0]); 51 | System.out.println("- Using base directory: " + baseDirectory.getAbsolutePath()); 52 | 53 | // If the maven-wrapper.properties exists, read it and check if it contains a custom 54 | // wrapperUrl parameter. 55 | File mavenWrapperPropertyFile = new File(baseDirectory, MAVEN_WRAPPER_PROPERTIES_PATH); 56 | String url = DEFAULT_DOWNLOAD_URL; 57 | if(mavenWrapperPropertyFile.exists()) { 58 | FileInputStream mavenWrapperPropertyFileInputStream = null; 59 | try { 60 | mavenWrapperPropertyFileInputStream = new FileInputStream(mavenWrapperPropertyFile); 61 | Properties mavenWrapperProperties = new Properties(); 62 | mavenWrapperProperties.load(mavenWrapperPropertyFileInputStream); 63 | url = mavenWrapperProperties.getProperty(PROPERTY_NAME_WRAPPER_URL, url); 64 | } catch (IOException e) { 65 | System.out.println("- ERROR loading '" + MAVEN_WRAPPER_PROPERTIES_PATH + "'"); 66 | } finally { 67 | try { 68 | if(mavenWrapperPropertyFileInputStream != null) { 69 | mavenWrapperPropertyFileInputStream.close(); 70 | } 71 | } catch (IOException e) { 72 | // Ignore ... 73 | } 74 | } 75 | } 76 | System.out.println("- Downloading from: " + url); 77 | 78 | File outputFile = new File(baseDirectory.getAbsolutePath(), MAVEN_WRAPPER_JAR_PATH); 79 | if(!outputFile.getParentFile().exists()) { 80 | if(!outputFile.getParentFile().mkdirs()) { 81 | System.out.println( 82 | "- ERROR creating output directory '" + outputFile.getParentFile().getAbsolutePath() + "'"); 83 | } 84 | } 85 | System.out.println("- Downloading to: " + outputFile.getAbsolutePath()); 86 | try { 87 | downloadFileFromURL(url, outputFile); 88 | System.out.println("Done"); 89 | System.exit(0); 90 | } catch (Throwable e) { 91 | System.out.println("- Error downloading"); 92 | e.printStackTrace(); 93 | System.exit(1); 94 | } 95 | } 96 | 97 | private static void downloadFileFromURL(String urlString, File destination) throws Exception { 98 | if (System.getenv("MVNW_USERNAME") != null && System.getenv("MVNW_PASSWORD") != null) { 99 | String username = System.getenv("MVNW_USERNAME"); 100 | char[] password = System.getenv("MVNW_PASSWORD").toCharArray(); 101 | Authenticator.setDefault(new Authenticator() { 102 | @Override 103 | protected PasswordAuthentication getPasswordAuthentication() { 104 | return new PasswordAuthentication(username, password); 105 | } 106 | }); 107 | } 108 | URL website = new URL(urlString); 109 | ReadableByteChannel rbc; 110 | rbc = Channels.newChannel(website.openStream()); 111 | FileOutputStream fos = new FileOutputStream(destination); 112 | fos.getChannel().transferFrom(rbc, 0, Long.MAX_VALUE); 113 | fos.close(); 114 | rbc.close(); 115 | } 116 | 117 | } 118 | -------------------------------------------------------------------------------- /.mvn/wrapper/maven-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dsyer/java-template-demo/daad8e23cc1147a2a5d1d51331f0edd03292d6b4/.mvn/wrapper/maven-wrapper.jar -------------------------------------------------------------------------------- /.mvn/wrapper/maven-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.8.1/apache-maven-3.8.1-bin.zip 2 | wrapperUrl=https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Reflectionless Templates With Spring 2 | 3 | A few Java libraries have shown up recently that use text templates, but compile to Java classes at build time. They can thus claim to some extent to be "reflection free". Together with potential benefits of runtime performance, they promise to be easy to use and integrate with GraalVM native image compilation, so they are quite interesting for people just getting started with that stack in Spring Boot 3.x. We take a look at a selection of libraries ([JStachio](https://github.com/jstachio/jstachio), [Rocker](https://github.com/fizzed/rocker), [JTE](https://github.com/casid/jte) and [ManTL](https://github.com/manifold-systems/manifold/tree/master/manifold-deps-parent/manifold-templates)) and how to get them running. 4 | 5 | The source code for the samples is in [GitHub](https://github.com/dsyer/java-template-demo) and each template engine has its own branch. The sample is intentionally very simple and doesn't use all the features of the template engines. The focus is on how to integrate them with Spring Boot and GraalVM. 6 | 7 | ## JStachio 8 | 9 | Since it is my favourite, I will start with JStachio. It is very easy to use and has a very small footprint and is also very fast at runtime. The templates are plain text files written in [Mustache](https://mustache.github.io/) which are then compiled to Java classes at build time and rendered at runtime. 10 | 11 | In the sample there is a template for the home page (`index.mustache`) that just prints a greeting and a visitor count: 12 | 13 | ```html 14 | {{ 17 |
18 | You are visitor number {{visits}}. 19 | {{/body}} 20 | {{/layout}} 21 | ``` 22 | 23 | It uses a trivial "layout" template (`layout.mustache`): 24 | 25 | ```html 26 | 27 | 28 | {{$body}}{{/body}} 29 | 30 | 31 | ``` 32 | 33 | (The layout is not strictly necessary but it is a good way to show how to compose templates). 34 | 35 | The JStachio APT processor will generate a Java class for each template it finds with a `@JStache` annotation which is used to identify the template file in the source code. In this case we have: 36 | 37 | ```java 38 | @JStache(path = "index") 39 | public class DemoModel { 40 | public String name; 41 | public long visits; 42 | 43 | public DemoModel(String name, long visits) { 44 | this.name = name; 45 | this.visits = visits; 46 | } 47 | } 48 | ``` 49 | 50 | The `path` attribute of the `@JStache` annotation is the name of the template file without the extension (see below for how that gets stitched together). You could also use a Java record for the model which is neat, but since the other template engines don't support it we'll leave it out and make the samples more comparable. 51 | 52 | ### Build Configuration 53 | 54 | To compile this to a Java class, you need to add some configuration to the compiler plugin in `pom.xml`: 55 | 56 | ```xml 57 | 58 | maven-compiler-plugin 59 | 60 | 61 | 62 | io.jstach 63 | jstachio-apt 64 | ${jstachio.version} 65 | 66 | 67 | 68 | 69 | ``` 70 | 71 | JStachio comes with some Spring Boot integration, so you only need to add it to the classpath: 72 | 73 | ```xml 74 | 75 | io.jstach 76 | jstachio-spring-boot-starter-webmvc 77 | ${jstachio.version} 78 | 79 | ``` 80 | 81 | ### Controller 82 | 83 | You can use the template in a controller, for example: 84 | 85 | ```java 86 | @GetMapping("/") 87 | public View view() { 88 | visitsRepository.add(); 89 | return JStachioModelView.of(new DemoModel("World", visitsRepository.get())); 90 | } 91 | ``` 92 | 93 | This controller returns a `View` constructed from a `DemoModel`. It could also just return the `DemoModel` directly and Spring Boot will wrap it in a `JStachioModelView` automatically. 94 | 95 | ### JStachio Configuration 96 | 97 | There is also global configuration in the `DemoApplication` class: 98 | 99 | ```java 100 | @JStachePath(prefix = "templates/", suffix = ".mustache") 101 | @SpringBootApplication 102 | public class DemoApplication { 103 | ... 104 | } 105 | ``` 106 | 107 | and a `package-info.java` file that points back to it (you need one of these per Java package that contains `@JStache` models): 108 | 109 | ```java 110 | @JStacheConfig(using = DemoApplication.class) 111 | package demo; 112 | ... 113 | ``` 114 | 115 | ### Running the Sample 116 | 117 | Run the application with `./mvnw spring-boot:run` (or in the IDE from the `main` method) and you should see the home page at `http://localhost:8080/`. 118 | 119 | The generated sources after compilation are in `target/generated-sources/annotations` and you can see the generated Java class for the `DemoModel` there: 120 | 121 | ``` 122 | $ tree target/generated-sources/annotations/ 123 | target/generated-sources/annotations/ 124 | └── demo 125 | └── DemoModelRenderer.java 126 | ``` 127 | 128 | The sample also includes a [test main](https://docs.spring.io/spring-boot/docs/3.2.3/maven-plugin/reference/htmlsingle/#run.test-run-goal) so you can run from the command line with `./mvnw spring-boot:test-run` or via the test main in the IDE, and the application will restart when you make changes in the IDE. One of the disadvantages of the build-time compilation is that you have to force a recompile to see changes in the templates. The IDE won't do that automatically, so you might have to use another tool to trigger a recompile. I have had some success with using this to force the model class to recompile when the template changes: 129 | 130 | ``` 131 | $ while inotifywait src/main/resources/templates -e close_write; do \ 132 | sleep 1; \ 133 | find src/main/java -name \*Model.java -exec touch {} \;; \ 134 | done 135 | ``` 136 | 137 | The `inotifywait` command is a tool that waits for a file to be closed after a write. It is easy to install and use on any Linux distribution or on a Mac. 138 | 139 | ### Native Image 140 | 141 | A native image can be generated with no additional configuration using `./mvnw -P native spring-boot:build-image` (or using the `native-image` plugin directly). The image starts up in less than 0.1s: 142 | 143 | ``` 144 | $ docker run -p 8080:8080 demo:0.0.1-SNAPSHOT 145 | 146 | . ____ _ __ _ _ 147 | /\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \ 148 | ( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \ 149 | \\/ ___)| |_)| | | | | || (_| | ) ) ) ) 150 | ' |____| .__|_| |_|_| |_\__, | / / / / 151 | =========|_|==============|___/=/_/_/_/ 152 | :: Spring Boot :: (v3.2.4) 153 | 154 | 2024-03-22T12:23:45.403Z INFO 1 --- [ main] demo.DemoApplication : Starting AOT-processed DemoApplication using Java 17.0.10 with PID 1 (/workspace/demo.DemoApplication started by cnb in /workspace) 155 | 2024-03-22T12:23:45.403Z INFO 1 --- [ main] demo.DemoApplication : No active profile set, falling back to 1 default profile: "default" 156 | 2024-03-22T12:23:45.418Z INFO 1 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat initialized with port 8080 (http) 157 | 2024-03-22T12:23:45.419Z INFO 1 --- [ main] o.apache.catalina.core.StandardService : Starting service [Tomcat] 158 | 2024-03-22T12:23:45.419Z INFO 1 --- [ main] o.apache.catalina.core.StandardEngine : Starting Servlet engine: [Apache Tomcat/10.1.19] 159 | 2024-03-22T12:23:45.429Z INFO 1 --- [ main] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring embedded WebApplicationContext 160 | 2024-03-22T12:23:45.429Z INFO 1 --- [ main] w.s.c.ServletWebServerApplicationContext : Root WebApplicationContext: initialization completed in 26 ms 161 | 2024-03-22T12:23:45.462Z INFO 1 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port 8080 (http) with context path '' 162 | 2024-03-22T12:23:45.462Z INFO 1 --- [ main] demo.DemoApplication : Started DemoApplication in 0.069 seconds (process running for 0.073) 163 | ``` 164 | 165 | ## Rocker 166 | 167 | Rocker can be used in a similar way to JStachio. The templates are written in a custom language that is like HTML with additional Java features (a bit like JSP). The home page looks like this (`demo.rocker.html`): 168 | 169 | ```html 170 | @import demo.DemoModel 171 | 172 | @args(DemoModel model) 173 | 174 | @templates.layout.template("Demo") -> { 175 |

Demo

176 |

Hello @model.name!

177 |
178 |
179 |

You are visitor number @model.visits.

180 | } 181 | ``` 182 | 183 | It imports the `DemoModel` object - the implementation is identical to the JStachio sample. The template also refers directly to its layout (calling a static method on `templates.layout`). The layout is a separate template file (`layout.rocker.html`): 184 | 185 | ```html 186 | @args (String title, RockerBody content) 187 | 188 | 189 | 190 | @title 191 | 192 | 193 | @content 194 | 195 | 196 | ``` 197 | 198 | ### Build Configuration 199 | 200 | Rocker needs an APT processor and some manual addition of the generated sources to the build input. It can all be configured in the `pom.xml`: 201 | 202 | ```xml 203 | 204 | com.fizzed 205 | rocker-maven-plugin 206 | 1.2.1 207 | 208 | 209 | 210 | generate-rocker-templates 211 | generate-sources 212 | 213 | generate 214 | 215 | 216 | ${java.version} 217 | src/main/resources 218 | target/generated-sources/rocker 219 | true 220 | UTF-8 221 | 222 | com.fizzed.rocker.processor.LoggingProcessor 223 | com.fizzed.rocker.processor.WhitespaceRemovalProcessor 224 | 225 | 226 | 227 | 228 | 229 | 230 | org.codehaus.mojo 231 | build-helper-maven-plugin 232 | 233 | 234 | generate-sources 235 | 236 | add-source 237 | 238 | 239 | 240 | ${project.build.directory}/generated-sources/rocker 241 | 242 | 243 | 244 | 245 | 246 | ``` 247 | 248 | ### Controller 249 | 250 | The controller implementation is very conventional - it constructs a model and returns the name of the "demo" view: 251 | 252 | ```java 253 | @GetMapping("/") 254 | public String view(Model model) { 255 | visitsRepository.add(); 256 | model.addAttribute("arguments", Map.of("model", new DemoModel("mystérieux visiteur", visitsRepository.get()))); 257 | return "demo"; 258 | } 259 | ``` 260 | 261 | We are using a naming convention for "arguments" as a special model attribute. This is a detail of the `View` implementation that we will see later. 262 | 263 | ### Rocker Configuration 264 | 265 | Rocker doesn't come with its own Spring Boot integration but it's not hard to implement, and you only have to do it once. The sample contains a `View` implementation, plus a `ViewResolver` and some configuration in `RockerAutoConfiguration`: 266 | 267 | ```java 268 | @Configuration 269 | public class RockerAutoConfiguration { 270 | @Bean 271 | public ViewResolver rockerViewResolver() { 272 | return new RockerViewResolver(); 273 | } 274 | } 275 | ``` 276 | 277 | The `RockerViewResolver` is a `ViewResolver` that uses the Rocker template engine to render the templates. The `View` implementation is a wrapper around the Rocker template class: 278 | 279 | ```java 280 | public class RockerViewResolver implements ViewResolver, Ordered { 281 | 282 | private String prefix = "templates/"; 283 | private String suffix = ".rocker.html"; 284 | 285 | @Override 286 | @Nullable 287 | public View resolveViewName(String viewName, Locale locale) throws Exception { 288 | RockerView view = new RockerView(prefix + viewName + suffix); 289 | return view; 290 | } 291 | 292 | @Override 293 | public int getOrder() { 294 | return Ordered.LOWEST_PRECEDENCE - 10; 295 | } 296 | 297 | } 298 | ``` 299 | 300 | If you look at the implementation of `RockerView` you will see that it is a wrapper around the Rocker template class, and that it contains some reflection code to find the template parameter names. This could be a problem for the native image, so it is not ideal, but we will see how to fix it later. Rocker internally also uses reflection to bind the template parameters to the model, so it is not completely reflection free anyway. 301 | 302 | ### Running the Sample 303 | 304 | If you run the sample with `./mvnw spring-boot:run` you will see the home page at `http://localhost:8080/`. The generated source code comes out as one Java class per template in `target/generated-sources/rocker/`: 305 | 306 | ``` 307 | $ tree target/generated-sources/rocker/ 308 | target/generated-sources/rocker/ 309 | └── templates 310 | ├── demo.java 311 | └── layout.java 312 | ``` 313 | 314 | ### Native Image 315 | 316 | The native image would need some additional configuration to permit the reflection during rendering. We had a few attempts at this and it quickly became apparent that reflection is used all over the internals of Rocker and it would be a lot of effort to get it to work with GraalVM. Maybe worth coming back to one day. 317 | 318 | ## JTE 319 | 320 | (The JTE sample is a direct copy from the project documentation. The other samples in this document only have the structure they do because they mirror this one.) 321 | 322 | Like Rocker, JTE has a template language similar to HTML with additional Java features. The templates in the project documentation are in a `jte` directory alongside `java`, so we adopt the same convention. The home page looks like this (`demo.jte`): 323 | 324 | ```java 325 | @import demo.DemoModel 326 | 327 | @param DemoModel model 328 | 329 | Hello ${model.name}! 330 |
331 |
332 | You are visitor number ${model.visits}. 333 | ``` 334 | 335 | There is no layout template in this sample because JTE doesn't explicitly support composition of templates. The `DemoModel` is similar to the one we used for the other samples. 336 | 337 | ### Build Configuration 338 | 339 | In the `pom.xml` you need to add the JTE compiler plugin: 340 | 341 | ```xml 342 | 343 | gg.jte 344 | jte-maven-plugin 345 | ${jte.version} 346 | 347 | ${basedir}/src/main/jte 348 | Html 349 | true 350 | 351 | 352 | 353 | 354 | generate-sources 355 | 356 | generate 357 | 358 | 359 | 360 | 361 | ``` 362 | 363 | along with some source and resource copying: 364 | 365 | ```xml 366 | 367 | org.codehaus.mojo 368 | build-helper-maven-plugin 369 | 370 | 371 | generate-sources 372 | 373 | add-source 374 | 375 | 376 | 377 | ${project.build.directory}/generated-sources/jte 378 | 379 | 380 | 381 | 382 | 383 | 384 | 385 | maven-resources-plugin 386 | 3.0.2 387 | 388 | 389 | copy-resources 390 | process-classes 391 | 392 | copy-resources 393 | 394 | 395 | ${project.build.outputDirectory} 396 | 397 | 398 | ${basedir}/target/generated-sources/jte 399 | 400 | **/*.bin 401 | 402 | false 403 | 404 | 405 | 406 | 407 | 408 | 409 | ``` 410 | 411 | The runtime dependencies are: 412 | 413 | ```xml 414 | 415 | gg.jte 416 | jte 417 | ${jte.version} 418 | 419 | 420 | gg.jte 421 | jte-spring-boot-starter-3 422 | ${jte.version} 423 | 424 | ``` 425 | 426 | ### Controller 427 | 428 | The controller implementation is very conventional - in fact it is identical to the one we used for Rocker. 429 | 430 | ### JTE Configuration 431 | 432 | JTE comes with its own Spring Boot autoconfiguration (we added it in the `pom.xml`), so you almost don't need to do anything else. There is one tiny thing you need to do to make it work with Spring Boot 3.x, which is to add a property to the `application.properties` file. For development time, especially if you are using [Spring Boot Devtools](https://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#using.devtools), you would want: 433 | 434 | ``` 435 | gg.jte.developmentMode=true 436 | ``` 437 | 438 | In production, switch that off with a Spring profile and use `gg.jte.usePrecompiledTemplates=true` instead. 439 | 440 | ### Running the Sample 441 | 442 | If you run the sample with `./mvnw spring-boot:run` you will see the home page at `http://localhost:8080/`. The generated source code comes out as one Java class per template in `target/generated-sources/jte/`: 443 | 444 | ``` 445 | $ tree target/generated-sources/jte/ 446 | target/generated-sources/jte/ 447 | └── gg 448 | └── jte 449 | └── generated 450 | └── precompiled 451 | ├── JtedemoGenerated.bin 452 | └── JtedemoGenerated.java 453 | ``` 454 | 455 | The `.bin` file is an efficient binary representation of the text template that is used at runtime, so it needs to be added to the classpath. 456 | 457 | ### Native Image 458 | 459 | A native image can be generated with some additional configuration. We need to make sure the `.bin` files are available and also that the generated Java classes can be reflected on: 460 | 461 | ```java 462 | @SpringBootApplication 463 | @ImportRuntimeHints(DemoRuntimeHints.class) 464 | public class DemoApplication { 465 | ... 466 | } 467 | 468 | class DemoRuntimeHints implements RuntimeHintsRegistrar { 469 | 470 | @Override 471 | public void registerHints(RuntimeHints hints, @Nullable ClassLoader classLoader) { 472 | hints.resources().registerPattern("**/*.bin"); 473 | hints.reflection().registerType(JtedemoGenerated.class, MemberCategory.INVOKE_DECLARED_CONSTRUCTORS, MemberCategory.INVOKE_DECLARED_METHODS); 474 | } 475 | 476 | } 477 | ``` 478 | 479 | So JTE is not completely reflection free, but it is possible to configure it quite easily to work with GraalVM native. 480 | 481 | ## ManTL 482 | 483 | ManTL (Manifold Template Language) is another template engine with Java-like syntax. The templates are compiled to Java classes at build time like with the other samples. The home page looks like this (`Demo.html.mtl`): 484 | 485 | ```html 486 | <%@ import demo.DemoModel %> 487 | 488 | <%@ params(DemoModel model) %> 489 | 490 | Hello ${model.name}! 491 |
492 |
493 | You are visitor number ${model.visits}. 494 | ``` 495 | 496 | where `DemoModel` is the same as in the other samples. 497 | 498 | ### Build Configuration 499 | 500 | Manifold is a bit different to the other examples in that it uses a JDK compiler plugin, as opposed to an APT processor. The configuration in `pom.xml` is a bit more complex. There is the `maven-compiler-plugin`: 501 | 502 | ```xml 503 | 504 | org.apache.maven.plugins 505 | maven-compiler-plugin 506 | 3.8.0 507 | 508 | 509 | -Xplugin:Manifold 510 | 511 | 512 | 513 | systems.manifold 514 | manifold-templates 515 | ${manifold.version} 516 | 517 | 518 | 519 | ``` 520 | 521 | and the runtime dependency: 522 | 523 | ```xml 524 | 525 | systems.manifold 526 | manifold-templates-rt 527 | ${manifold.version} 528 | 529 | ``` 530 | 531 | ### Controller 532 | 533 | Our controller in this sample looks more like the JStachio one than the Rocker/JTE one: 534 | 535 | ```java 536 | @GetMapping("/") 537 | public View view(Model model, HttpServletResponse response) { 538 | visitsRepository.add(); 539 | return new StringView(() -> Demo.render(new DemoModel("mystérieux visiteur", visitsRepository.get()))); 540 | } 541 | ``` 542 | 543 | where `StringView` is a convenience class that wraps the template and renders it: 544 | 545 | ```java 546 | public class StringView implements View { 547 | 548 | private final Supplier output; 549 | 550 | public StringView(Supplier output) { 551 | this.output = output; 552 | } 553 | 554 | @Override 555 | public void render(Map model, HttpServletRequest request, HttpServletResponse response) 556 | throws Exception { 557 | String result = output.get(); 558 | response.setContentType(MediaType.TEXT_HTML_VALUE); 559 | response.setCharacterEncoding(StandardCharsets.UTF_8.name()); 560 | response.setContentLength(result.getBytes().length); 561 | 562 | response.getOutputStream().write(result.getBytes()); 563 | response.flushBuffer(); 564 | } 565 | } 566 | ``` 567 | 568 | ### Running the Sample 569 | 570 | You can build and run the application on the command line using `./mvnw spring-boot:run` and inspect the result on `http://localhost:8080`. The generated source code comes out as a class and ancillary stuff per template: 571 | 572 | ``` 573 | $ tree target/classes/templates/ 574 | target/classes/templates/ 575 | ├── Demo$LayoutOverride.class 576 | ├── Demo.class 577 | └── Demo.html.mtl 578 | ``` 579 | 580 | ManTL only works in IntelliJ after installing a special plugin, and not at all in Eclipse or NetBeans or VSCode. You may be able to run the main method from those IDEs, but the code that refers to templates will have compiler errors because the compiler plugin is missing. 581 | 582 | ### Native Image 583 | 584 | The compiler plugin is not supported by GraalVM, so you can't use ManTL with GraalVM native images. 585 | 586 | ## Summary 587 | 588 | All the template engines we looked at here are reflection free in the sense that the templates are compiled to Java classes at build time. They are all easy to use and integrate with Spring, and they all have or can be provided with some kind of Spring Boot autoconfiguration. JStachio is the most lightweight and fastest at runtime, and it has the best support for GraalVM native images. Rocker is also very fast at runtime, but it uses reflection internally and it is not easy to get it to work with GraalVM. JTE is a bit more complex to configure, but it is also very fast at runtime and it is easy to get it to work with GraalVM. ManTL is the most complex to configure and it doesn't work with GraalVM at all. It also only works with IntelliJ as an IDE. 589 | 590 | If you would like to see more samples then the each of the template engines has its own documentation, so follow the links above. My own work on JStachio has produced a few additional examples, for example the [Mustache PetClinic](https://github.com/spring-petclinic/spring-petclinic-mustache/tree/jstache), and also a [Todo MVC](https://github.com/dsyer/spring-todo-mvc/tree/jstachio) implementation, originally by [Ollie Drotbohm](https://github.com/odrotbohm) and adapted to various different template engines. 591 | 592 | Dave Syer 593 | London 2024 -------------------------------------------------------------------------------- /mvnw: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # ---------------------------------------------------------------------------- 3 | # Licensed to the Apache Software Foundation (ASF) under one 4 | # or more contributor license agreements. See the NOTICE file 5 | # distributed with this work for additional information 6 | # regarding copyright ownership. The ASF licenses this file 7 | # to you under the Apache License, Version 2.0 (the 8 | # "License"); you may not use this file except in compliance 9 | # with the License. You may obtain a copy of the License at 10 | # 11 | # https://www.apache.org/licenses/LICENSE-2.0 12 | # 13 | # Unless required by applicable law or agreed to in writing, 14 | # software distributed under the License is distributed on an 15 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 16 | # KIND, either express or implied. See the License for the 17 | # specific language governing permissions and limitations 18 | # under the License. 19 | # ---------------------------------------------------------------------------- 20 | 21 | # ---------------------------------------------------------------------------- 22 | # Maven Start Up Batch script 23 | # 24 | # Required ENV vars: 25 | # ------------------ 26 | # JAVA_HOME - location of a JDK home dir 27 | # 28 | # Optional ENV vars 29 | # ----------------- 30 | # M2_HOME - location of maven2's installed home dir 31 | # MAVEN_OPTS - parameters passed to the Java VM when running Maven 32 | # e.g. to debug Maven itself, use 33 | # set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 34 | # MAVEN_SKIP_RC - flag to disable loading of mavenrc files 35 | # ---------------------------------------------------------------------------- 36 | 37 | if [ -z "$MAVEN_SKIP_RC" ] ; then 38 | 39 | if [ -f /etc/mavenrc ] ; then 40 | . /etc/mavenrc 41 | fi 42 | 43 | if [ -f "$HOME/.mavenrc" ] ; then 44 | . "$HOME/.mavenrc" 45 | fi 46 | 47 | fi 48 | 49 | # OS specific support. $var _must_ be set to either true or false. 50 | cygwin=false; 51 | darwin=false; 52 | mingw=false 53 | case "`uname`" in 54 | CYGWIN*) cygwin=true ;; 55 | MINGW*) mingw=true;; 56 | Darwin*) darwin=true 57 | # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home 58 | # See https://developer.apple.com/library/mac/qa/qa1170/_index.html 59 | if [ -z "$JAVA_HOME" ]; then 60 | if [ -x "/usr/libexec/java_home" ]; then 61 | export JAVA_HOME="`/usr/libexec/java_home`" 62 | else 63 | export JAVA_HOME="/Library/Java/Home" 64 | fi 65 | fi 66 | ;; 67 | esac 68 | 69 | if [ -z "$JAVA_HOME" ] ; then 70 | if [ -r /etc/gentoo-release ] ; then 71 | JAVA_HOME=`java-config --jre-home` 72 | fi 73 | fi 74 | 75 | if [ -z "$M2_HOME" ] ; then 76 | ## resolve links - $0 may be a link to maven's home 77 | PRG="$0" 78 | 79 | # need this for relative symlinks 80 | while [ -h "$PRG" ] ; do 81 | ls=`ls -ld "$PRG"` 82 | link=`expr "$ls" : '.*-> \(.*\)$'` 83 | if expr "$link" : '/.*' > /dev/null; then 84 | PRG="$link" 85 | else 86 | PRG="`dirname "$PRG"`/$link" 87 | fi 88 | done 89 | 90 | saveddir=`pwd` 91 | 92 | M2_HOME=`dirname "$PRG"`/.. 93 | 94 | # make it fully qualified 95 | M2_HOME=`cd "$M2_HOME" && pwd` 96 | 97 | cd "$saveddir" 98 | # echo Using m2 at $M2_HOME 99 | fi 100 | 101 | # For Cygwin, ensure paths are in UNIX format before anything is touched 102 | if $cygwin ; then 103 | [ -n "$M2_HOME" ] && 104 | M2_HOME=`cygpath --unix "$M2_HOME"` 105 | [ -n "$JAVA_HOME" ] && 106 | JAVA_HOME=`cygpath --unix "$JAVA_HOME"` 107 | [ -n "$CLASSPATH" ] && 108 | CLASSPATH=`cygpath --path --unix "$CLASSPATH"` 109 | fi 110 | 111 | # For Mingw, ensure paths are in UNIX format before anything is touched 112 | if $mingw ; then 113 | [ -n "$M2_HOME" ] && 114 | M2_HOME="`(cd "$M2_HOME"; pwd)`" 115 | [ -n "$JAVA_HOME" ] && 116 | JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" 117 | fi 118 | 119 | if [ -z "$JAVA_HOME" ]; then 120 | javaExecutable="`which javac`" 121 | if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then 122 | # readlink(1) is not available as standard on Solaris 10. 123 | readLink=`which readlink` 124 | if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then 125 | if $darwin ; then 126 | javaHome="`dirname \"$javaExecutable\"`" 127 | javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" 128 | else 129 | javaExecutable="`readlink -f \"$javaExecutable\"`" 130 | fi 131 | javaHome="`dirname \"$javaExecutable\"`" 132 | javaHome=`expr "$javaHome" : '\(.*\)/bin'` 133 | JAVA_HOME="$javaHome" 134 | export JAVA_HOME 135 | fi 136 | fi 137 | fi 138 | 139 | if [ -z "$JAVACMD" ] ; then 140 | if [ -n "$JAVA_HOME" ] ; then 141 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 142 | # IBM's JDK on AIX uses strange locations for the executables 143 | JAVACMD="$JAVA_HOME/jre/sh/java" 144 | else 145 | JAVACMD="$JAVA_HOME/bin/java" 146 | fi 147 | else 148 | JAVACMD="`which java`" 149 | fi 150 | fi 151 | 152 | if [ ! -x "$JAVACMD" ] ; then 153 | echo "Error: JAVA_HOME is not defined correctly." >&2 154 | echo " We cannot execute $JAVACMD" >&2 155 | exit 1 156 | fi 157 | 158 | if [ -z "$JAVA_HOME" ] ; then 159 | echo "Warning: JAVA_HOME environment variable is not set." 160 | fi 161 | 162 | CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher 163 | 164 | # traverses directory structure from process work directory to filesystem root 165 | # first directory with .mvn subdirectory is considered project base directory 166 | find_maven_basedir() { 167 | 168 | if [ -z "$1" ] 169 | then 170 | echo "Path not specified to find_maven_basedir" 171 | return 1 172 | fi 173 | 174 | basedir="$1" 175 | wdir="$1" 176 | while [ "$wdir" != '/' ] ; do 177 | if [ -d "$wdir"/.mvn ] ; then 178 | basedir=$wdir 179 | break 180 | fi 181 | # workaround for JBEAP-8937 (on Solaris 10/Sparc) 182 | if [ -d "${wdir}" ]; then 183 | wdir=`cd "$wdir/.."; pwd` 184 | fi 185 | # end of workaround 186 | done 187 | echo "${basedir}" 188 | } 189 | 190 | # concatenates all lines of a file 191 | concat_lines() { 192 | if [ -f "$1" ]; then 193 | echo "$(tr -s '\n' ' ' < "$1")" 194 | fi 195 | } 196 | 197 | BASE_DIR=`find_maven_basedir "$(pwd)"` 198 | if [ -z "$BASE_DIR" ]; then 199 | exit 1; 200 | fi 201 | 202 | ########################################################################################## 203 | # Extension to allow automatically downloading the maven-wrapper.jar from Maven-central 204 | # This allows using the maven wrapper in projects that prohibit checking in binary data. 205 | ########################################################################################## 206 | if [ -r "$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" ]; then 207 | if [ "$MVNW_VERBOSE" = true ]; then 208 | echo "Found .mvn/wrapper/maven-wrapper.jar" 209 | fi 210 | else 211 | if [ "$MVNW_VERBOSE" = true ]; then 212 | echo "Couldn't find .mvn/wrapper/maven-wrapper.jar, downloading it ..." 213 | fi 214 | if [ -n "$MVNW_REPOURL" ]; then 215 | jarUrl="$MVNW_REPOURL/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" 216 | else 217 | jarUrl="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" 218 | fi 219 | while IFS="=" read key value; do 220 | case "$key" in (wrapperUrl) jarUrl="$value"; break ;; 221 | esac 222 | done < "$BASE_DIR/.mvn/wrapper/maven-wrapper.properties" 223 | if [ "$MVNW_VERBOSE" = true ]; then 224 | echo "Downloading from: $jarUrl" 225 | fi 226 | wrapperJarPath="$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" 227 | if $cygwin; then 228 | wrapperJarPath=`cygpath --path --windows "$wrapperJarPath"` 229 | fi 230 | 231 | if command -v wget > /dev/null; then 232 | if [ "$MVNW_VERBOSE" = true ]; then 233 | echo "Found wget ... using wget" 234 | fi 235 | if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then 236 | wget "$jarUrl" -O "$wrapperJarPath" 237 | else 238 | wget --http-user=$MVNW_USERNAME --http-password=$MVNW_PASSWORD "$jarUrl" -O "$wrapperJarPath" 239 | fi 240 | elif command -v curl > /dev/null; then 241 | if [ "$MVNW_VERBOSE" = true ]; then 242 | echo "Found curl ... using curl" 243 | fi 244 | if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then 245 | curl -o "$wrapperJarPath" "$jarUrl" -f 246 | else 247 | curl --user $MVNW_USERNAME:$MVNW_PASSWORD -o "$wrapperJarPath" "$jarUrl" -f 248 | fi 249 | 250 | else 251 | if [ "$MVNW_VERBOSE" = true ]; then 252 | echo "Falling back to using Java to download" 253 | fi 254 | javaClass="$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.java" 255 | # For Cygwin, switch paths to Windows format before running javac 256 | if $cygwin; then 257 | javaClass=`cygpath --path --windows "$javaClass"` 258 | fi 259 | if [ -e "$javaClass" ]; then 260 | if [ ! -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then 261 | if [ "$MVNW_VERBOSE" = true ]; then 262 | echo " - Compiling MavenWrapperDownloader.java ..." 263 | fi 264 | # Compiling the Java class 265 | ("$JAVA_HOME/bin/javac" "$javaClass") 266 | fi 267 | if [ -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then 268 | # Running the downloader 269 | if [ "$MVNW_VERBOSE" = true ]; then 270 | echo " - Running MavenWrapperDownloader.java ..." 271 | fi 272 | ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$MAVEN_PROJECTBASEDIR") 273 | fi 274 | fi 275 | fi 276 | fi 277 | ########################################################################################## 278 | # End of extension 279 | ########################################################################################## 280 | 281 | export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"} 282 | if [ "$MVNW_VERBOSE" = true ]; then 283 | echo $MAVEN_PROJECTBASEDIR 284 | fi 285 | MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" 286 | 287 | # For Cygwin, switch paths to Windows format before running java 288 | if $cygwin; then 289 | [ -n "$M2_HOME" ] && 290 | M2_HOME=`cygpath --path --windows "$M2_HOME"` 291 | [ -n "$JAVA_HOME" ] && 292 | JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` 293 | [ -n "$CLASSPATH" ] && 294 | CLASSPATH=`cygpath --path --windows "$CLASSPATH"` 295 | [ -n "$MAVEN_PROJECTBASEDIR" ] && 296 | MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"` 297 | fi 298 | 299 | # Provide a "standardized" way to retrieve the CLI args that will 300 | # work with both Windows and non-Windows executions. 301 | MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $@" 302 | export MAVEN_CMD_LINE_ARGS 303 | 304 | WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain 305 | 306 | exec "$JAVACMD" \ 307 | $MAVEN_OPTS \ 308 | -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ 309 | "-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ 310 | ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" 311 | -------------------------------------------------------------------------------- /mvnw.cmd: -------------------------------------------------------------------------------- 1 | @REM ---------------------------------------------------------------------------- 2 | @REM Licensed to the Apache Software Foundation (ASF) under one 3 | @REM or more contributor license agreements. See the NOTICE file 4 | @REM distributed with this work for additional information 5 | @REM regarding copyright ownership. The ASF licenses this file 6 | @REM to you under the Apache License, Version 2.0 (the 7 | @REM "License"); you may not use this file except in compliance 8 | @REM with the License. You may obtain a copy of the License at 9 | @REM 10 | @REM https://www.apache.org/licenses/LICENSE-2.0 11 | @REM 12 | @REM Unless required by applicable law or agreed to in writing, 13 | @REM software distributed under the License is distributed on an 14 | @REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | @REM KIND, either express or implied. See the License for the 16 | @REM specific language governing permissions and limitations 17 | @REM under the License. 18 | @REM ---------------------------------------------------------------------------- 19 | 20 | @REM ---------------------------------------------------------------------------- 21 | @REM Maven Start Up Batch script 22 | @REM 23 | @REM Required ENV vars: 24 | @REM JAVA_HOME - location of a JDK home dir 25 | @REM 26 | @REM Optional ENV vars 27 | @REM M2_HOME - location of maven2's installed home dir 28 | @REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands 29 | @REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending 30 | @REM MAVEN_OPTS - parameters passed to the Java VM when running Maven 31 | @REM e.g. to debug Maven itself, use 32 | @REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 33 | @REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files 34 | @REM ---------------------------------------------------------------------------- 35 | 36 | @REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' 37 | @echo off 38 | @REM set title of command window 39 | title %0 40 | @REM enable echoing by setting MAVEN_BATCH_ECHO to 'on' 41 | @if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% 42 | 43 | @REM set %HOME% to equivalent of $HOME 44 | if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") 45 | 46 | @REM Execute a user defined script before this one 47 | if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre 48 | @REM check for pre script, once with legacy .bat ending and once with .cmd ending 49 | if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat" 50 | if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd" 51 | :skipRcPre 52 | 53 | @setlocal 54 | 55 | set ERROR_CODE=0 56 | 57 | @REM To isolate internal variables from possible post scripts, we use another setlocal 58 | @setlocal 59 | 60 | @REM ==== START VALIDATION ==== 61 | if not "%JAVA_HOME%" == "" goto OkJHome 62 | 63 | echo. 64 | echo Error: JAVA_HOME not found in your environment. >&2 65 | echo Please set the JAVA_HOME variable in your environment to match the >&2 66 | echo location of your Java installation. >&2 67 | echo. 68 | goto error 69 | 70 | :OkJHome 71 | if exist "%JAVA_HOME%\bin\java.exe" goto init 72 | 73 | echo. 74 | echo Error: JAVA_HOME is set to an invalid directory. >&2 75 | echo JAVA_HOME = "%JAVA_HOME%" >&2 76 | echo Please set the JAVA_HOME variable in your environment to match the >&2 77 | echo location of your Java installation. >&2 78 | echo. 79 | goto error 80 | 81 | @REM ==== END VALIDATION ==== 82 | 83 | :init 84 | 85 | @REM Find the project base dir, i.e. the directory that contains the folder ".mvn". 86 | @REM Fallback to current working directory if not found. 87 | 88 | set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% 89 | IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir 90 | 91 | set EXEC_DIR=%CD% 92 | set WDIR=%EXEC_DIR% 93 | :findBaseDir 94 | IF EXIST "%WDIR%"\.mvn goto baseDirFound 95 | cd .. 96 | IF "%WDIR%"=="%CD%" goto baseDirNotFound 97 | set WDIR=%CD% 98 | goto findBaseDir 99 | 100 | :baseDirFound 101 | set MAVEN_PROJECTBASEDIR=%WDIR% 102 | cd "%EXEC_DIR%" 103 | goto endDetectBaseDir 104 | 105 | :baseDirNotFound 106 | set MAVEN_PROJECTBASEDIR=%EXEC_DIR% 107 | cd "%EXEC_DIR%" 108 | 109 | :endDetectBaseDir 110 | 111 | IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig 112 | 113 | @setlocal EnableExtensions EnableDelayedExpansion 114 | for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a 115 | @endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% 116 | 117 | :endReadAdditionalConfig 118 | 119 | SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" 120 | set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" 121 | set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain 122 | 123 | set DOWNLOAD_URL="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" 124 | 125 | FOR /F "tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( 126 | IF "%%A"=="wrapperUrl" SET DOWNLOAD_URL=%%B 127 | ) 128 | 129 | @REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central 130 | @REM This allows using the maven wrapper in projects that prohibit checking in binary data. 131 | if exist %WRAPPER_JAR% ( 132 | if "%MVNW_VERBOSE%" == "true" ( 133 | echo Found %WRAPPER_JAR% 134 | ) 135 | ) else ( 136 | if not "%MVNW_REPOURL%" == "" ( 137 | SET DOWNLOAD_URL="%MVNW_REPOURL%/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" 138 | ) 139 | if "%MVNW_VERBOSE%" == "true" ( 140 | echo Couldn't find %WRAPPER_JAR%, downloading it ... 141 | echo Downloading from: %DOWNLOAD_URL% 142 | ) 143 | 144 | powershell -Command "&{"^ 145 | "$webclient = new-object System.Net.WebClient;"^ 146 | "if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^ 147 | "$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^ 148 | "}"^ 149 | "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%DOWNLOAD_URL%', '%WRAPPER_JAR%')"^ 150 | "}" 151 | if "%MVNW_VERBOSE%" == "true" ( 152 | echo Finished downloading %WRAPPER_JAR% 153 | ) 154 | ) 155 | @REM End of extension 156 | 157 | @REM Provide a "standardized" way to retrieve the CLI args that will 158 | @REM work with both Windows and non-Windows executions. 159 | set MAVEN_CMD_LINE_ARGS=%* 160 | 161 | %MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* 162 | if ERRORLEVEL 1 goto error 163 | goto end 164 | 165 | :error 166 | set ERROR_CODE=1 167 | 168 | :end 169 | @endlocal & set ERROR_CODE=%ERROR_CODE% 170 | 171 | if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost 172 | @REM check for post script, once with legacy .bat ending and once with .cmd ending 173 | if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat" 174 | if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd" 175 | :skipRcPost 176 | 177 | @REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' 178 | if "%MAVEN_BATCH_PAUSE%" == "on" pause 179 | 180 | if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE% 181 | 182 | exit /B %ERROR_CODE% 183 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | org.springframework.boot 8 | spring-boot-starter-parent 9 | 3.2.4 10 | 11 | 12 | com.example 13 | demo 14 | 0.0.1-SNAPSHOT 15 | demo 16 | Demo project for Spring Boot 17 | 18 | 17 19 | 1.3.5 20 | 21 | 22 | 23 | org.springframework.boot 24 | spring-boot-starter-web 25 | 26 | 27 | io.jstach 28 | jstachio-spring-boot-starter-webmvc 29 | ${jstachio.version} 30 | 31 | 32 | 33 | org.springframework.boot 34 | spring-boot-starter-test 35 | test 36 | 37 | 38 | 39 | org.springframework.boot 40 | spring-boot-devtools 41 | test 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | org.springframework.boot 50 | spring-boot-maven-plugin 51 | 52 | 53 | org.graalvm.buildtools 54 | native-maven-plugin 55 | 56 | 57 | maven-compiler-plugin 58 | 59 | 60 | 61 | io.jstach 62 | jstachio-apt 63 | ${jstachio.version} 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | snapshots-repo 75 | https://oss.sonatype.org/content/repositories/snapshots 76 | 77 | false 78 | 79 | 80 | true 81 | 82 | 83 | 84 | 85 | -------------------------------------------------------------------------------- /src/main/java/demo/DemoApplication.java: -------------------------------------------------------------------------------- 1 | package demo; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | 6 | import io.jstach.jstache.JStachePath; 7 | 8 | @SpringBootApplication 9 | @JStachePath(prefix = "templates/", suffix = ".mustache") 10 | public class DemoApplication { 11 | 12 | public static void main(String[] args) { 13 | SpringApplication.run(DemoApplication.class, args); 14 | } 15 | 16 | } -------------------------------------------------------------------------------- /src/main/java/demo/DemoController.java: -------------------------------------------------------------------------------- 1 | package demo; 2 | 3 | import org.springframework.beans.factory.annotation.Autowired; 4 | import org.springframework.beans.factory.annotation.Value; 5 | import org.springframework.stereotype.Controller; 6 | import org.springframework.web.bind.annotation.GetMapping; 7 | import org.springframework.web.bind.annotation.ResponseBody; 8 | import org.springframework.web.servlet.View; 9 | 10 | import io.jstach.opt.spring.webmvc.JStachioModelView; 11 | 12 | @Controller 13 | public class DemoController { 14 | 15 | @Autowired 16 | VisitsRepository visitsRepository; 17 | 18 | @Value("${spring.profiles.active:PROFILE_NOT_SET}") 19 | private String profile; 20 | 21 | @ResponseBody 22 | @GetMapping(value = "/profile") 23 | public String profile() { 24 | return profile; 25 | } 26 | 27 | @GetMapping("/") 28 | public View view() { 29 | visitsRepository.add(); 30 | return JStachioModelView.of(new DemoModel("mystérieux visiteur", visitsRepository.get())); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/main/java/demo/DemoModel.java: -------------------------------------------------------------------------------- 1 | package demo; 2 | 3 | import io.jstach.jstache.JStache; 4 | 5 | @JStache(path = "index") 6 | public class DemoModel { 7 | public String name; 8 | public long visits; 9 | 10 | public DemoModel(String name, long visits) { 11 | this.name = name; 12 | this.visits = visits; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/demo/VisitsRepository.java: -------------------------------------------------------------------------------- 1 | package demo; 2 | 3 | import org.springframework.stereotype.Service; 4 | 5 | import java.util.concurrent.atomic.LongAdder; 6 | 7 | @Service 8 | public class VisitsRepository { 9 | private final LongAdder visits = new LongAdder(); 10 | 11 | public void add() { 12 | visits.increment(); 13 | } 14 | 15 | public long get() { 16 | return visits.longValue(); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/main/java/demo/package-info.java: -------------------------------------------------------------------------------- 1 | @JStacheConfig(using = DemoApplication.class) 2 | package demo; 3 | 4 | import io.jstach.jstache.JStacheConfig; 5 | -------------------------------------------------------------------------------- /src/main/resources/application.properties: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dsyer/java-template-demo/daad8e23cc1147a2a5d1d51331f0edd03292d6b4/src/main/resources/application.properties -------------------------------------------------------------------------------- /src/main/resources/templates/index.mustache: -------------------------------------------------------------------------------- 1 | {{ 4 |
5 | You are visitor number {{visits}}. 6 | {{/body}} 7 | {{/layout}} -------------------------------------------------------------------------------- /src/main/resources/templates/layout.mustache: -------------------------------------------------------------------------------- 1 | 2 | 3 | {{$body}}{{/body}} 4 | 5 | -------------------------------------------------------------------------------- /src/test/java/demo/DemoApplicationTests.java: -------------------------------------------------------------------------------- 1 | package demo; 2 | 3 | import static org.assertj.core.api.Assertions.assertThat; 4 | 5 | import org.junit.jupiter.api.Test; 6 | import org.springframework.beans.factory.annotation.Autowired; 7 | import org.springframework.boot.SpringApplication; 8 | import org.springframework.boot.test.context.SpringBootTest; 9 | import org.springframework.boot.test.web.client.TestRestTemplate; 10 | import org.springframework.http.HttpStatus; 11 | import org.springframework.http.ResponseEntity; 12 | 13 | @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) 14 | public class DemoApplicationTests { 15 | @Autowired 16 | TestRestTemplate restTemplate; 17 | 18 | @Test 19 | void contextLoads() { 20 | ResponseEntity value = restTemplate.getForEntity("/", String.class); 21 | assertThat(value.getStatusCode()).isEqualTo(HttpStatus.OK); 22 | assertThat(value.getBody()).contains("mystérieux visiteur"); 23 | assertThat(value.getHeaders().getContentType().toString()).isEqualTo("text/html;charset=UTF-8"); 24 | assertThat(value.getHeaders().getContentLength()).isNotNull(); 25 | } 26 | 27 | public static void main(String[] args) { 28 | SpringApplication.run(DemoApplication.class, args); 29 | } 30 | 31 | } 32 | --------------------------------------------------------------------------------