§What’s new in Play 2.7
This page highlights the new features of Play 2.7. If you want to learn about the changes you need to make when you migrate to Play 2.7, check out the Play 2.7 Migration Guide.
§Scala 2.13 support
Play 2.7 is the first release of Play that is cross-built against Scala 2.13, 2.12, and 2.11. A number of dependencies were updated to achieve this.
You can select which version of Scala you would like to use by setting the scalaVersion
setting in your build.sbt
.
For Scala 2.12:
scalaVersion := "2.12.16"
For Scala 2.11:
scalaVersion := "2.11.12"
For Scala 2.13:
scalaVersion := "2.13.10"
§Lifecycle managed by Akka’s Coordinated Shutdown
Play 2.6 introduced the usage of Akka’s Coordinated Shutdown but still didn’t use it all across the core framework or exposed it to the end user. Coordinated Shutdown is an Akka Extension with a registry of tasks that can be run in an ordered fashion during the shutdown of the Actor System.
Coordinated Shutdown internally handles Play 2.7 Play’s lifecycle and an instance of CoordinatedShutdown
is available for injection. Coordinated Shutdown gives you fine grained phases - organized as a directed acyclic graph (DAG) - where you can register tasks instead of just having a single phase like Play’s application lifecycle. For example, you can add tasks to run before or after server binding, or after all the current requests finishes. Also, you will have better integration with Akka Cluster.
You can find more details on the new section on Coordinated Shutdown on the Play manual, or you can have a look at Akka’s reference docs on Coordinated Shutdown.
§Guice was upgraded to 4.2.2
Guice, the default dependency injection framework used by Play, was upgraded to 4.2.2 (from 4.1.0). Have a look at the 4.2.2, 4.2.1 and the 4.2.0 release notes. This new Guice version introduces breaking changes, so make sure you check the Play 2.7 Migration Guide.
§Java forms bind multipart/form-data
file uploads
Until Play 2.6, the only way to retrieve a file that was uploaded via a multipart/form-data
encoded form was by calling request.body().asMultipartFormData().getFile(...)
inside the action method.
Starting with Play 2.7 such an uploaded file will now also be bound to a Java Form. If you are not using a custom multipart file part body parser all you need to do is add a FilePart
of type TemporaryFile
to your form:
import play.libs.Files.TemporaryFile;
import play.mvc.Http.MultipartFormData.FilePart;
public class MyForm {
private FilePart<TemporaryFile> myFile;
public void setMyFile(final FilePart<TemporaryFile> myFile) {
this.myFile = myFile;
}
public FilePart<TemporaryFile> getMyFile() {
return this.myFile;
}
}
Like before, use the FormFactory
you injected into your Controller to create the form:
Form<MyForm> form = formFactory.form(MyForm.class).bindFromRequest(req);
If the binding was successful (form validation passed) you can access the file:
MyForm myform = form.get();
myform.getMyFile();
Some useful methods were added as well to work with uploaded files:
// Get all files of the form
form.files();
// Access the file of a Field instance
Field myFile = form.field("myFile");
field.file();
// To access a file of a DynamicForm instance
dynamicForm.file("myFile");
Note: If you are using using a custom multipart file part body parser you just have to replace
TemporaryFile
with the type your body parser uses.
§Constraint annotations offered for Play Java are now @Repeatable
All of the constraint annotations defined by play.data.validation.Constraints
are now @Repeatable
. This change lets you, for example, reuse the same annotation on the same element several times but each time with different groups
. For some constraints however it makes sense to let them repeat itself anyway, like @ValidateWith
:
@Validate(groups={GroupA.class})
@Validate(groups={GroupB.class})
public class MyForm {
@ValidateWith(MyValidator.class)
@ValidateWith(MyOtherValidator.class)
@Pattern(value="[a-k]", message="Should be a - k")
@Pattern(value="[c-v]", message="Should be c - v")
@MinLength(value=4, groups={GroupA.class})
@MinLength(value=7, groups={GroupB.class})
private String name;
//...
}
You can of course also make your own custom constraints @Repeatable
as well and Play will automatically recognize that.
§Payloads for Java validate
and isValid
methods
When using advanced validation features you can now pass a ValidationPayload
object, containing useful information sometimes needed for a validation process, to a Java validate
or isValid
method.
To pass such a payload to a validate
method just annotate your form with @ValidateWithPayload
(instead of just @Validate
) and implement ValidatableWithPayload
(instead of just Validatable
):
import java.util.Map;
import com.typesafe.config.Config;
import play.data.validation.Constraints.ValidatableWithPayload;
import play.data.validation.Constraints.ValidateWithPayload;
import play.data.validation.Constraints.ValidationPayload;
import play.i18n.Lang;
import play.i18n.Messages;
import play.libs.typedmap.TypedMap;
@ValidateWithPayload
public class SomeForm implements ValidatableWithPayload<String> {
@Override
public String validate(ValidationPayload payload) {
Lang lang = payload.getLang();
Messages messages = payload.getMessages();
Map<String, Object> ctxArgs = payload.getArgs();
TypedMap attrs = payload.getAttrs();
Config config = payload.getConfig();
// ...
}
}
In case you wrote your own custom class-level constraint, you can also pass a payload to an isValid
method by implementing PlayConstraintValidatorWithPayload
(instead of just PlayConstraintValidator
):
import javax.validation.ConstraintValidatorContext;
import play.data.validation.Constraints.PlayConstraintValidatorWithPayload;
import play.data.validation.Constraints.ValidationPayload;
// ...
public class ValidateWithDBValidator implements PlayConstraintValidatorWithPayload<SomeValidatorAnnotation, SomeValidatableInterface<?>> {
//...
@Override
public boolean isValid(final SomeValidatableInterface<?> value, final ValidationPayload payload, final ConstraintValidatorContext constraintValidatorContext) {
// You can now pass the payload on to your custom validate(...) method:
return reportValidationStatus(value.validate(...., payload), constraintValidatorContext);
}
}
Note: Don’t get confused with
ValidationPayload
andConstraintValidatorContext
: The former class is provided by Play and is what you use in your day-to-day work when dealing with forms in Play. The latter class is defined by the Bean Validation specification and is used only internally in Play - with one exception: This class emerges when your write your own custom class-level constraints, like in the last example above, where you only need to pass it on to thereportValidationStatus
method however anyway.
§Support for Caffeine
Play now offers a CacheApi implementation based on Caffeine. Caffeine is the recommended cache implementation for Play users.
To migrate from EhCache to Caffeine you will have to remove ehcache
from your dependencies and replace it with caffeine
. To customize the settings from the defaults, you will also need to update the configuration in application.conf as explained in the documentation.
Read the documentation for the Java cache API and Scala cache API to learn more about configuring caching with Play.
§New Content Security Policy Filter
There is a new Content Security Policy filter available that supports CSP nonce and hashes for embedded content.
The previous setting of enabling CSP by default and setting it to default-src 'self'
was too strict, and interfered with plugins. The CSP filter is not enabled by default, and the contentSecurityPolicy
in the SecurityHeaders filter is now deprecated and set to null
by default.
The CSP filter uses Google’s Strict CSP policy by default, which is a nonce based policy. It is recommended to use this as a starting point, and use the included CSPReport body parsers and actions to log CSP violations before enforcing CSP in production.
§HikariCP upgraded
HikariCP was updated to its latest major version. Have a look at the Migration Guide to see what changed.
§Play WS curl
filter for Java
Play WS enables you to create play.libs.ws.WSRequestFilter
to inspect or enrich the requests made. Play provides a “log as curl
” filter, but this was lacking for Java developers. You can now write something like:
ws.url("https://www.playframework.com")
.setRequestFilter(new AhcCurlRequestLogger())
.addHeader("My-Header", "Header value")
.get();
And then the following log will be printed:
curl \
--verbose \
--request GET \
--header 'My-Header: Header Value' \
'https://www.playframework.com'
This can be specially useful if you want to reproduce the request in isolation and also change curl
parameters to see how it goes.
§Gzip Filter now supports compression level configuration
When using gzip encoding, you can now configure the compression level to use. You can configure it using play.filters.gzip.compressionLevel
, for example:
play.filters.gzip.compressionLevel = 9
See more details at GzipEncoding.
§API Additions
Here are some of the relevant API additions we made for Play 2.7.0.
§Result HttpEntity
streamed methods
Previous versions of Play had convenient methods to stream results using HTTP chunked transfer encoding:
- Java
-
public Result chunked() { Source<ByteString, NotUsed> body = Source.from(Arrays.asList(ByteString.fromString("first"), ByteString.fromString("second"))); return ok().chunked(body); }
- Scala
-
def chunked = Action { val body = Source(List("first", "second", "...")) Ok.chunked(body) }
In Play 2.6, there was no convenient method to return a streamed Result in the same way without using HTTP chunked encoding. You instead had to write this:
- Java
-
public Result streamed() { Source<ByteString, NotUsed> body = Source.from(Arrays.asList(ByteString.fromString("first"), ByteString.fromString("second"))); return ok().sendEntity(new HttpEntity.Streamed(body, Optional.empty(), Optional.empty())); }
- Scala
-
def streamed = Action { val body = Source(List("first", "second", "...")).map(s => ByteString.fromString(s)) Ok.sendEntity(HttpEntity.Streamed(body, None, None)) }
Play 2.7 fixes this by adding a new streamed
method on results, that works similar to chunked
:
- Java
-
public Result streamed() { Source<ByteString, NotUsed> body = Source.from(Arrays.asList(ByteString.fromString("first"), ByteString.fromString("second"))); return ok().streamed(body, Optional.empty(), Optional.empty()); }
- Scala
-
def streamed = Action { val body = Source(List("first", "second", "...")).map(s => ByteString.fromString(s)) Ok.streamed(body, contentLength = None) }
§New Http Error Handlers
Play 2.7 brings two new implementations for play.api.http.HttpErrorHandler
. The first one is JsonHttpErrorHandler
, which will return errors formatted in JSON and is a better alternative if you are developing an REST API that accepts and returns JSON payloads. The second one is HtmlOrJsonHttpErrorHandler
which returns HTML or JSON errors based on the preferences specified in client’s Accept
header. It is a better option if your application uses a mixture of HTML and JSON, as is common in modern web apps.
You can read more details at the docs for Java or Scala.
§Nicer syntax for Router.withPrefix
In Play 2.7 we introduce some syntax sugar to use play.api.routing.Router.withPrefix
. Instead of writing:
val router = apiRouter.withPrefix("/api")
You can now write:
val router = "/api" /: apiRouter
Or even combine more path segments:
val router = "/api" /: "v1" /: apiRouter
§Concatenating Routers
In Play 2.7 we introduce a new method orElse
to programatically compose Routers
.
You can now compose routers as following:
- Java
-
Router router = oneRouter.orElse(anotherRouter)
- Scala
-
val router = oneRouter.orElse(anotherRouter)
§Isolation level for Database transactions
You can now choose an isolation level when using play.api.db.Database.withTransaction
API (play.db.Database
for Java users). For example:
- Java
-
public void someDatabaseOperation() { database.withTransaction(TransactionIsolationLevel.ReadUncommitted, connection -> { ResultSet resultSet = connection.prepareStatement("select * from users where id = 10").executeQuery(); // consume the resultSet and return some value }); }
- Scala
-
def someDatabaseOperation(): Unit = { database.withTransaction(TransactionIsolationLevel.ReadUncommitted) { connection => val resultSet: ResultSet = connection.prepareStatement("select * from users where id = 10").executeQuery(); // consume the resultSet and return some value } }
The available transaction isolation levels mimic what is defined in java.sql.Connection
.
Next: Migration Guides