_____ _____ ___ ___ _
/ // / | \ ___| \| | ___ _ _
/ // / | ' // -_) '_/| |/ _ | \| |
/ // / |_|_\\___|_| |_|\___|\_ /
/____//____/ /_/
RePlay Framework, https://github.com/replay-framework/replay
RePlay is a fork of the Play1 framework, created by Codeborne in 2017. Forking was needed to make some breaking changes (detailed below) that would not be acceptable on Play1. RePlay is a simplification of the Play1 codebase that aims to improve developer ergonomics. The main differences between Play1 and RePlay are outlined below.
RePlay originally forked Play v1.5.0. Any Play1 improvements that were made since then are ported to RePlay when applicable. Version 2 of the Play Framework (Play2) is significantly different from Play1. It caters for Scala web application projects, and uses Scala internally. Porting a Play1 application to Play2 is really hard and has questionable benefits. RePlay aims to provide a more sensible upgrade path for Play1 applications.
We use Gitter to discuss RePlay development, feel free to join the channel to ask questions or just say hi.
- Uses standard Java build tooling —Gradle— for dependency management and builds:
- resulting in better compile times by incremental builds,
- no dependencies (
.jars) in version control (both for the framework's and your own project's repository), - no dependency on Ivy (an outdated dependency resolver),
- no Python scripts (with RePlay one simply uses Gradle or Maven),
- no "modules" folder (which was a custom dependency management mechanism),
- Removes most built-in Play modules (console, docviewer, grizzly, secure, testrunner) and the ability to serve WebSockets. These were not used by RePlay's users (could be reintroduced if needed).
- The
pdfandexcelPlay1 contrib modules are part of the RePlay project (they are plugins). - Does not require patches to Hibernate, Javaflow, etc.
- It does not use JBoss Javassist for bytecode manipulating "enhancers", resulting in:
- shorter application startup times (seriously improves development cycles),
- and support for other JVM languages, like Kotlin (example project).
- Less "magic", like: the before mentioned "enhancers" and creative use of exceptions for redirecting/responding/returning in controller methods.
- No overuse of
staticfields/methods throughout your application code; RePlay follows generally accepted OO best practices. - More actively maintained.
- More up to date dependencies (e.g. Hibernate).
- Promotes dependency injection for decoupling concerns (using Google's Guice as a DI provider like Play2).
- Where possible functionality has been refactored into plugins (more on that below) to increase modularity.
- Removed the
VirtualFileclass (since RePlay 2.4.0), all resources are simply loaded from the classpath.
RePlay requires JDK 17 to be built; it is the lower limit of several of RePlay's dependencies: ECJ (for rendering GroovyTemplates), FlyingSaucer (PDF generation) and Selenide (UI tests). Your project should use a JDK version equal to or greater than 17 that's also supported by Hibernate 6.6 (the version RePlay currently depends on), which are JDK 17, 21, 22 and 23.
You need to add RePlay dependencies to your build.gradle (or pom.xml):
dependencies {
implementation("io.github.replay-framework:framework:2.5.0")
implementation("io.github.replay-framework:javanet:2.5.0") // you can replace "javanet" by "netty3" or "netty4"
// Optionally:
implementation("io.github.replay-framework:guice:2.5.0")
implementation("io.github.replay-framework:fastergt:2.5.0")
implementation("io.github.replay-framework:liquibase:2.5.0")
implementation("io.github.replay-framework:pdf:2.5.0")
implementation("io.github.replay-framework:excel:2.5.0")
implementation("io.github.replay-framework:ehcache:2.5.0") // or `memcached`, or disable caching by not specifying a caching package. Earlier `memcached=true` setting won't be checked anymore!
}RePlay does not come with the play command line tool (written in Python 2.7) that it part of Play1.
Hence, the play new scaffolding generator is not available in RePlay.
To start a new RePlay application make a copy of demo application and work your way up from there.
Subprojects in RePlay's replay-tests/ folder show how to do certain things in RePlay (like using LiquiBase and
Hibernate backed applications (in liquibase-app), Kotlin (in helloworld-kotlin), some more advanced controller
techniques (in criminals) a multi-module application (in multi-module-app)
and dependency injection with Guice (in dependency-injection)).
Documentation for RePlay is found in (or referred to from) this README.
NOTE: Due to its small community, RePlay is not likely the best choice for a new project. Same holds true for Play1 and even Play2 (when not using Scala). RePlay primarily caters to maintainers of Play1-based applications. This README contains an extensive guide for porting Play1 applications to RePlay.
For a large part the documentation of Play1 may be used as a reference. Keep the differences between Play1 and RePlay (outlined above) in mind, in order to know what parts of the Play1 documentation to ignore.
API docs for the RePlay framework package are generated with ./gradlew :framework:javadoc after which they are found in the /framework/build/docs/javadoc/ folder.
The javadoc.io project provides online access to the Javadoc documentation of RePlay.
RePlay does not come with the play command line tool, which is used to start a Play1 application in development mode (i.e.: with play run)
providing auto-compilation and hot-swapping of code changes.
Developers of RePlay applications need to set up an IDE to get a good development flow.
These steps set up a hot-swapping development flow with IntelliJ IDEA for the criminals RePlay example application:
- Clone the replay repository
- Use IntelliJ IDEA to
File > Open...the replay project (notImport) by selecting the root of this project's repository - In
File > Settings... > Build, Excecution, Deployment > Compiler > Java Compilerfill the Additional command line parameters with the value:-parameters(also found in thebuild.gradlefile, it put the method argument names in the bytecode by which the framework auto-binds params at run time) - In
File > Settings... > Build, Excecution, Deployment > Buitl Tools > Gradleset both Build and run using and Run test using toIntelliJ IDEA(makes restarting and hot-swapping much faster) - Go to
Run -> Edit Configurations..., click the+(Add New Configuration) in the top-right corner and select "Application" - Fill in the following details, and click
OK:
- Name:
Criminals(how this run/debug configuration shows up in the IntelliJ UI) - JDK/JRE: select one you prefer (Java 17 seems to work fine)
- Use classpath of module:
criminals.main - Main class:
replay.replay-tests.criminals.Application(the package name,criminalsshould match the package that contains youApplicationclass inapp/) - VM options (shown with Modify options drop-down item Add VM options):
-XX:+ShowCodeDetailsInExceptionMessages(for more helpful errors) (applicable only for Java 14+)
Now a "Run/Debug Configuration" with the name of your app shows up in the top-right of the screen. You can press the "Run" button (with the green play icon) to start the application from the IDE.
To run the application in debug mode press the "Debug" button (with a little bug icon, next to the "Run" button) and all should work.
When in debug mode you can use CTRL-SHIFT-F9 to "Reload Changed Classes" (as IntelliJ calls hot-swapping).
This only works when class/method signatures were not changed.
If hot-swapping failed, you will see a notification in IDEA after which you need to restart the application.
To fully restart the project in debug mode use SHIFT-F9, or press the bug icon button again.
Finally, in File -> Settings... -> Build, Execution, Deployment -> Build Tools -> Gradle set both
Build and run using and Run test using to IntelliJ IDEA.
This should make restarting and hot-swapping much (5-30x) faster!
The ./gradlew jar command produces the build/libs/appname.jar file.
The following should start your application from the command line (possibly adding additional flags):
java -cp "build/classes/java/main:build/libs/*:build/lib/*" appname.Application
Replace appname with the name of the package your Application class resides in. The classpath string (after -cp) contains three parts:
- The first bit points to the folder with the application's
.classfiles (build/classes/java/main) built by the Gradle build script, as that's what RePlay (and Play1 as well) use instead of the copies of these files as found in the application's JAR file. - The second bit (
build/libs/*) points the application JAR file as build by Gradle (e.g.:./gradlew jar). - The last bit (
build/lib/*) points to the dependencies of the project as installed by Gradle (should be last, or they may overshadow project definitions).
You may find some warnings for "illegal reflective access" when running the application. These are safe to ignore up until JVM version 17.
With this flag --add-opens java.base/java.lang=ALL-UNNAMED these warnings from guice may be suppressed (this will be fixed in a future version of guice).
To suppress the "illegal reflective access" warnings from netty you could use io.github.replay-framework:netty4 instead of io.github.replay-framework:netty3.
Play1 installs some plugins out-of-the-box which you can disable in your project.
The plugins that Play1 enables by default will need to be explicitly added to your RePlay project's play.plugins file.
The ability to disable plugins is no longer needed (and has therefor been removed).
Some Play1 plugins do not have a RePlay equivalent, such as:
play.plugins.EnhancerPlugin (RePlay does not do byte-code "enhancing" by design),
play.ConfigurationChangeWatcherPlugin, play.db.Evolutions and play.plugins.ConfigurablePluginDisablingPlugin
(no longer needed as just explained).
The RePlay project comes with the following plugins:
play.data.parsing.TempFilePlugin¹ — Creates temporary folders for file parsing and deletes them after request completion.play.data.validation.ValidationPlugin¹ — Adds validation on controller methods parameters based on annotations.play.db.DBPlugin¹ — Sets up the Postgres, MySQL or H2 data source based on the configuration values.play.db.jpa.JPAPlugin¹ — Initialises required JPA EntityManagerFactories.play.i18n.MessagesPlugin¹ — The internationalization system for UI strings.play.jobs.JobsPlugin¹ — Simple cron-style or out-of-request-cycle jobs runner.play.libs.WS¹ — Simple HTTP client (to make webservices requests).play.modules.excel.Plugin— Installs the Excel spreadsheet rendering plugin (requires theio.github.replay-framework:pdflibrary). In Play1 this is available as a community plugin.play.modules.gtengineplugin.GTEnginePlugin² — Installs the Groovy Templates engine for rendering views (requires theio.github.replay-framework:fastergtlibrary).play.modules.logger.RequestLogPlugin— logs every request with response type+statusplay.modules.logger.RePlayLogoPlugin— Shows the RePlay logo at application startupplay.plugins.PlayStatusPlugin¹ — Installs the authenticated/@statusendpoint.play.plugins.security.AuthenticityTokenPlugin— Add automatic validation of a form'sauthenticityTokento mitigate CSRF attacks. In Play1 thecheckAuthenticity()method is built into theControllerclass and needs to be explicitly called.
¹) This plugin is installed by default in Play1 (no entry in the play.plugins file needed).
²) Built into the Play1 framework (not as a plugin), shipped as a plugin in RePlay.
A community plugin for creating PDFs exists for Play1.
In RePlay this functionality is part of the main project
and available as a regular library (no longer a plugin) named io.github.replay-framework:pdf.
RePlay projects put play.plugins file in conf/. The syntax of the play.plugins file remains the same.
Write your own plugins by extending play.PlayPlugin is still possible, porting one of the many Play1 modules
to RePlay should be straightforward or not needed at all.
Porting a Play1 application to RePlay requires quite some work, depending on the size of the application. The work will be significantly less than porting the application to Play2 or a currently popular Java MVC framework (like Spring Boot).
A serious part of the work stems from the removal of Play1's "Enhancers", these use JBoss Javassist to apply runtime bytecode manipulation which add methods and intercept method calls or member field access. Removing the enhancers gives RePlay many of its benefits: quick builds, reduce start-up times, allow non-Java JVM language interop, reduce magic, make mocking easier and results in more idiomatic Java code.
It is advised to perform the porting work as much as possible while still being based on Play1. This allows you to break up the effort in smaller "testable" steps making the effort more incremental and thereby greatly reducing the complexity of actual switch to RePlay. Where this is possible the guide below points this out with a TIP.
The following list breaks down the porting effort into tasks:
- Port the dependency specification from
conf/dependencies.yml(Ivy2 format) tobuild.gradle(Gradle format). - Ensure that
app/play.pluginsfile (or the file where theplay.plugins.descriptorconfiguration property is pointing) is on the classpath (e.g.sourceSets.main.resources { srcDir 'app' }) and add all plugins you need explicitly (see the section on "Plugins"). - Add the
app/<appname>/Application.javaandapp/<appname>/Module.java(see the RePlay example project and multi-module-app test and RePlay example project for inspiration). - Play1 recommends to subclass from
db.Model, which is deprecated in RePlay. Instead, implement a base model as part of your project as seen inreplay-test/liquibase-app. This give you more flexibility in configuring Hibernate handling of theidcolumn. - Play1's
PropertiesEnhancerwas removed.- This enhancer reduces the boilerplate needed to make classes adhere to the "Java Bean" standard.
In short: a bean is a Java class that (1) implements
java.io.Serializable, (2) implements public getter/setter methods for accessing the state, and (3) implements the default constructor (a public constructor that takes no arguments). All@Entityannotated classes (e.g. model classes) should adhere to the Bean standard. Play1'sPropertiesEnhancercreates the default constructor in case it is absent, creates getter/setter methods and rewrites direct access to Entities' public member fields (e.g.:obj.memberField;andobj.memberField = newValue;) to calls to the corresponding getter/setter methods. - In for a large part the model code still works: adherence to the Java Bean standard is not strictly enforced.
- In some cases the model code does not work without Play1's PropertiesEnhancer:
- Runtime errors for lacking default constructors: simply implement them for all
@Entityannotated classes. In most cases addingpublic ClassName() {}suffices. IntelliJ can help with that. - In some cases a member field's getter needs to be implemented and used (instead of the member field access) for Hibernate to work.
IntelliJ can do this per file, right-click a file and
Refactor > Encapsulate fields, where you pick all public non-static fields. - Since Groovy maps direct field access to use the getters and setters, your template code should still work.
- Runtime errors for lacking default constructors: simply implement them for all
- TIP: By setting
play.propertiesEnhancer.enabled=falseinconf/application.confof a Play1 project, this work required can be performed on the Play1 based version of the application. - TIP2: Use IntelliJ's
Refactor -> Encapsulate Fields...on all@Entityannotated classes to have them generated for you. - TIP3: For the
idfield onplay.db.jpa.Modelonly encapsulate with a getter. To do so copy theModelclass into your project, string replace allimport play.db.jpa.Modelto point to your copy of the class, runRefactor -> Encapsulate Fields...on your own class (only generate the getter!), finally remove your class and string replace the imports back to what they were.
- This enhancer reduces the boilerplate needed to make classes adhere to the "Java Bean" standard.
In short: a bean is a Java class that (1) implements
- Play1's
JPAEnhancerwas removed.-
In RePlay, entity classes (they in Play1 extend
Model) have to implementcreate,count,find*,allanddelete*methods themselves.- TIP: Reimplement these methods using the methods found in RePlay's
play.db.jpa.JPARepository.
- TIP: Reimplement these methods using the methods found in RePlay's
-
TIP: By adding the following lines to
conf/application.confof a Play1 project, the work required can be performed on the Play1 based version of the application.plugins.disable.0=play.db.jpa.JPAPlugin plugins.disable.1=play.modules.jpastats.JPAStatsPlugin -
TIP: Split the files in
app/models/into entities (e.g. aUserclass with the Hibernate entity definition) and repositories (e.g. aUserRepositoryclass with the static methods for retrievingUserentities from the database).
-
public class UserRepo {
static findById(final long id) {
return JPARepository.from(User.class).findById(id);
}
}refresh()has been removed fromplay.db.jpa.GenericModel; simply replace occurrences with:JPA.em().refresh(entityYouWantToRefresh)JPA.setRollbackOnly()becomesJPA.em().getTransaction().setRollbackOnly()play.Loggerhas been slimmed down (and renamed toPlayLoggingSetup). In RePlay it merely initializes the slf4j logger within the framework, it cannot be used for actual logging statements (e.g.Logger.warn(...)).- Where the
Loggerof Play1 uses theString.formatinterpolation (with%s,%d, etc.), the slf4j uses{}for interpolation (which is a bit faster). - TIP: You can already replace the use of
play.Loggerwith the slf4j logger in your Play1 application. - In RePlay logging is done as follows (common Java idiom):
- Where the
import org.slf4j.Logger; // replace `import play.Logger;` with these
import org.slf4j.LoggerFactory;
public class YourClassThatNeedsLogging {
// the following line allows quick access to the logger within this class' context
private static final Logger logger = LoggerFactory.getLogger(YourClassThatNeedsLogging.class);
public YourClassThatNeedsLogging(int i) {
logger.debug("Constructor invoked with param: {}", i); // example logging statement
}
// ...
}Play.classloaderis removed; replace it withCurrentClass.class.getClassLoader().play.libs.Cryptohas been slimmed down and is now calledCrypter.- TIP: You can simply copy the
play.libs.Cryptofile from Play1 into your project.
- TIP: You can simply copy the
play.libs.WShas been split up into theplay.libs.wspackage containing the classes that have been split out.Router.absolute()now takes a param. Fix this by passing itRouter.absolute(Http.Request.current()).IO.readFileAsString()now needs an additionalCharsetargument (usuallyStandardCharsets.UTF_8suffices).play.cache.Cache.get(...)only takes one argument, removing additional arguments is usually enough.play.Pluginchanged some of the method signatures.play.data.binding.TypeBinderchanged some of the method signatures.play.jobs.Jobrequires overriding ofdoJobWithResult()instead ofdoJob(). If the job does no return value the subclass should be parameterized overVoid, and returnnull. For example:
@OnApplicationStart
public class LoadMenuJob extends Job<Void> {
@Override
public Void doJobWithResult() {
// ...
return null;
}
}- The
play.mvc.Mailerclass was dropped, use theplay.libs.Mailclass instead.- TIP: Copy RePlay's
play.libs.Mailclass into your Play1 project and port over the mail logic while your application is still running on top of Play1. - Here an example of a simple text mail:
- TIP: Copy RePlay's
public class TextMails extends Mail {
public static void welcomeNewUSer(final User user, final Organization organization) {
TextEmail email = new TextEmail(); // subclass of a `org.apache.commons.mail.*` class
email.setFrom("[email protected]");
email.addRecipient(user.getEmailAddress());
email.setSubject(
String.format("Welcome %s! Follow the link in this mail to active your account.",
organization.name));
email.setMsg(
TemplateLoader.load("app/mails/text/welcomeNonTrialInternal.txt"))
.render(Map.of("user", user, "org", organization));
send(email);
}
}- Port the controller classes over to RePlay. This is likely the biggest task of the porting effort and
cannot be performed from your Play1 application like some other porting tasks.
- Since
play.result.Resultno longer extendsException, your controllers should returnResultinstead of throwing it. In Play1 the controller methods that trigger the request's response (likerender(...),renderText(...)andrenderJSON(...)) throw an exception. By this exception the rest of the method will not be executed (which may confuse IDEs). In RePlay this is considered abuse of the exception system, and the good oldreturnstatement is used to achieve the same. The following changes are needed to adapt Play1 code to this:- Make all action methods (the ones pointed at by
conf/routes) non-static. You may need to remove thestatickeyword from some non-action methods as well. - Make all action methods return
play.mvc.Resultinstead ofvoid(as RePlay does use exceptions for responding to requests). Some examples of what needs to change:renderJson(...);becomesreturn new RenderJson(...);.render(...);becomesreturn new View(...);, with slightly different arguments e.g.:render(token);becomesreturn new View("path/to/ControllerName/template.html", Map.of("order", order, "token", token));, orreturn new View("...").with(("order", order).with("token", token);. The second style allowsnullvalues to be passed to the template.
notFoundIfNull(token);becomesif (token == null) return new NotFound("Token missing");.- Triggering redirects using the bytecode mingled Play1 idiom
Controller.actionMethod(...);or justactionMethod()(in the same class) becomesreturn Redirect(...);where the path is provided as first argument. Alternatively theRedirectToActionclass can be used with a string pointing to controller methods (like in Play1).
- Methods annotated with
@Beforeand@Afteralso returnResult, and should returnnullto signify continuation (e.g. to the next@Beforeannotated method or to the controller action method itself).- In RePlay the
priorityannotation-argument is removed from that annotation.
- In RePlay the
- Sometimes private methods that return a value in Play1 also can trigger a response, because responses are triggered by exceptions in Play1. This is no longer allowed using RePlay and thus the code that triggers responses (actual controller code) should be separated from code that merely handles values (probably not controller code). In these cases it would be nice if Java already had multiple return values (through sum-types).
- Private methods with a
voidreturn type that trigger responses in Play1 (by throwing exceptions) need to returnResultin replay. In case these private methods do not trigger a response they should returnnullin the RePlay scenario. The call sites of those methods need the following bit of code to pass throughResultobjects:var result = privateMethod(); if (result != null) return result;. This ensures the rest of the method is evaluated.
- Make all action methods (the ones pointed at by
Http.Request.current()becomesrequest(as in Play1 many things are static that are not in RePlay).params.flash();becomesparams.flash(flash);(this stores the render params in the cookie to survive a redirect).- TIP: Start by moving your controller classes to an
unported/folder, and move them one-by-one back tocontroller/while porting. Ensure thatgitunderstand files were moved to allow merging in changes from Play1-based branches of your application.
- Since
Validation.valid(obj)needs an additional String as parameter to which the validation results are bound, and thus becomes:Validation.valid("obj", obj)Check,CheckWith,CheckWithCheckandEqualshave been removed from theplay.data.validationpackage.- They have been removed in favour of calling validation methods explicitly from the body of controller methods (opposed to configuring validations through annotations). This allows much needed control over your application's response in case of validation errors.
- TIP: Copy those files from Play1 into your project to ease the porting effort (maybe except
Equalsas it has so many dependencies).
- The
params.get()method now always returns aString, useparseXYZ()methods (likeBoolean.parseBoolean()) to convert results. - Writing directly to the stream
response.out, e.g. a final call toImageIO.write(outputImage, "png", response.out)with a Play1 codebase, needs an additionalreturn new Ok()with RePlay. - While porting the controllers you will find some changes to the views (templates) are required too:
- In some cases the full package path needs to be provided, e.g.:
Play.configuration.getProperty("key")becomesplay.Play.configuration.getProperty("key").
- In some cases the full package path needs to be provided, e.g.:
- Due to changed encrypting/signing of
CookieSessionStoreall active sessions are logged out when migrating from Play1 to RePlay. This means that running the Play1 version of the app side-by-side with the RePlay version is not possible (all users get logged out all the time). - As of September 2024, Play1 brings Hibernate 5.6 while RePlay upgraded to 6.4. This may result in some problems:
- Hibernate 6.4 is stricter when it comes to mapping columns to properties. This results in a
NonUniqueDiscoveredSqlAliasExceptionthrown when a column name occurs twice (e.g. theidcolumn) when mapping the result of a query with joins to an entity. - Hibernate changed the "generation strategy" for MySQL, hence you may want to implement
jpa.Modelyourself which is fully supported in RePlay.
- Hibernate 6.4 is stricter when it comes to mapping columns to properties. This results in a
RePlay doesn't support using other RePlay applications as modules, like Play1 (see issue #290)!
To mimic that feature, a new application.conf property play.classes.scanJars was introduced.
Just define your RePlay modules as a dependencies, and put their jar names in application.conf as comma separated:
play.classes.scanJars=my-fancy-dependency*.jar,my-other-dep-*.jar
The * wildcard is only for easy matching the versioned jar names.
For more details, see the multi-module-app example!
The RePlay Framework is distributed under MIT license.
The Play1 Framework, that RePlay forked, is distributed under Apache 2 licence.