-
Good Developer Experience
-
Fast feedback loop
-
Good testability
-
JustWorks ™
-
-
No more WAR!
-
Twitter! Follow @Lspacewalker
-
Java 6 EOL 2013-02
-
Java 7 EOL 2015-02
-
Java 9 2016 Q4 (2017 late Q1? Jigsaw extension)
-
λ → {}
-
Interface Upgrade
-
Default and static methods
-
-
Stream API
-
Gradle wrapper - batteries included (
gradlew
) -
Consistent, reproducible builds
-
Avoid "Works On My Machine" syndrome
-
-
Great IDE integration
-
Continuous build mode (TDD)
-
./gradlew -t or ./gradlew --continuous
-
gradle init
gives you…
- gradlew and gradlew.bat
-
Gradle wrapper scripts
- build.gradle
-
Main build file, specifies plugins, tasks, dependencies
- settings.gradle
-
Extra project metadata settings
-
application
-
Create ops friendly distributions
-
Don’t forget to set
mainClassName
-
-
-
Creates "fat jars", plays nicely with
application
plugin
-
-
idea
-
Customize IntelliJ project/module/workspace
-
Don’t VCS IDE settings
-
plugins {
id 'java'
id 'idea'
id 'com.github.johnrengelman.shadow' version '1.2.2'
// id "$id" version "$version"
}
idea {
project {
jdkName = '1.8' // (1)
languageLevel = '1.8' // (2)
vcs = 'Git' // (3)
}
}
-
Set JDK to 1.8
-
Set target language level to 1.8
-
Set VCS manager to Git
./gradlew shadowJar
Produces executable jar with flattened dependencies.
./gradlew installShadowApp
Produces executable jar and shell scripts for starting jar. Can also produce zip file via ./gradlew distShadowZip
or tar via ./gradlew distShadowTar
EditorConfig helps developers define and maintain consistent coding styles between different editors and IDEs.
Don’t argue about formatting, pick a standard and stick to it.
root = true
[*] # for all files
indent_style = space
indent_size = 2
# We recommend you to keep these unchanged
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
Supported by many IDEs, e.g. IntelliJ CTRL+ALT+L
-
Nice functionality around LXC
-
Images, file-system layer snap-shotting
-
Lighter than Virtualization
-
Total size
-
Boot Time
-
-
Counters "Works On My Machine" syndrome
-
Nice way to bring up services/dependencies/mechanisms that may not be available for your OS
-
Tool to provision Docker ready VM for Mac/Win
Once setup, you need to inform your environment about the VM.
$ docker-machine env default
export DOCKER_TLS_VERIFY="1"
export DOCKER_HOST="tcp://192.168.99.100:2376"
export DOCKER_CERT_PATH="C:\Users\danny\.docker\machine\machines\default"
export DOCKER_MACHINE_NAME="default"
# Run this command to configure your shell:
# eval "$(C:\Program Files\Docker Toolbox\docker-machine.exe env default)"
FROM ubuntu:14.04 # (1)
RUN apt-get update && apt-get install -y redis-server # (2)
EXPOSE 6379 # (3)
ENTRYPOINT ["/usr/bin/redis-server"] # (4)
-
Base image from Ubuntu Trusty image
-
Install Redis into new image
-
Declare that container is listening on port 6379
-
Start Redis server when container starts
$ docker build -t danhyun/redis .
$ docker run --name redis -d -p 6379:6379 danhyun/redis
$ docker exec -it redis bash
root@d42014247c2e:/# redis-cli
127.0.0.1:6379> set hello world
OK
127.0.0.1:6379> get hello
"world"
127.0.0.1:6379> del hello
(integer) 1
127.0.0.1:6379> get hello
(nil)
$ docker run --name postgres -e POSTGRES_PASSWORD=password -d -p 5432:5432 postgres
This command pulls down a postgres
Docker image from Docker Hub, names the container postgres
, detaches from session, maps container’s port 5432 to local port 5432.
$ docker exec -it postgres bash
root@b1db931a37a7:/# psql -U postgres
psql (9.4.5)
Type "help" for help.
postgres=# \l
postgres=# create database modern;
CREATE DATABASE
postgres=# \c modern
You are now connected to database "modern" as user "postgres".
modern=# create table meeting (
id serial primary key,
organizer varchar(255),
topic varchar(255),
description text
);
CREATE TABLE
modern=#insert into meeting
(organizer, topic, description)
values
('Dan H', 'Modern Java Web Development', 'A survey of essential tools/frameworks/techniques for the modern Java developer');
INSERT 0 1
modern=# select * from meeting;
id | organizer | topic | description
----+-----------+-----------------------------+--------------------------------------------------------------------------------
1 | Dan H | Modern Java Web Development | A survey of essentia tools/frameworks/techniques for the modern Java developer
(1 row)
-
Free signup
-
Rapid prototyping (free versions of services available)
Get the Heroku toolbelt here
Heroku only needs 2 things:
-
Procfile
- tells Heroku what to execute -
A
stage
task from Gradle
-
JDK 8+
-
just jar files, no binaries to install, no codegen
-
-
Minimal framework overhead (low resource usage, save $$$)
-
Unopinionated - Make your app solve your problems, don’t let framework get in the way
-
Reactive, Non-blocking and fully asynchronous
-
Excellent testing support
-
Functional interface
-
void handle(Context context) {}
-
send response now or delegate to the next handler
-
convenience API for specifying request handling flow
-
"if-else" for handlers
-
Chains are composable
-
Map like lookup for services
-
Immutable
-
Way to communicate between handlers
-
Promises
-
Operations
-
Blocking
Blazing fast JDBC library.
Configure HikariCP to use our dockerized PostgreSQL instance.
db:
dataSourceClassName: org.postgresql.ds.PGSimpleDataSource
username: postgres
password: password
dataSourceProperties:
databaseName: modern
serverName: 192.168.99.100
portNumber: 5432
.module(HikariModule.class, config -> {
config
.setDataSourceClassName("org.postgresql.ds.PGSimpleDataSource");
config.setUsername("postgres");
config.setPassword("password");
config.addDataSourceProperty("databaseName", "modern");
config.addDataSourceProperty("serverName", "192.168.99.100");
config.addDataSourceProperty("portNumber", "5432");
})
.bindInstance(HikariConfig.class, configData.get("/db", HikariConfig.class))
.module(HikariModule.class)
ServerConfig configData = ServerConfig.builder()
.baseDir(BaseDir.find())
.yaml("db.yaml")
.env()
.sysProps()
.args(args)
.require("/db", HikariConfig.class)
.build();
-
Type Safe fluent style API for accessing DB
buildscript { repositories { jcenter() } dependencies { classpath 'org.postgresql:postgresql:9.4-1206-jdbc42' classpath 'org.jooq:jooq-codegen:3.7.1' classpath 'org.jyaml:jyaml:1.3' } } dependencies { runtime 'org.postgresql:postgresql:9.4-1206-jdbc42' compile 'org.jooq:jooq:3.7.1' compile ratpack.dependency('hikari') } import org.jooq.util.jaxb.* import org.jooq.util.* import org.ho.yaml.Yaml task jooqCodegen { doLast { def config = Yaml.load(file('src/ratpack/postgres.yaml')).db def dsProps = config.dataSourceProperties Configuration configuration = new Configuration() .withJdbc(new Jdbc() .withDriver("org.postgresql.Driver") .withUrl("jdbc:postgresql://$dsProps.serverName:$dsProps.portNumber/$dsProps.databaseName") .withUser(config.username) .withPassword(config.password)) .withGenerator(new Generator() // .withGenerate(new Generate() // .withImmutablePojos(true) // (1) // .withDaos(true) // (2) // .withFluentSetters(true)) // (3) .withDatabase(new Database() .withName("org.jooq.util.postgres.PostgresDatabase") .withIncludes(".*") .withExcludes("") .withInputSchema("public")) .withTarget(new Target() .withPackageName("jooq") .withDirectory("src/main/java"))) GenerationTool.generate(configuration) } }
-
Generates immutable POJOs
-
Generates DAOs
-
Generates fluent setters for generated Records/POJOs/Interfaces
DSLContext
provides type-safe fluent API style querying.
jOOQ will responsibly borrow and release connections from the provided DataSource
.
public class DefaultMeetingRepository implements MeetingRepository {
private final DSLContext context;
@Inject
public DefaultMeetingRepository(DSLContext context) {
this.context = context;
}
@Override
public Promise<List<Meeting>> getMeetings() {
return Blocking.get(() ->
context
.select().from(MEETING).fetchInto(Meeting.class) // (1)
);
}
@Override
public Operation addMeeting(Meeting meeting) {
return Blocking.op(() -> context.newRecord(MEETING, meeting).store());
}
}
-
fetchInto(Class)
provides SQL to POJO mapping. POJOs can be generated by jOOQ if desired.
public class JooqModule extends AbstractModule {
@Override
protected void configure() {
bind(MeetingRepository.class).to(DefaultMeetingRepository.class).in(Scopes.SINGLETON);
}
@Provides
@Singleton
public DSLContext dslContext(DataSource dataSource) {
return DSL.using(new DefaultConfiguration().derive(dataSource));
}
}
dependencies {
compile 'biz.paluch.redis:lettuce:4.0.1.Final'
}
redis:
host: 192.168.99.100
port: 6379
public class RedisConfig {
private String url;
public String getUrl() {
return url;
}
public void setUrl(String url) {
this.url = url;
}
}
RatpackServer.start(ratpackServerSpec -> ratpackServerSpec
.serverConfig(config -> config
.baseDir(BaseDir.find())
.yaml("postgres.yaml")
.yaml("redis.yaml")
.env()
.sysProps()
.args(args)
.require("/db", HikariConfig.class)
.require("/redis", RedisConfig.class) // (1)
)
-
Add
RedisConfig
to the Registry
public class RedisModule extends AbstractModule {
@Override
protected void configure() { }
@Provides
@Singleton
public RedisClient redisClient(RedisConfig config) { // (1)
return RedisClient.create(config.getUrl());
}
@Provides
@Singleton
public StatefulRedisConnection<String, String> asyncCommands(RedisClient client) {
return client.connect();
}
@Provides
@Singleton
public RedisAsyncCommands<String, String> asyncCommands(StatefulRedisConnection<String, String> connection) {
return connection.async();
}
@Provides
@Singleton
public Service redisCleanup(RedisClient client, StatefulRedisConnection<String, String> connection) {
return new Service() { // (2)
@Override
public void onStop(StopEvent event) throws Exception {
connection.close(); // (3)
client.shutdown(); // (3)
}
};
}
}
-
Get
RedisConfig
from Registry -
Service
provides an opportunity to hook into Ratpack’s start/stop lifecycle events -
Cleanup Redis connection and client
public interface RatingRepository {
Promise<Map<String, String>> getRatings(Long meetingId);
default Promise<Double> getAverageRating(Long meetingId) {
return getRatings(meetingId)
.map(m -> m.entrySet()
.stream()
.map(e -> Pair.of(Integer.valueOf(e.getKey()), Integer.valueOf(e.getValue())))
.flatMapToInt(pair -> IntStream.range(0, pair.right).map(i -> pair.left))
.average().orElse(0d)
);
}
Operation rateMeeting(String meetingId, String rating);
}
public class DefaultRatingRepository implements RatingRepository {
private final RedisAsyncCommands<String, String> commands;
@Inject
public DefaultRatingRepository(RedisAsyncCommands<String, String> commands) {
this.commands = commands;
}
Function<Long, String> getKeyForMeeting = (id) -> "meeting:" + id + ":rating";
@Override
public Promise<Map<String, String>> getRatings(Long meetingId) {
return Promise.of(downstream ->
commands
.hgetall(getKeyForMeeting.apply(meetingId)) // (1)
.thenAccept(downstream::success) // (2)
);
}
@Override
public Operation rateMeeting(String meetingId, String rating) {
return Promise.of(downstream ->
commands.hincrby(
getKeyForMeeting.apply(Long.valueOf(meetingId)),
String.valueOf(rating), 1
).thenAccept(downstream::success)
).operation();
}
}
-
Equivalent of
HGETALL meeting:$id:rating
-
Signal to downstream consumer that Lettuce is done with async activity
public interface MeetingService {
Promise<List<Meeting>> getMeetings();
Operation addMeeting(Meeting meeting);
Operation rateMeeting(String id, String rating);
}
public class DefaultMeetingService implements MeetingService {
private final MeetingRepository meetingRepository;
private final RatingRepository ratingRepository;
public DefaultMeetingService(MeetingRepository meetingRepository, RatingRepository ratingRepository) {
this.meetingRepository = meetingRepository;
this.ratingRepository = ratingRepository;
}
@Override
public Promise<List<Meeting>> getMeetings() {
return meetingRepository.getMeetings()
.flatMap(meetings ->
Promise.value(
meetings.stream()
.peek(meeting ->
ratingRepository.getAverageRating(meeting.getId())
.then(meeting::setRating) // (1)
)
.collect(Collectors.toList()))
);
}
@Override
public Operation addMeeting(Meeting meeting) {
return meetingRepository.addMeeting(meeting);
}
@Override
public Operation rateMeeting(String id, String rating) {
return ratingRepository.rateMeeting(id, rating);
}
}
-
This is naughty, don’t perform side effects
Create a new module to register our RatingRepository
and MeetingService
public class MeetingModule extends AbstractModule {
@Override
protected void configure() {
}
@Provides
@Singleton
public RatingRepository ratingRepository(RedisAsyncCommands<String, String> commands) {
return new DefaultRatingRepository(commands);
}
@Provides
@Singleton
public MeetingService meetingService(MeetingRepository meetingRepository, RatingRepository ratingRepository) {
return new DefaultMeetingService(meetingRepository, ratingRepository);
}
}
public class App {
public static void main(String[] args) throws Exception {
RatpackServer.start(serverSpec -> serverSpec
.serverConfig(/*config*/)
.registry(Guice.registry(bindings -> bindings
.module(HikariModule.class)
.module(JooqModule.class)
.module(RedisModule.class)
.module(MeetingModule.class) // (1)
.bind(MeetingChainAction.class)
))
.handlers(/*handlers*/)
);
}
}
-
Register our new module with the app
Main command to execute:
web: env DATABASE_URL=$DATABASE_URL build/installShadow/modern-java-web/bin/modern-java-web redis.url=$REDIS_URL
Ratpack can pick up config information from just about anywhere.
Here we expose DATABASE_URL
as an env variable and pass in REDIS_URL
as redis.url
as a program arg.
task stage(dependsOn: installShadowApp)
$ heroku create
Creating gentle-beyond-5974... done, stack is cedar-14 // (1)
https://gentle-beyond-5974.herokuapp.com/ | https://git.heroku.com/gentle-beyond-5974
.git // (3)
Git remote heroku added // (2)
heroku-cli: Updating... done.
-
cedar-14
is the Java 8 platform, Heroku’s default Java offering -
Generated app name
gentle-beyond-5974
-
Added git remote named
heroku
-
Install Plugin
heroku plugins:install heroku-redis
-
Add to app
heroku addons:create heroku-redis:hobby-dev
-
Pass
$REDIS_URL
to your app -
Heroku redis-cli
heroku redis:cli
-
Add PostgreSQL to app
heroku addons:create heroku-postgresql:hobby-dev
-
Wait to come online
heroku pg:wait
-
Pass
$DATABASE_URL
to app -
Connect to remote
heroku pg:psql
(Requires psql installed locally)
Heroku exposes the Postgres URL in a format that JDBC cannot parse.
public interface HerokuUtils {
Function<String, List<String>> extractDbProperties = (url) -> {
if (Strings.isNullOrEmpty(url)) return Collections.<String>emptyList();
Pattern herokuDbPattern = Pattern
.compile("postgres://(?<username>[^:]+):(?<password>[^:]+)@(?<serverName>[^:]+):(?<portNumber>[0-9]+)/(?<databaseName>.+)"); // (1)
Matcher matcher = herokuDbPattern.matcher(url);
if (!matcher.matches()) return Collections.<String>emptyList();
return Stream
.of("username", "password", "databaseName", "serverName", "portNumber")
.map(prop -> Pair.of(prop, matcher.group(prop))) // (2)
.map(pair -> pair.left.equals(pair.left.toLowerCase()) ?
pair : Pair.of("dataSourceProperties." + pair.left, pair.right)
)
.map(pair -> Pair.of("db." + pair.left, pair.right))
.map(pair -> pair.left + "=" + pair.right)
.collect(Collectors.toList());
};
}
-
As of Java 7 you can provide group names in regex
-
We ask for match by group name and construct a
Pair
of property to extracted value
public class App {
public static void main(String[] args) throws Exception {
List<String> programArgs = Lists.newArrayList(args);
programArgs.addAll(
HerokuUtils.extractDbProperties
.apply(System.getenv("DATABASE_URL")) // (1)
);
RatpackServer.start(serverSpec -> serverSpec
.serverConfig(config -> config
.baseDir(BaseDir.find())
.yaml("postgres.yaml")
.yaml("redis.yaml")
.env()
.sysProps()
.args(programArgs.stream().toArray(String[]::new)) //(2)
.require("/db", HikariConfig.class)
.require("/redis", RedisConfig.class)
)
.registry(/* registry */)
.handlers(/* handlers */)
);
}
}
-
Extract db properties if present
-
Pass newly constructed list to Ratpack’s server config
$ git push heroku master
remote: BUILD SUCCESSFUL
remote:
remote: Total time: 40.614 secs
remote: -----> Discovering process types
remote: Procfile declares types -> web
remote:
remote: -----> Compressing... done, 71.3MB
remote: -----> Launching... done, v4
remote: https://gentle-beyond-5974.herokuapp.com/ deployed to Heroku
remote:
remote: Verifying deploy... done.
Heroku will see that this is a Gradle project and invoke ./gradlew stage
.
After the build Heroku will run the command from Procfile
.
$ heroku open
Opens your newly minted webapp in your browser.