-
Notifications
You must be signed in to change notification settings - Fork 212
Getting started : Tutorial
#Getting Started
Create a Java 8 application with a package : app1.simple
package app1.simple;
import com.aol.micro.server.MicroServerStartup;
import com.aol.micro.server.config.Microserver;
public class MicroserverApp {
public static void main(String[] args){
new MicroserverApp(()->"simple").run();
}
}
add the following dependencies:-
compile group: 'com.aol.advertising.microservices', name:'microserver', version:'0.51'
testCompile group: 'junit', name: 'junit', version:'4.10'
testCompile group: 'org.mockito', name: 'mockito-all', version:'1.9.5'
testCompile group: 'org.hamcrest', name: 'hamcrest-all', version:'1.1'
testCompile group: 'org.hsqldb', name:'hsqldb', version:'2.0.0'
Run (as a standard Java application)
Browse to : http://localhost:8080/simple/application.wadl
Application wadl - various REST end points available
e.g. http://localhost:8080/simple/active/jobs
Will show any active & completed scheduled jobs on your system
##Add a REST End point
- Create a new class MyRestEndPoint
- Make a Microserver end point (add @Rest annotation)
- Specify jax-rs path (@Path(“/mypath”) )
- Define a “hello world” end point e.g.
@GET
@Produces("text/plain")
@Path("/hello")
public String hello(){
return "world";
}
Start application and browse to End point Browse to : http://localhost:8080/simple/mypath/hello
##Testing your End point
- Create a unit test.
- Create @Before and @After methods
- Write a test using the async NIO REST Client
@Before should start the Microserver, passing in our MicroserverApp.class to configure it. It should use the start() method rather than run();
MicroServerStartup server;
@Before
public void startServer(){
server = new MicroserverApp( MicroserverApp.class, ()-> "simple");
server.start();
}
@After should shut down the server
@After
public void stopServer(){
server.stop();
}
Create an instance of Microserver Rest Client that will accept “text/plain” responses
private final RestClient<String> rest = new RestClient(1000,1000).withAccept("text/plain");
Then test :-
public class SimpleAppTest {
private final RestClient<String> rest = new RestClient(1000,1000).withAccept("text/plain");
MicroServerStartup server;
@Before
public void startServer(){
server = new MicroserverApp( MicroserverApp.class, ()-> "simple");
server.start();
}
@After
public void stopServer(){
server.stop();
}
@Test
public void basicEndPoint(){
assertThat(rest.get("http://localhost:8080/simple/mypath/hello").join(),is("world"));
}
}
##Tracking Requests
Autowire in a Guava EventBus
private final EventBus bus;
@Autowired
public MyRestEndPoint(EventBus bus ){
this.bus = bus;
}
We like immutability :) Add in an AtomicLong to generate Correlation Ids
private final AtomicLong correlationProvider = new AtomicLong(0);
In the hello method post some data around the request
@GET
@Produces("text/plain")
@Path("/hello")
public String hello(){
long correlationId = correlationProvider.incrementAndGet();
bus.post(RequestEvents.start(QueryIPRetriever.getIpAddress(),correlationId));
try{
return "world";
}finally{
bus.post(RequestEvents.finish("success",correlationId));
}
}
Browse to http://localhost:8080/simple/mypath/hello to generate some requests Browse to http://localhost:8080/simple/active/requests to view tracked request data
##Adding a datasource
During early development, when can configure properties inside the Microserver annotation before migrating to a property file. We can also tell Microserver to configure a JDBC Datasource as a Spring bean. It will automatically pick up our properties. E.g.
@Microserver(springClasses = { Classes. DATASOURCE_CLASSES}, properties = {
"db.connection.driver", "org.hsqldb.jdbcDriver", "db.connection.url",
"jdbc:hsqldb:mem:aname", "db.connection.username", "sa",
"db.connection.dialect", "org.hibernate.dialect.HSQLDialect",
"db.connection.ddl.auto", "create-drop" })
By using db.connection.ddl.auto create-drop, the in memory database (hsqldb) will auto-create the DB on startup.
If we'd like to configure JDBC instead we can tell Microserver to configure Spring JDBC_CLASSES.
@Microserver(springClasses = { Classes. JDBC_CLASSES}, properties = {
"db.connection.driver", "org.hsqldb.jdbcDriver", "db.connection.url",
"jdbc:hsqldb:mem:aname", "db.connection.username", "sa",
"db.connection.dialect", "org.hibernate.dialect.HSQLDialect",
"db.connection.ddl.auto", "create-drop" },
entityScan = "app1.simple”)
By adding an entityScan parameter, we can pick up JPA annotated beans and using them to create our database structure for us. Auto-create the DB on startup :- so add an entity and entity scan property
@javax.persistence.Entity
@Table(name = "t_jdbc", uniqueConstraints = @UniqueConstraint(columnNames = {
"name", "value" }))
public class Entity implements java.io.Serializable {
private static final long serialVersionUID = 1L;
private Long id;
private String name;
private String value;
private int version;
@Version
@Column(name = "version", nullable = false)
public int getVersion() {
return version;
}
@Id
@GeneratedValue(strategy = IDENTITY)
@Column(name = "id", unique = true, nullable = false)
public Long getId() {
return id;
}
@Column(name = "name", nullable = false)
public String getName() {
return name;
}
@Column(name = "value", nullable = false)
public String getValue() {
return value;
}
… ADD SETTERS WITH IDE (OR USE LOMBOK :)
}
Inject Microserver's SQL dao into our Rest resource. This is a delegate to Spring's JDBC TEMPLATE.
private final EventBus bus;
private final AtomicLong correlationProvider = new AtomicLong(0);
private final SQL dao;
@Autowired
public MyRestEndPoint(EventBus bus,final SQL dao) {
this.bus = bus;
this.dao = dao;
}
Add simple create and get Rest End points
@GET
@Produces("text/plain")
@Path("/create")
public String createEntity() {
dao.getJdbc().update("insert into t_jdbc VALUES (1,'hello','world',1)");
return "ok";
}
@GET
@Produces("application/json")
@Path("/get")
public Entity get() {
return dao.getJdbc().<Entity>queryForObject("select * from t_jdbc",new BeanPropertyRowMapper(Entity.class));
}
Start your app.
Browse to : http://localhost:8080/simple/mypath/create Response should be : ok
Browse to : http://localhost:8080/simple/mypath/get Response should be : {"id": 1,"name": "hello","value": "world","version": 1}
##Configuring Hibernate
Tell Microserver to configure Spring to use Hibernate, by adding HIBERNATE_CLASSES to springClasses.
@Microserver(springClasses = { Classes. JDBC_CLASSES, Classes. HIBERNATE_CLASSES}, properties = {
"db.connection.driver", "org.hsqldb.jdbcDriver", "db.connection.url",
"jdbc:hsqldb:mem:aname", "db.connection.username", "sa",
"db.connection.dialect", "org.hibernate.dialect.HSQLDialect",
"db.connection.ddl.auto", "create-drop" },
entityScan = "app1.simple”)
Let’s create a simple data service class. In this case we will auto-wire in Microserver's Generic Hibernate Service, that provides a range of powerful data access methods for any entity.
@Component
public class DataService {
private final GenericHibernateService<Entity, Long> dao;
@Autowired
public DataService(DAOProvider<Entity, Long> daoProvider) {
dao = daoProvider.get(Entity.class);
}
public void createEntity(String name, String value) {
dao.save(new Entity(name, value));
}
public ImmutableList<Entity> findAll(String name){
return ImmutableList.copyOf(searchByName(name));
}
private List<Entity> searchByName(String name) {
return dao.<Entity>search(new Search()
.addFilter(dao.getFilterFromExample(new Entity(name))));
}
}
And the constructors to our Entity class.
public Entity(String name2, String value2) {
this.name = name2;
this.value = value2;
}
public Entity(String name2) {
this.name = name2;
}
public Entity(){
}
We can inject this in to our REST end point and call it from there.
private final EventBus bus;
private final AtomicLong correlationProvider = new AtomicLong(0);
private final SQL dao;
private final DataService dataService;
@Autowired
public MyRestEndPoint(final EventBus bus,final SQL dao, final DataService dataService) {
this.bus = bus;
this.dao = dao;
this.dataService = dataService;
}
@GET
@Produces("text/plain")
@Path("/create-entity")
public String createEntityHibernate(@QueryParam("name") String name,@QueryParam("value") String value) {
this.dataService.createEntity(name, value);
return "ok";
}
@GET
@Produces("application/json")
@Path("/findAll")
public ImmutableList<Entity> findByName(@QueryParam("name")String name) {
return this.dataService.findAll(name);
}
Start your application
Browse to : http://localhost:8080/simple/mypath/create-entity?name=%22test%22&value=%221%22
Expected response : ok
Browse to : http://localhost:8080/simple/mypath/findAll?name=%22test%22
Expected response : [{"id": 1,"name": ""test"","value": ""1"","version": 0}]
##Capturing metrics
Microserver uses Codahale metrics and Spring to capture Metrics. Because of Spring / Jersey / Codahale integration put Codahale metrics in Service Layer classes not on the Rest resources themselves (this is due to how Spring proxies annotated classes).
Let’s annotate our DataService and track usage metrics there. Add @Timed annotation to both public methods.
@Timed
public void createEntity(String name, String value) {
dao.save(new Entity(name, value));
}
@Timed
public ImmutableList<Entity> findAll(String name){
return ImmutableList.copyOf(searchByName(name));
}
- Start your app.
- Start jconsole (type jconsole at the command line).
- Connect to your app in jconsole.
- Select Mbeans and view your metrics.
##Adding a scheduled job
Let’s create a scheduled job that calls our REST end point (just for fun, because it is a normal Spring been too) to save data via our hibernate end point.
Step 1: Create a Job class that implements ScheduledJob.
Inject our DataService into it and create a new entity with the current time when the Job is triggered.
@Component
public class Job implements ScheduledJob<Job>{
private final DataService service;
@Autowired
public Job(DataService service){
this.service = service;
}
@Override
public SystemData scheduleAndLog() {
Long time =System.currentTimeMillis();
service.createEntity("time", ""+time);
return SystemData.<String,Long>builder().errors(0).processed(1).dataMap(
HashMapBuilder.of("time", time).build() ).build();
}
}
Microserver tracks all calls to public SystemData scheduleAndLog() on Spring beans that implement ScheduledJob. It uses the SystemData object returned to populate the Active Jobs view. SystemData allows us to return information about the job just run - how many records were processed, how many errors & any interesting data points.
Step 2 : Create a Schedular class.
We’ve found it very useful to put all Scheduling work in a single class per app. Let’s trigger our job to run every second -
@Component
public class Schedular {
private final Job job;
@Autowired
public Schedular(Job job) {
this.job = job;
}
@Scheduled(fixedDelay=1000)
public void schedule(){
job.scheduleAndLog();
}
}
Let’s start our App.
Browse to : http://localhost:8080/simple/mypath/findAll?name=time
Expected Response : (Something similar to this :-)
[{"id": 1,"name": "time","value": "1424267280800","version": 0},{"id": 2,"name": "time","value": "1424267281925","version": 0},{"id": 3,"name": "time","value": "1424267282928","version": 0},{"id": 4,"name": "time","value": "1424267283931","version": 0},{"id": 5,"name": "time","value": "1424267284934","version": 0},{"id": 6,"name": "time","value": "1424267285936","version": 0},{"id": 7,"name": "time","value": "1424267286939","version": 0},{"id": 8,"name": "time","value": "1424267287941","version": 0},{"id": 9,"name": "time","value": "1424267288945","version": 0},{"id": 10,"name": "time","value": "1424267289948","version": 0},{"id": 11,"name": "time","value": "1424267290951","version": 0},{"id": 12,"name": "time","value": "1424267291954","version": 0},{"id": 13,"name": "time","value": "1424267292956","version": 0},{"id": 14,"name": "time","value": "1424267293959","version": 0},{"id": 15,"name": "time","value": "1424267294961","version": 0},{"id": 16,"name": "time","value": "1424267295964","version": 0},{"id": 17,"name": "time","value": "1424267296967","version": 0},{"id": 18,"name": "time","value": "1424267297970","version": 0},{"id": 19,"name": "time","value": "1424267298972","version": 0},{"id": 20,"name": "time","value": "1424267299975","version": 0},{"id": 21,"name": "time","value": "1424267300977","version": 0}]
Browse to : http://localhost:8080/simple/active/jobs
Expected Response :- (Something similar to this :-)
{"removed": 9,"added": 38,"active": {"id_app1.simple.Job-14": {"freeMemory": 289252096,"startedAt": 1424267318019,"startedAtFormatted": "2015.02.18 at 13:48:38 GMT","processingThread": 14,"type": "app1.simple.Job","timesExecuted": 38}},"events": 38,"recently-finished": [{"event": {"freeMemory": 321690464,"startedAt": 1424267280792,"startedAtFormatted": "2015.02.18 at 13:48:00 GMT","processingThread": 13,"type": "app1.simple.Job","timesExecuted": 1},"completed": 1424267280922,"completed-formated": "2015.02.18 at 13:48:00 GMT","time-taken": 130,"memory-change": -37086936},{"event": {"freeMemory": 327086936,"startedAt": 1424267281924,"startedAtFormatted": "2015.02.18 at 13:48:01 GMT","processingThread": 13,"type": "app1.simple.Job","timesExecuted": 2},"completed": 1424267281926,"completed-formated": "2015.02.18 at 13:48:01 GMT","time-taken": 2,"memory-change": 0},{"event": {"freeMemory": 325751648,"startedAt": 1424267282928,"startedAtFormatted": "2015.02.18 at 13:48:02 GMT","processingThread": 14,"type": "app1.simple.Job","timesExecuted": 3},"completed": 1424267282930,"completed-formated": "2015.02.18 at 13:48:02 GMT","time-taken": 2,"memory-change": 0},{"event": {"freeMemory": 325751616,"startedAt": 1424267283931,"startedAtFormatted": "2015.02.18 at 13:48:03 GMT","processingThread": 14,"type": "app1.simple.Job","timesExecuted": 4},"completed": 1424267283933,"completed-formated": "2015.02.18 at 13:48:03 GMT","time-taken": 2,"memory-change": 0},{"event": {"freeMemory": 325751552,"startedAt": 1424267284933,"startedAtFormatted": "2015.02.18 at 13:48:04 GMT","processingThread": 14,"type": "app1.simple.Job","timesExecuted": 5},"completed": 1424267284935,"completed-formated": "2015.02.18 at 13:48:04 GMT","time-taken": 2,"memory-change": -357608},{"event": {"freeMemory": 325393912,"startedAt": 1424267285936,"startedAtFormatted": "2015.02.18 at 13:48:05 GMT","processingThread": 14,"type": "app1.simple.Job","timesExecuted": 6},"completed": 1424267285938,"completed-formated": "2015.02.18 at 13:48:05 GMT","time-taken": 2,"memory-change": 0},{"event": {"freeMemory": 325393720,"startedAt": 1424267286938,"startedAtFormatted": "2015.02.18 at 13:48:06 GMT","processingThread": 14,"type": "app1.simple.Job","timesExecuted": 7},"completed": 1424267286941,"completed-formated": "2015.02.18 at 13:48:06 GMT","time-taken": 3,"memory-change": 0},{"event": {"freeMemory": 325393688,"startedAt": 1424267287941,"startedAtFormatted": "2015.02.18 at 13:48:07 GMT","processingThread": 14,"type": "app1.simple.Job","timesExecuted": 8},"completed": 1424267287944,"completed-formated": "2015.02.18 at 13:48:07 GMT","time-taken": 3,"memory-change": 0},{"event": {"freeMemory": 325393624,"startedAt": 1424267288945,"startedAtFormatted": "2015.02.18 at 13:48:08 GMT","processingThread": 14,"type": "app1.simple.Job","timesExecuted": 9},"completed": 1424267288947,"completed-formated": "2015.02.18 at 13:48:08 GMT","time-taken": 2,"memory-change": 0}]}
##NIO Request handling
If we would like to scale our Microservice to handle a massive number of simultaneous requests, we have to abandon the traditional thread-per-request model of Java web servers. Luckily, Grizzly is an NIO (Non-blocking Input Output) server, and can handle a large number of requests per thread.
Let’s add an NIO End point to our App. Async / NIO end points take the form :-
@GET
@Path("/expensive")
@Produces("text/plain")
public void expensive(@Suspended AsyncResponse asyncResponse){
}
Client code should perform the ‘expensive’ operation on their own thread pool, and return the Request thread back to Grizzly, so it can continue to respond to users. When the expensive operation is complete, client code should call asyncResponse.resume.
Another AOL open source project is SimpleReact, which makes concurrency easier. Let’s use it to do some work on different thread pool. In this case, we will load data from the db and return it to the user. (See https://www.youtube.com/watch?v=3aElhM9JG7I and https://medium.com/@johnmcclean/introducing-eagerfuturestream-lazyfuturestream-bab0529b833e and https://github.com/aol/simple-react).
###Using EagerFutureStream
@GET
@Path("/expensive")
@Produces("application/json")
public void expensiveDb(@Suspended AsyncResponse asyncResponse){
EagerFutureStream.sequentialBuilder()
.react(()-> dataService.findAll("time") )
.map(list -> JacksonUtil.serializeToJson(list))
.peek(asyncResponse::resume);
}
###Using SimpleReactStream with SimpleReact builder.
@GET
@Path("/expensive")
@Produces("application/json")
public void expensiveDb(@Suspended AsyncResponse asyncResponse){
new SimpleReact().react( ()-> dataService.findAll("time") )
.then(list -> asyncResponse.resume(JacksonUtil.serializeToJson(list)));
}
Start your application. Browse to : http://localhost:8080/simple/mypath/expensive
Expected Response : -
[{"id": 1,"name": "time","value": "1424268782733","version": 0},{"id": 2,"name": "time","value": "1424268783866","version": 0},{"id": 3,"name": "time","value": "1424268784870","version": 0},{"id": 4,"name": "time","value": "1424268785873","version": 0},{"id": 5,"name": "time","value": "1424268786875","version": 0},{"id": 6,"name": "time","value": "1424268787878","version": 0},{"id": 7,"name": "time","value": "1424268788880","version": 0},{"id": 8,"name": "time","value": "1424268789883","version": 0},{"id": 9,"name": "time","value": "1424268790886","version": 0},{"id": 10,"name": "time","value": "1424268791889","version": 0},{"id": 11,"name": "time","value": "1424268792893","version": 0}] Getting Ready for Release
##Clean up properties
Create a file application.properties, add it to the classpath (or file system in directory application launched from).
@Microserver(springClasses = { JDBC_CLASSES, HIBERNATE_CLASSES}, entityScan = "app1.simple")
public class MicroserverApp {
public static void main(String[] args){
new MicroserverApp(()->"simple").run();
}
}
application.properties file :-
db.connection.driver=org.hsqldb.jdbcDriver db.connection.url=jdbc:hsqldb:mem:aname db.connection.username=sa db.connection.dialect=org.hibernate.dialect.HSQLDialect db.connection.ddl.auto=create-drop
##Documenting our API with Swagger
Add @Api annotation to the REST end point.
@Api(value = "/mypath", description = "Resource to show stats for a box using sigar")
public class MyRestEndPoint {
}
Add @ApiOperation to each method
@Rest
@Path("/mypath")
@Api(value = "/stats", description = "Resource to show stats for a box using sigar")
public class MyRestEndPoint {
private final EventBus bus;
private final AtomicLong correlationProvider = new AtomicLong(0);
private final SQL dao;
private final DataService dataService;
@Autowired
public MyRestEndPoint(final EventBus bus,final SQL dao, final DataService dataService) {
this.bus = bus;
this.dao = dao;
this.dataService = dataService;
}
@GET
@Produces("text/plain")
@Path("/hello")
@ApiOperation(value = "Hello world", response = String.class)
public String hello(){
long correlationId = correlationProvider.incrementAndGet();
bus.post(RequestEvents.start(QueryIPRetriever.getIpAddress(),correlationId));
try{
return "world";
}finally{
bus.post(RequestEvents.finish("success",correlationId));
}
}
@GET
@Produces("text/plain")
@Path("/create")
@ApiOperation(value = "Create db entity", response = String.class)
public String createEntity() {
dao.update("insert into t_jdbc VALUES (1,'hello','world',1)");
return "ok";
}
@GET
@Produces("application/json")
@Path("/get")
@ApiOperation(value = "Query for single entity", response = Entity.class)
public Entity get() {
return dao.<Entity>queryForObject("select * from t_jdbc",new BeanPropertyRowMapper(Entity.class));
}
@GET
@Produces("text/plain")
@Path("/create-entity")
@ApiOperation(value = "Create a hibernate entity", response = String.class)
public String createEntityHibernate(@QueryParam("name") String name,@QueryParam("value") String value) {
this.dataService.createEntity(name, value);
return "ok";
}
@GET
@Produces("application/json")
@Path("/findAll")
@ApiOperation(value = "Find by name", response = Entity.class)
public ImmutableList<Entity> findByName(@QueryParam("name")String name) {
return this.dataService.findAll(name);
}
@GET
@Path("/expensive")
@Produces("application/json")
@ApiOperation(value = "Do Expensive operation", response = List.class)
public void expensiveDb(@Suspended AsyncResponse asyncResponse){
new SimpleReact().react( ()-> dataService.findAll("time") )
.then(list -> asyncResponse.resume(JacksonUtil.serializeToJson(list)));
}
}
Browse to :- http://localhost:8080/api-docs/mypath