Skip to content

Commit 37cc917

Browse files
authored
Merge pull request #121 from fugerit-org/1-add-graalvm-support-to-apache-freemarker
Added GraalVM native support to Apache FreeMarker
2 parents 54667e0 + 4ad7311 commit 37cc917

File tree

16 files changed

+665
-7
lines changed

16 files changed

+665
-7
lines changed

.github/workflows/ci.yml

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,9 +31,9 @@ jobs:
3131
build:
3232
strategy:
3333
matrix:
34-
os: [windows-latest, ubuntu-latest]
34+
os: [windows-latest, ubuntu-latest, ubuntu-24.04-arm]
3535
runs-on: ${{ matrix.os }}
36-
concurrency: main_tests_${{ github.ref }}
36+
concurrency: main_tests_${{ github.ref }}_${{ matrix.os }}
3737
steps:
3838
- name: Welcome Message
3939
run: 'echo "Started with parameters: ${{ matrix.os }} because ${{ github.event_name }} on ${{ github.ref }}"'
@@ -58,6 +58,33 @@ jobs:
5858
- name: Run Build
5959
id: build_step
6060
run: './gradlew "-Pfreemarker.signMethod=none" "-Pfreemarker.allowUnsignedReleaseBuild=true" --continue clean build'
61+
- name: Set up GraalVM 21
62+
uses: actions/setup-java@v4
63+
with:
64+
java-version: 21
65+
distribution: graalvm
66+
# test pipeline to check native support
67+
#
68+
# - GraalVM is added to the runner
69+
# - A simple native project is build and run
70+
#
71+
# At the something like that should be found in the log :
72+
#
73+
# INFO: name : FreeMarker Native Demo, version : 2.3.35-nightly
74+
# Jan 15, 2025 4:28:19 PM freemarker.log._JULLoggerFactory$JULLogger info
75+
# INFO: result :
76+
# <html>
77+
# <head>
78+
# <title>Hello : FreeMarker GraalVM Native Demo</title>
79+
# </head>
80+
# <body>
81+
# <h1>Hello : FreeMarker GraalVM Native Demo</h1>
82+
# <p>Test template for Apache FreeMarker GraalVM native support (2.3.35-nightly)</p>
83+
# </body>
84+
# </html>
85+
- name: Test GraalVM native support (build and run)
86+
id: native_test
87+
run: './gradlew :freemarker-test-graalvm-native:nativeCompile;./freemarker-test-graalvm-native/build/native/nativeCompile/freemarker-test-graalvm-native'
6188
- name: Upload Failed Report
6289
uses: actions/upload-artifact@v4
6390
if: failure() && steps.build_step.outcome == 'failure'

README.md

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ Apache FreeMarker™ {version}
22
============================
33

44
[![Build status](https://github.com/apache/freemarker/actions/workflows/ci.yml/badge.svg)](https://github.com/apache/freemarker/actions/workflows/ci.yml)
5+
![GraalVM Ready](https://img.shields.io/badge/GraalVM-Ready-orange)
56

67
For the latest version or to report bugs visit:
78
https://freemarker.apache.org/
@@ -252,3 +253,141 @@ Gradle project. After that, it's recommended to set these preferences (based on
252253
- Project -> Properties -> FindBugs -> [x] Run Automatically
253254
- There should 0 errors. But sometimes the plugin fails to take the
254255
@SuppressFBWarnings annotations into account; then use Project -> Clean.
256+
257+
### GraalVM Native Support
258+
259+
Apache FreeMarker is compatible with Ahead-of-Time (AOT) compilation using GraalVM as of version 2.3.35. However, any custom Java objects or resources used in the data model must still be registered for reflection.
260+
261+
Refer to the [GraalVM documentation](https://www.graalvm.org/latest/docs/) for more details, especially:
262+
263+
- [Reflection in Native Image](https://www.graalvm.org/jdk21/reference-manual/native-image/dynamic-features/Reflection/)
264+
- [Accessing Resources in Native Image](https://www.graalvm.org/jdk21/reference-manual/native-image/dynamic-features/Resources/)
265+
266+
**TIP:** You can find many configuration samples in the [graalvm-reachability-metadata](https://github.com/oracle/graalvm-reachability-metadata) repository.
267+
268+
Here is a sample usage guide for ApacheFreeMarker + GraalVM.
269+
270+
To run the sample in classic Just In Time Way, we only need :
271+
272+
* FreeMarkerGraalVMSample.java
273+
* sample.ftl
274+
275+
But for the Ahead Of Time application with GraalVM some additional configuration is required :
276+
277+
* custom-reflect-config.json
278+
279+
#### FreeMarkerGraalVMSample.java sample class
280+
281+
```java
282+
import freemarker.log.Logger;
283+
import freemarker.template.Configuration;
284+
import freemarker.template.Template;
285+
import freemarker.template.TemplateException;
286+
287+
import java.io.IOException;
288+
import java.io.StringWriter;
289+
import java.io.Writer;
290+
import java.util.HashMap;
291+
import java.util.Map;
292+
293+
public class FreeMarkerGraalVMSample {
294+
295+
private final static Logger LOG = Logger.getLogger(FreeMarkerGraalVMSample.class.getName());
296+
297+
/* data model */
298+
public class Data {
299+
private String description;
300+
public String getDescription() {
301+
return description;
302+
}
303+
public void setDescription(String description) {
304+
this.description = description;
305+
}
306+
}
307+
308+
private void handleTemplate(Writer writer, String templatePath, Map<String, Object> dataModel) throws IOException, TemplateException {
309+
Configuration cfg = new Configuration( Configuration.VERSION_2_3_34 );
310+
cfg.setClassForTemplateLoading( FreeMarkerGraalVMSample.class, "/templates" );
311+
Template template = cfg.getTemplate( templatePath );
312+
template.process( dataModel, writer );
313+
}
314+
315+
public void runSample() {
316+
try ( StringWriter writer = new StringWriter() ) {
317+
Map<String, Object> dataModel = new HashMap<>();
318+
Data data = new Data();
319+
data.setDescription( "FreeMarkerGraalVMSample" );
320+
dataModel.put("data", data);
321+
handleTemplate( writer, "sample.ftl", dataModel );
322+
LOG.info( writer.toString() );
323+
} catch (Exception e) {
324+
LOG.error( e.getMessage(), e );
325+
}
326+
}
327+
328+
public static void main(String[] args) {
329+
FreeMarkerGraalVMSample sample = new FreeMarkerGraalVMSample();
330+
sample.runSample();
331+
}
332+
333+
}
334+
```
335+
336+
#### Apache FreeMarker template
337+
338+
```ftl
339+
<freemarker-graalvm-sample>
340+
<freemarker-version>${.version}</freemarker-version>
341+
<description>${data.description}</description>
342+
</freemarker-graalvm-sample>
343+
```
344+
345+
#### Reflection configuration, custom-reflect-config.json
346+
347+
Refers to [Reflection in Native Image](https://www.graalvm.org/jdk21/reference-manual/native-image/dynamic-features/Reflection/) guide
348+
349+
```json
350+
[{
351+
"name" : "FreeMarkerGraalVMSample$Data",
352+
"methods" : [ {
353+
"name" : "<init>",
354+
"parameterTypes" : [ ]
355+
},{
356+
"name" : "getDescription",
357+
"parameterTypes" : [ ]
358+
} ]
359+
}]
360+
```
361+
362+
#### Build the native image
363+
364+
```shell
365+
#!/bin/bash
366+
367+
# setting up environment
368+
export BASEDIR=.
369+
export CP=./lib/freemarker-gae-2.3.35-SNAPSHOT.jar:.
370+
371+
# just in time application build
372+
javac -cp ${CP} -d build ./src/FreeMarkerGraalVMSample.java
373+
374+
# ahead of time application build
375+
#
376+
# -H:IncludeResources=^templates/.*
377+
# will make the templates available to the native-image
378+
#
379+
# -H:ReflectionConfigurationFiles=./config/custom-reflect-config.json
380+
# will setup reflection custom configuration
381+
native-image \
382+
-cp "${CP}:build" \
383+
-H:Path=build \
384+
-H:Class=FreeMarkerGraalVMSample \
385+
-H:IncludeResources=^templates/.* \
386+
-H:+UnlockExperimentalVMOptions \
387+
-H:ReflectionConfigurationFiles=./config/custom-reflect-config.json \
388+
--no-fallback \
389+
--report-unsupported-elements-at-runtime
390+
391+
# running the application
392+
./build/freemarkergraalvmsample
393+
```

freemarker-core/src/main/java/freemarker/log/Logger.java

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,11 @@
3232
* {@code log4j-over-slf4j} properly installed (means, you have no real Log4j in your class path, and SLF4J has a
3333
* backing implementation like {@code logback-classic}), then FreeMarker will use SLF4J directly instead of Log4j (since
3434
* FreeMarker 2.3.22).
35-
*
35+
*
36+
* NOTE: When running on GraalVM native image (system property 'org.graalvm.nativeimage.imagecode' set),
37+
* FreeMarker 2.4 behaviour will be anticipated (SLF4J / Apache Commons are auto-detected).
38+
* Additionally, log4j-over-slf4j lookup is skipped.
39+
*
3640
* <p>
3741
* If the auto detection sequence describet above doesn't give you the result that you want, see
3842
* {@link #SYSTEM_PROPERTY_NAME_LOGGER_LIBRARY}.
@@ -157,6 +161,9 @@ public abstract class Logger {
157161
private static final String REAL_LOG4J_PRESENCE_CLASS = "org.apache.log4j.FileAppender";
158162
private static final String LOG4J_OVER_SLF4J_TESTER_CLASS = "freemarker.log._Log4jOverSLF4JTester";
159163

164+
// it is true if running in a GraalVM native build (issue #229) - see https://www.graalvm.org/sdk/javadoc/org/graalvm/nativeimage/ImageInfo.html#PROPERTY_IMAGE_CODE_KEY
165+
private static final boolean IS_GRAALVM_NATIVE = System.getProperty( "org.graalvm.nativeimage.imagecode" ) != null;
166+
160167
/**
161168
* Order matters! Starts with the lowest priority.
162169
*/
@@ -193,12 +200,22 @@ private static String getLibraryName(int libraryEnum) {
193200
return LIBRARIES_BY_PRIORITY[(libraryEnum - 1) * 2 + 1];
194201
}
195202

196-
private static boolean isAutoDetected(int libraryEnum) {
197-
// 2.4: Remove libraryEnum == LIBRARY_SLF4J || libraryEnum == LIBRARY_COMMONS
203+
// legacy auto-detection (until FreeMarker 2.3.X)
204+
private static boolean isAutoDetectedLegacy( int libraryEnum ) {
198205
return !(libraryEnum == LIBRARY_AUTO || libraryEnum == LIBRARY_NONE
199206
|| libraryEnum == LIBRARY_SLF4J || libraryEnum == LIBRARY_COMMONS);
200207
}
201208

209+
// next generation auto-detection (FreeMarker 2.4.X and on)
210+
private static boolean isAutoDetectedNG( int libraryEnum ) {
211+
return !(libraryEnum == LIBRARY_AUTO || libraryEnum == LIBRARY_NONE);
212+
}
213+
214+
private static boolean isAutoDetected(int libraryEnum) {
215+
// 2.4: Remove libraryEnum == LIBRARY_SLF4J || libraryEnum == LIBRARY_COMMONS (use isAutoDetectedNG())
216+
return IS_GRAALVM_NATIVE ? isAutoDetectedNG(libraryEnum) : isAutoDetectedLegacy(libraryEnum);
217+
}
218+
202219
private static int libraryEnum;
203220
private static LoggerFactory loggerFactory;
204221
private static boolean initializedFromSystemProperty;
@@ -428,7 +445,8 @@ private static LoggerFactory createLoggerFactory(int libraryEnum) throws ClassNo
428445
if (libraryEnum == LIBRARY_AUTO) {
429446
for (int libraryEnumToTry = MAX_LIBRARY_ENUM; libraryEnumToTry >= MIN_LIBRARY_ENUM; libraryEnumToTry--) {
430447
if (!isAutoDetected(libraryEnumToTry)) continue;
431-
if (libraryEnumToTry == LIBRARY_LOG4J && hasLog4LibraryThatDelegatesToWorkingSLF4J()) {
448+
// skip hasLog4LibraryThatDelegatesToWorkingSLF4J when running in GraalVM native image
449+
if (!IS_GRAALVM_NATIVE && libraryEnumToTry == LIBRARY_LOG4J && hasLog4LibraryThatDelegatesToWorkingSLF4J()) {
432450
libraryEnumToTry = LIBRARY_SLF4J;
433451
}
434452

@@ -443,7 +461,7 @@ private static LoggerFactory createLoggerFactory(int libraryEnum) throws ClassNo
443461
e);
444462
}
445463
}
446-
logWarnInLogger("Auto detecton couldn't set up any logger libraries; FreeMarker logging suppressed.");
464+
logWarnInLogger("Auto detection couldn't set up any logger libraries; FreeMarker logging suppressed.");
447465
return new _NullLoggerFactory();
448466
} else {
449467
return createLoggerFactoryForNonAuto(libraryEnum);
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# Licensed to the Apache Software Foundation (ASF) under one
2+
# or more contributor license agreements. See the NOTICE file
3+
# distributed with this work for additional information
4+
# regarding copyright ownership. The ASF licenses this file
5+
# to you under the Apache License, Version 2.0 (the
6+
# "License"); you may not use this file except in compliance
7+
# with the License. You may obtain a copy of the License at
8+
#
9+
# http://www.apache.org/licenses/LICENSE-2.0
10+
#
11+
# Unless required by applicable law or agreed to in writing,
12+
# software distributed under the License is distributed on an
13+
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14+
# KIND, either express or implied. See the License for the
15+
# specific language governing permissions and limitations
16+
# under the License.
17+
18+
Args = --initialize-at-run-time=freemarker.ext.jython.JythonWrapper,\
19+
--initialize-at-run-time=freemarker.ext.jython.JythonModel
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
[ {
2+
"condition" : {
3+
"typeReachable" : "freemarker.template.Configuration"
4+
},
5+
"name" : "freemarker.log._SLF4JLoggerFactory",
6+
"methods" : [ {
7+
"name" : "<init>",
8+
"parameterTypes" : [ ]
9+
} ]
10+
}, {
11+
"condition" : {
12+
"typeReachable" : "freemarker.template.Configuration"
13+
},
14+
"name" : "freemarker.log.SLF4JLoggerFactory",
15+
"methods" : [ {
16+
"name" : "<init>",
17+
"parameterTypes" : [ ]
18+
} ]
19+
}, {
20+
"condition" : {
21+
"typeReachable" : "freemarker.template.Configuration"
22+
},
23+
"name" : "freemarker.log._AvalonLoggerFactory.java",
24+
"methods" : [ {
25+
"name" : "<init>",
26+
"parameterTypes" : [ ]
27+
} ]
28+
}, {
29+
"condition" : {
30+
"typeReachable" : "freemarker.template.Configuration"
31+
},
32+
"name" : "freemarker.log._CommonsLoggingLoggerFactory.java",
33+
"methods" : [ {
34+
"name" : "<init>",
35+
"parameterTypes" : [ ]
36+
} ]
37+
}, {
38+
"condition" : {
39+
"typeReachable" : "freemarker.template.Configuration"
40+
},
41+
"name" : "freemarker.log._JULLoggerFactory.java",
42+
"methods" : [ {
43+
"name" : "<init>",
44+
"parameterTypes" : [ ]
45+
} ]
46+
}, {
47+
"condition" : {
48+
"typeReachable" : "freemarker.template.Configuration"
49+
},
50+
"name" : "freemarker.log._Log4jLoggerFactory.java",
51+
"methods" : [ {
52+
"name" : "<init>",
53+
"parameterTypes" : [ ]
54+
} ]
55+
}, {
56+
"condition" : {
57+
"typeReachable" : "freemarker.template.Configuration"
58+
},
59+
"name" : "freemarker.log._Log4jOverSLF4JTester.java",
60+
"methods" : [ {
61+
"name" : "<init>",
62+
"parameterTypes" : [ ]
63+
} ]
64+
}, {
65+
"condition" : {
66+
"typeReachable" : "freemarker.template.Configuration"
67+
},
68+
"name" : "freemarker.log._NullLoggerFactory.java",
69+
"methods" : [ {
70+
"name" : "<init>",
71+
"parameterTypes" : [ ]
72+
} ]
73+
}, {
74+
"condition" : {
75+
"typeReachable" : "freemarker.template.Configuration"
76+
},
77+
"name" : "freemarker.log.CommonsLoggingLoggerFactory.java",
78+
"methods" : [ {
79+
"name" : "<init>",
80+
"parameterTypes" : [ ]
81+
} ]
82+
} ]
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
{
2+
"bundles": [],
3+
"resources": {
4+
"includes": [
5+
{
6+
"pattern": "\\Qfreemarker/ext/beans/DefaultMemberAccessPolicy-rules\\E",
7+
"condition": {
8+
"typeReachable": "freemarker.ext.beans.DefaultMemberAccessPolicy"
9+
}
10+
},
11+
{
12+
"pattern": "\\Qfreemarker/ext/beans/unsafeMethods.properties\\E",
13+
"condition": {
14+
"typeReachable": "freemarker.ext.beans.LegacyDefaultMemberAccessPolicy"
15+
}
16+
},
17+
{
18+
"pattern": "\\Qfreemarker/version.properties\\E",
19+
"condition": {
20+
"typeReachable": "freemarker.template.utility.ClassUtil"
21+
}
22+
}
23+
]
24+
}
25+
}

0 commit comments

Comments
 (0)