Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 29 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,9 @@ jobs:
build:
strategy:
matrix:
os: [windows-latest, ubuntu-latest]
os: [windows-latest, ubuntu-latest, ubuntu-24.04-arm]
runs-on: ${{ matrix.os }}
concurrency: main_tests_${{ github.ref }}
concurrency: main_tests_${{ github.ref }}_${{ matrix.os }}
steps:
- name: Welcome Message
run: 'echo "Started with parameters: ${{ matrix.os }} because ${{ github.event_name }} on ${{ github.ref }}"'
Expand All @@ -58,6 +58,33 @@ jobs:
- name: Run Build
id: build_step
run: './gradlew "-Pfreemarker.signMethod=none" "-Pfreemarker.allowUnsignedReleaseBuild=true" --continue clean build'
- name: Set up GraalVM 21
uses: actions/setup-java@v4
with:
java-version: 21
distribution: graalvm
# test pipeline to check native support
#
# - GraalVM is added to the runner
# - A simple native project is build and run
#
# At the something like that should be found in the log :
#
# INFO: name : FreeMarker Native Demo, version : 2.3.35-nightly
# Jan 15, 2025 4:28:19 PM freemarker.log._JULLoggerFactory$JULLogger info
# INFO: result :
# <html>
# <head>
# <title>Hello : FreeMarker GraalVM Native Demo</title>
# </head>
# <body>
# <h1>Hello : FreeMarker GraalVM Native Demo</h1>
# <p>Test template for Apache FreeMarker GraalVM native support (2.3.35-nightly)</p>
# </body>
# </html>
- name: Test GraalVM native support (build and run)
id: native_test
run: './gradlew :freemarker-test-graalvm-native:nativeCompile;./freemarker-test-graalvm-native/build/native/nativeCompile/freemarker-test-graalvm-native'
- name: Upload Failed Report
uses: actions/upload-artifact@v4
if: failure() && steps.build_step.outcome == 'failure'
Expand Down
139 changes: 139 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ Apache FreeMarker™ {version}
============================

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

For the latest version or to report bugs visit:
https://freemarker.apache.org/
Expand Down Expand Up @@ -252,3 +253,141 @@ Gradle project. After that, it's recommended to set these preferences (based on
- Project -> Properties -> FindBugs -> [x] Run Automatically
- There should 0 errors. But sometimes the plugin fails to take the
@SuppressFBWarnings annotations into account; then use Project -> Clean.

### GraalVM Native Support

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.

Refer to the [GraalVM documentation](https://www.graalvm.org/latest/docs/) for more details, especially:

- [Reflection in Native Image](https://www.graalvm.org/jdk21/reference-manual/native-image/dynamic-features/Reflection/)
- [Accessing Resources in Native Image](https://www.graalvm.org/jdk21/reference-manual/native-image/dynamic-features/Resources/)

**TIP:** You can find many configuration samples in the [graalvm-reachability-metadata](https://github.com/oracle/graalvm-reachability-metadata) repository.

Here is a sample usage guide for ApacheFreeMarker + GraalVM.

To run the sample in classic Just In Time Way, we only need :

* FreeMarkerGraalVMSample.java
* sample.ftl

But for the Ahead Of Time application with GraalVM some additional configuration is required :

* custom-reflect-config.json

#### FreeMarkerGraalVMSample.java sample class

```java
import freemarker.log.Logger;
import freemarker.template.Configuration;
import freemarker.template.Template;
import freemarker.template.TemplateException;

import java.io.IOException;
import java.io.StringWriter;
import java.io.Writer;
import java.util.HashMap;
import java.util.Map;

public class FreeMarkerGraalVMSample {

private final static Logger LOG = Logger.getLogger(FreeMarkerGraalVMSample.class.getName());

/* data model */
public class Data {
private String description;
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
}

private void handleTemplate(Writer writer, String templatePath, Map<String, Object> dataModel) throws IOException, TemplateException {
Configuration cfg = new Configuration( Configuration.VERSION_2_3_34 );
cfg.setClassForTemplateLoading( FreeMarkerGraalVMSample.class, "/templates" );
Template template = cfg.getTemplate( templatePath );
template.process( dataModel, writer );
}

public void runSample() {
try ( StringWriter writer = new StringWriter() ) {
Map<String, Object> dataModel = new HashMap<>();
Data data = new Data();
data.setDescription( "FreeMarkerGraalVMSample" );
dataModel.put("data", data);
handleTemplate( writer, "sample.ftl", dataModel );
LOG.info( writer.toString() );
} catch (Exception e) {
LOG.error( e.getMessage(), e );
}
}

public static void main(String[] args) {
FreeMarkerGraalVMSample sample = new FreeMarkerGraalVMSample();
sample.runSample();
}

}
```

#### Apache FreeMarker template

```ftl
<freemarker-graalvm-sample>
<freemarker-version>${.version}</freemarker-version>
<description>${data.description}</description>
</freemarker-graalvm-sample>
```

#### Reflection configuration, custom-reflect-config.json

Refers to [Reflection in Native Image](https://www.graalvm.org/jdk21/reference-manual/native-image/dynamic-features/Reflection/) guide

```json
[{
"name" : "FreeMarkerGraalVMSample$Data",
"methods" : [ {
"name" : "<init>",
"parameterTypes" : [ ]
},{
"name" : "getDescription",
"parameterTypes" : [ ]
} ]
}]
```

#### Build the native image

```shell
#!/bin/bash

# setting up environment
export BASEDIR=.
export CP=./lib/freemarker-gae-2.3.35-SNAPSHOT.jar:.

# just in time application build
javac -cp ${CP} -d build ./src/FreeMarkerGraalVMSample.java

# ahead of time application build
#
# -H:IncludeResources=^templates/.*
# will make the templates available to the native-image
#
# -H:ReflectionConfigurationFiles=./config/custom-reflect-config.json
# will setup reflection custom configuration
native-image \
-cp "${CP}:build" \
-H:Path=build \
-H:Class=FreeMarkerGraalVMSample \
-H:IncludeResources=^templates/.* \
-H:+UnlockExperimentalVMOptions \
-H:ReflectionConfigurationFiles=./config/custom-reflect-config.json \
--no-fallback \
--report-unsupported-elements-at-runtime

# running the application
./build/freemarkergraalvmsample
```
28 changes: 23 additions & 5 deletions freemarker-core/src/main/java/freemarker/log/Logger.java
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,11 @@
* {@code log4j-over-slf4j} properly installed (means, you have no real Log4j in your class path, and SLF4J has a
* backing implementation like {@code logback-classic}), then FreeMarker will use SLF4J directly instead of Log4j (since
* FreeMarker 2.3.22).
*
*
* NOTE: When running on GraalVM native image (system property 'org.graalvm.nativeimage.imagecode' set),
* FreeMarker 2.4 behaviour will be anticipated (SLF4J / Apache Commons are auto-detected).
* Additionally, log4j-over-slf4j lookup is skipped.
*
* <p>
* If the auto detection sequence describet above doesn't give you the result that you want, see
* {@link #SYSTEM_PROPERTY_NAME_LOGGER_LIBRARY}.
Expand Down Expand Up @@ -157,6 +161,9 @@ public abstract class Logger {
private static final String REAL_LOG4J_PRESENCE_CLASS = "org.apache.log4j.FileAppender";
private static final String LOG4J_OVER_SLF4J_TESTER_CLASS = "freemarker.log._Log4jOverSLF4JTester";

// 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
private static final boolean IS_GRAALVM_NATIVE = System.getProperty( "org.graalvm.nativeimage.imagecode" ) != null;

/**
* Order matters! Starts with the lowest priority.
*/
Expand Down Expand Up @@ -193,12 +200,22 @@ private static String getLibraryName(int libraryEnum) {
return LIBRARIES_BY_PRIORITY[(libraryEnum - 1) * 2 + 1];
}

private static boolean isAutoDetected(int libraryEnum) {
// 2.4: Remove libraryEnum == LIBRARY_SLF4J || libraryEnum == LIBRARY_COMMONS
// legacy auto-detection (until FreeMarker 2.3.X)
private static boolean isAutoDetectedLegacy( int libraryEnum ) {
return !(libraryEnum == LIBRARY_AUTO || libraryEnum == LIBRARY_NONE
|| libraryEnum == LIBRARY_SLF4J || libraryEnum == LIBRARY_COMMONS);
}

// next generation auto-detection (FreeMarker 2.4.X and on)
private static boolean isAutoDetectedNG( int libraryEnum ) {
return !(libraryEnum == LIBRARY_AUTO || libraryEnum == LIBRARY_NONE);
}

private static boolean isAutoDetected(int libraryEnum) {
// 2.4: Remove libraryEnum == LIBRARY_SLF4J || libraryEnum == LIBRARY_COMMONS (use isAutoDetectedNG())
return IS_GRAALVM_NATIVE ? isAutoDetectedNG(libraryEnum) : isAutoDetectedLegacy(libraryEnum);
}

private static int libraryEnum;
private static LoggerFactory loggerFactory;
private static boolean initializedFromSystemProperty;
Expand Down Expand Up @@ -428,7 +445,8 @@ private static LoggerFactory createLoggerFactory(int libraryEnum) throws ClassNo
if (libraryEnum == LIBRARY_AUTO) {
for (int libraryEnumToTry = MAX_LIBRARY_ENUM; libraryEnumToTry >= MIN_LIBRARY_ENUM; libraryEnumToTry--) {
if (!isAutoDetected(libraryEnumToTry)) continue;
if (libraryEnumToTry == LIBRARY_LOG4J && hasLog4LibraryThatDelegatesToWorkingSLF4J()) {
// skip hasLog4LibraryThatDelegatesToWorkingSLF4J when running in GraalVM native image
if (!IS_GRAALVM_NATIVE && libraryEnumToTry == LIBRARY_LOG4J && hasLog4LibraryThatDelegatesToWorkingSLF4J()) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But that still prefers Log4J API instead of SLF4J API when we have log4j-over-slf4j, under GrallVM Native only, and it should prefer SLF4J in that case. I know, this Logger class is a mess. Anyway, I looked into more, and the easiest way to achieve what I said is just changing isAutoDetected so that if IS_GRAALVM_NATIVE is true, then we return true for LIBRARY_SLF4J. Actually, even better long term, let's return true for LIBRARY_COMMONS too, and that way for GraalVM Native we just brought ahead what's planned for 2.4 anyway.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm having a deeper look at it.

Copy link
Author

@fugerit79 fugerit79 Jan 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ddekany let's see if this is ok for you.

I preferred to create separate methods so maybe it's easier to understand than complex conditional statements, do you agree?

    // legacy auto-detection (until FreeMarker 2.3.X)
    private static boolean isAutoDetectedLegacy( int libraryEnum ) {
        return !(libraryEnum == LIBRARY_AUTO || libraryEnum == LIBRARY_NONE
                || libraryEnum == LIBRARY_SLF4J || libraryEnum == LIBRARY_COMMONS);
    }

    // next generation auto-detection (FreeMarker 2.4.X and on)
    private static boolean isAutoDetectedNG( int libraryEnum ) {
        return !(libraryEnum == LIBRARY_AUTO || libraryEnum == LIBRARY_NONE);
    }

    private static boolean isAutoDetected(int libraryEnum) {
        // 2.4: Remove libraryEnum == LIBRARY_SLF4J || libraryEnum == LIBRARY_COMMONS (use only isAutoDetectedNG()=
        return IS_GRAALVM_NATIVE ? isAutoDetectedNG(libraryEnum) : isAutoDetectedLegacy(libraryEnum);
    }

At beginning didn't fully grasped the meaning of all logger detection logic. I hope that now it is ok.

libraryEnumToTry = LIBRARY_SLF4J;
}

Expand All @@ -443,7 +461,7 @@ private static LoggerFactory createLoggerFactory(int libraryEnum) throws ClassNo
e);
}
}
logWarnInLogger("Auto detecton couldn't set up any logger libraries; FreeMarker logging suppressed.");
logWarnInLogger("Auto detection couldn't set up any logger libraries; FreeMarker logging suppressed.");
return new _NullLoggerFactory();
} else {
return createLoggerFactoryForNonAuto(libraryEnum);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.

Args = --initialize-at-run-time=freemarker.ext.jython.JythonWrapper,\
--initialize-at-run-time=freemarker.ext.jython.JythonModel
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
[ {
"condition" : {
"typeReachable" : "freemarker.template.Configuration"
},
"name" : "freemarker.log._SLF4JLoggerFactory",
"methods" : [ {
"name" : "<init>",
"parameterTypes" : [ ]
} ]
}, {
"condition" : {
"typeReachable" : "freemarker.template.Configuration"
},
"name" : "freemarker.log.SLF4JLoggerFactory",
"methods" : [ {
"name" : "<init>",
"parameterTypes" : [ ]
} ]
}, {
"condition" : {
"typeReachable" : "freemarker.template.Configuration"
},
"name" : "freemarker.log._AvalonLoggerFactory.java",
"methods" : [ {
"name" : "<init>",
"parameterTypes" : [ ]
} ]
}, {
"condition" : {
"typeReachable" : "freemarker.template.Configuration"
},
"name" : "freemarker.log._CommonsLoggingLoggerFactory.java",
"methods" : [ {
"name" : "<init>",
"parameterTypes" : [ ]
} ]
}, {
"condition" : {
"typeReachable" : "freemarker.template.Configuration"
},
"name" : "freemarker.log._JULLoggerFactory.java",
"methods" : [ {
"name" : "<init>",
"parameterTypes" : [ ]
} ]
}, {
"condition" : {
"typeReachable" : "freemarker.template.Configuration"
},
"name" : "freemarker.log._Log4jLoggerFactory.java",
"methods" : [ {
"name" : "<init>",
"parameterTypes" : [ ]
} ]
}, {
"condition" : {
"typeReachable" : "freemarker.template.Configuration"
},
"name" : "freemarker.log._Log4jOverSLF4JTester.java",
"methods" : [ {
"name" : "<init>",
"parameterTypes" : [ ]
} ]
}, {
"condition" : {
"typeReachable" : "freemarker.template.Configuration"
},
"name" : "freemarker.log._NullLoggerFactory.java",
"methods" : [ {
"name" : "<init>",
"parameterTypes" : [ ]
} ]
}, {
"condition" : {
"typeReachable" : "freemarker.template.Configuration"
},
"name" : "freemarker.log.CommonsLoggingLoggerFactory.java",
"methods" : [ {
"name" : "<init>",
"parameterTypes" : [ ]
} ]
} ]
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
{
"bundles": [],
"resources": {
"includes": [
{
"pattern": "\\Qfreemarker/ext/beans/DefaultMemberAccessPolicy-rules\\E",
"condition": {
"typeReachable": "freemarker.ext.beans.DefaultMemberAccessPolicy"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't we also need to add freemarker/ext/beans/unsafeMethods.properties accessed from freemarker.ext.beans.LegacyDefaultMemberAccessPolicy here?

Copy link
Author

@fugerit79 fugerit79 Jan 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe 'freemarker/ext/beans/unsafeMethods.properties' when freemarker.ext.beans.LegacyDefaultMemberAccessPolicy is reachable? (just commited it this way)

do you think is it better to squash commits?

}
},
{
"pattern": "\\Qfreemarker/ext/beans/unsafeMethods.properties\\E",
"condition": {
"typeReachable": "freemarker.ext.beans.LegacyDefaultMemberAccessPolicy"
}
},
{
"pattern": "\\Qfreemarker/version.properties\\E",
"condition": {
"typeReachable": "freemarker.template.utility.ClassUtil"
}
}
]
}
}
Loading