_____ _____ ___ ___ _
/ // / | \ ___| \| | ___ _ _
/ // / | ' // -_) '_/| |/ _ | \| |
/ // / |_|_\\___|_| |_|\___|\_ /
/____//____/ /_/
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 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 dependecies (
.jar
s) 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
pdf
andexcel
Play1 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
static
fields/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
VirtualFile
class (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 Compiler
fill the Additional command line parameters with the value:-parameters
(also found in thebuild.gradle
file, 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 > Gradle
set 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,criminals
should match the package that contains youApplication
class 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
.class
files (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:pdf
library). 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:fastergt
library).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/@status
endpoint.play.plugins.security.AuthenticityTokenPlugin
— Add automatic validation of a form'sauthenticityToken
to mitigate CSRF attacks. In Play1 thecheckAuthenticity()
method is built into theController
class 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 it's 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.plugins
file (or the file where theplay.plugins.descriptor
configuration 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.java
andapp/<appname>/Module.java
(see the RePlay example project and multi-module-app test for inspiration). 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 theid
column. - Play1's
PropertiesEnhancer
was 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@Entity
annotated classes (e.g. model classes) should adhere to the Bean standard. Play1'sPropertiesEnhancer
creates 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
@Entity
annotated 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=false
inconf/application.conf
of 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@Entity
annotated classes to have them generated for you. - TIP3: For the
id
field onplay.db.jpa.Model
only encapsulate with a getter. To do so copy theModel
class into your project, string replace allimport play.db.jpa.Model
to 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
JPAEnhancer
was removed.-
In RePlay, entity classes (they in Play1 extend
Model
) have to implementcreate
,count
,find*
,all
anddelete*
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.conf
of 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. aUser
class with the Hibernate entity definition) and repositories (e.g. aUserRepository
class with the static methods for retrievingUser
entities 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.Logger
has 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
Logger
of Play1 uses theString.format
interpolation (with%s
,%d
, etc.), the slf4j uses{}
for interpolation (which is a bit faster). - TIP: You can already replace the use of
play.Logger
with 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.classloader
is removed; replace it withCurrentClass.class.getClassLoader()
.play.libs.Crypto
has been slimmed down and is now calledCrypter
.- TIP: You can simply copy the
play.libs.Crypto
file from Play1 into your project.
- TIP: You can simply copy the
play.libs.WS
has been split up into theplay.libs.ws
package 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 additionalCharset
argument (usuallyStandardCharsets.UTF_8
suffices).play.cache.Cache.get(...)
only takes one argument, removing additional arguments is usually enough.play.Plugin
changed some of the method signatures.play.data.binding.TypeBinder
changed some of the method signatures.play.jobs.Job
requires 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.Mailer
class was dropped, use theplay.libs.Mail
class instead.- TIP: Copy RePlay's
play.libs.Mail
class 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("info@example.org");
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.Result
no longer extendsException
, your controllers should returnResult
instead 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 oldreturn
statement 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 thestatic
keyword from some non-action methods as well. - Make all action methods return
play.mvc.Result
instead 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 allowsnull
values 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 theRedirectToAction
class can be used with a string pointing to controller methods (like in Play1).
- Methods annotated with
@Before
and@After
also returnResult
, and should returnnull
to signify continuation (e.g. to the next@Before
annotated method or to the controller action method itself).- In RePlay the
priority
annotation-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
void
return type that trigger responses in Play1 (by throwing exceptions) need to returnResult
in replay. In case these private methods do not trigger a response they should returnnull
in the RePlay scenario. The call sites of those methods need the following bit of code to pass throughResult
objects: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 thatgit
understand 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
,CheckWithCheck
andEquals
have been removed from theplay.data.validation
package.- 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
Equals
as 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
CookieSessionStore
all 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
NonUniqueDiscoveredSqlAliasException
thrown when a column name occurs twice (e.g. theid
column) 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.Model
yourself which is fully supported in RePlay.
- Hibernate 6.4 is stricter when it comes to mapping columns to properties. This results in a
The RePlay Framework is distributed under MIT license.
The Play1 Framework, that RePlay forked, is distributed under Apache 2 licence.