-
Gradle: Install via either Homebrew (brew install gradle) or GVM.
-
(Optional) MySQL Workbench: http://dev.mysql.com/downloads/workbench/
This lab will guide you through building a basic ``Hello World'' style application. You will utilize a combination of Spring Boot, Spring Data, and Spring Cloud. You’ll also leverage a large dataset to show the power of paging, sorting, and searching with Spring Data REST.
This exercise is based on https://github.com/scottfrederick/spring-boot-cities which was created by Scott Frederick.
-
Browse to Spring Initializr and complete the form as follows:
- Group
-
org.example
- Artifact
-
cities
- Name
-
cities
- Description
-
Demo project
- Package name
-
org.example.cities
- Styles
-
Actuator, JPA, Rest Repositories, Web
- Type
-
Gradle project
Then click Generate. This will result in a file called starter.zip being downloaded to your workstation.
-
Create a directory to house your code, and into that directory unzip starter.zip.
-
Optionally run your favorite of gradle idea or gradle eclipse. Then open the project in your favorite editor or IDE of choice.
-
Add a runtime dependency on the HyperSQL in-memory database to build.gradle:
dependencies { // ... compile("org.hsqldb:hsqldb") }
-
Create the package org.example.cities.model and in that package create the class City. Into that file you can paste the following source code, which represents cities based on postal codes, global coordinates, etc:
@Entity @Table(name="city") public class City implements Serializable { private static final long serialVersionUID = 1L; @Id @GeneratedValue private long id; @Column(nullable = false) private String name; @Column(nullable = false) private String county; @Column(nullable = false) private String stateCode; @Column(nullable = false) private String postalCode; @Column private String latitude; @Column private String longitude; }
Notice that we’re using JPA annotations for everything. You’ll need to use your favorite IDE’s features to add the appropriate import statements as well as getters and setters.
-
Create the package org.example.cities.repositories and in that package create the interface CityRepository. Paste the following code and add appropriate imports:
@RepositoryRestResource(collectionResourceRel = "cities", path = "cities") public interface CityRepository extends PagingAndSortingRepository<City, Long> { }
-
Add JPA and REST Repository support to Application class.
@Configuration @ComponentScan @EnableAutoConfiguration @EnableJpaRepositories // <---- Add this @Import(RepositoryRestMvcConfiguration.class) // <---- And this public class Application { public static void main(String[] args) { SpringApplication.run(Application.class, args); } }
-
Build the application:
$ gradle assemble
-
Run the application:
$ java -jar build/libs/cities-0.0.1-SNAPSHOT.jar
-
Access the application using curl. You’ll see that the primary endpoint automatically exposes the ability to page, size, and sort the response JSON. So what have you done? Created three classes and one build file, resulting in a fully-functional REST micro-service. The application’s DataSource is created automatically by Boot using the in-memory database because no other DataSource was detected in the project.
$ curl -i localhost:8080/cities HTTP/1.1 200 OK Server: Apache-Coyote/1.1 X-Application-Context: application Content-Type: application/hal+json Transfer-Encoding: chunked Date: Tue, 27 May 2014 19:34:45 GMT { "_links" : { "self" : { "href" : "http://localhost:8080/cities{?page,size,sort}", "templated" : true } }, "page" : { "size" : 20, "totalElements" : 0, "totalPages" : 0, "number" : 0 } }
Next we’ll import some data.
-
Add https://github.com/pivotalservices/pse-training-cf-hw-module/blob/master/code/src/main/resources/import.sql to src/main/resources. This is a rather large data set containing all of the postal codes in the United States (and its territories). This file will automatically be picked up by Boot and run by Hibernate.
-
Build the application:
$ gradle assemble
-
Run the application:
$ java -jar build/libs/cities-0.0.1-SNAPSHOT.jar
-
Access the application again using curl. Notice the appropriate hypermedia is included for next, previous, and self. You can also select pages and page size by utilizing ?size=n&page=n on the URL string. Finally, you can sort the data utilizing ?sort=fieldName.
curl -i localhost:8080/cities HTTP/1.1 200 OK Server: Apache-Coyote/1.1 X-Application-Context: application Content-Type: application/hal+json Transfer-Encoding: chunked Date: Tue, 27 May 2014 19:59:58 GMT { "_links" : { "next" : { "href" : "http://localhost:8080/cities?page=1&size=20" }, "self" : { "href" : "http://localhost:8080/cities{?page,size,sort}", "templated" : true } }, "_embedded" : { "cities" : [ { "name" : "HOLTSVILLE", "county" : "SUFFOLK", "stateCode" : "NY", "postalCode" : "00501", "latitude" : "+40.922326", "longitude" : "-072.637078", "_links" : { "self" : { "href" : "http://localhost:8080/cities/1" } } }, { "name" : "HOLTSVILLE", "county" : "SUFFOLK", "stateCode" : "NY", "postalCode" : "00544", "latitude" : "+40.922326", "longitude" : "-072.637078", "_links" : { "self" : { "href" : "http://localhost:8080/cities/2" } } }, { "name" : "ADJUNTAS", "county" : "ADJUNTAS", "stateCode" : "PR", "postalCode" : "00601", "latitude" : "+18.165273", "longitude" : "-066.722583", "_links" : { "self" : { "href" : "http://localhost:8080/cities/3" } } }, { "name" : "AGUADA", "county" : "AGUADA", "stateCode" : "PR", "postalCode" : "00602", "latitude" : "+18.393103", "longitude" : "-067.180953", "_links" : { "self" : { "href" : "http://localhost:8080/cities/4" } } }, { "name" : "AGUADILLA", "county" : "AGUADILLA", "stateCode" : "PR", "postalCode" : "00603", "latitude" : "+18.455913", "longitude" : "-067.145780", "_links" : { "self" : { "href" : "http://localhost:8080/cities/5" } } }, { "name" : "AGUADILLA", "county" : "AGUADILLA", "stateCode" : "PR", "postalCode" : "00604", "latitude" : "+18.493520", "longitude" : "-067.135883", "_links" : { "self" : { "href" : "http://localhost:8080/cities/6" } } }, { "name" : "AGUADILLA", "county" : "AGUADILLA", "stateCode" : "PR", "postalCode" : "00605", "latitude" : "+18.465162", "longitude" : "-067.141486", "_links" : { "self" : { "href" : "http://localhost:8080/cities/7" } } }, { "name" : "MARICAO", "county" : "MARICAO", "stateCode" : "PR", "postalCode" : "00606", "latitude" : "+18.172947", "longitude" : "-066.944111", "_links" : { "self" : { "href" : "http://localhost:8080/cities/8" } } }, { "name" : "ANASCO", "county" : "ANASCO", "stateCode" : "PR", "postalCode" : "00610", "latitude" : "+18.288685", "longitude" : "-067.139696", "_links" : { "self" : { "href" : "http://localhost:8080/cities/9" } } }, { "name" : "ANGELES", "county" : "UTUADO", "stateCode" : "PR", "postalCode" : "00611", "latitude" : "+18.279531", "longitude" : "-066.802170", "_links" : { "self" : { "href" : "http://localhost:8080/cities/10" } } }, { "name" : "ARECIBO", "county" : "ARECIBO", "stateCode" : "PR", "postalCode" : "00612", "latitude" : "+18.450674", "longitude" : "-066.698262", "_links" : { "self" : { "href" : "http://localhost:8080/cities/11" } } }, { "name" : "ARECIBO", "county" : "ARECIBO", "stateCode" : "PR", "postalCode" : "00613", "latitude" : "+18.458093", "longitude" : "-066.732732", "_links" : { "self" : { "href" : "http://localhost:8080/cities/12" } } }, { "name" : "ARECIBO", "county" : "ARECIBO", "stateCode" : "PR", "postalCode" : "00614", "latitude" : "+18.429675", "longitude" : "-066.674506", "_links" : { "self" : { "href" : "http://localhost:8080/cities/13" } } }, { "name" : "BAJADERO", "county" : "ARECIBO", "stateCode" : "PR", "postalCode" : "00616", "latitude" : "+18.444792", "longitude" : "-066.640678", "_links" : { "self" : { "href" : "http://localhost:8080/cities/14" } } }, { "name" : "BARCELONETA", "county" : "BARCELONETA", "stateCode" : "PR", "postalCode" : "00617", "latitude" : "+18.447092", "longitude" : "-066.544255", "_links" : { "self" : { "href" : "http://localhost:8080/cities/15" } } }, { "name" : "BOQUERON", "county" : "CABO ROJO", "stateCode" : "PR", "postalCode" : "00622", "latitude" : "+17.998531", "longitude" : "-067.187318", "_links" : { "self" : { "href" : "http://localhost:8080/cities/16" } } }, { "name" : "CABO ROJO", "county" : "CABO ROJO", "stateCode" : "PR", "postalCode" : "00623", "latitude" : "+18.062201", "longitude" : "-067.149541", "_links" : { "self" : { "href" : "http://localhost:8080/cities/17" } } }, { "name" : "PENUELAS", "county" : "PENUELAS", "stateCode" : "PR", "postalCode" : "00624", "latitude" : "+18.023535", "longitude" : "-066.726156", "_links" : { "self" : { "href" : "http://localhost:8080/cities/18" } } }, { "name" : "CAMUY", "county" : "CAMUY", "stateCode" : "PR", "postalCode" : "00627", "latitude" : "+18.477891", "longitude" : "-066.854770", "_links" : { "self" : { "href" : "http://localhost:8080/cities/19" } } }, { "name" : "CASTANER", "county" : "LARES", "stateCode" : "PR", "postalCode" : "00631", "latitude" : "+18.269187", "longitude" : "-066.864993", "_links" : { "self" : { "href" : "http://localhost:8080/cities/20" } } } ] }, "page" : { "size" : 20, "totalElements" : 42741, "totalPages" : 2138, "number" : 0 } }
-
Try the following curl statements to see how the application behaves:
$ curl -i "localhost:8080/cities?size=5" $ curl -i "localhost:8080/cities?size=5&page=3" $ curl -i "localhost:8080/cities?sort=postalCode,desc"
Next we’ll add searching capabilities.
-
Let’s add some additional finder methods to CityRepository:
@RestResource(path = "name", rel = "name") Page<City> findByNameIgnoreCase(@Param("q") String name, Pageable pageable); @RestResource(path = "nameContains", rel = "nameContains") Page<City> findByNameContainsIgnoreCase(@Param("q") String name, Pageable pageable); @RestResource(path = "state", rel = "state") Page<City> findByStateCodeIgnoreCase(@Param("q") String stateCode, Pageable pageable); @RestResource(path = "postalCode", rel = "postalCode") Page<City> findByPostalCode(@Param("q") String postalCode, Pageable pageable);
-
Build the application:
$ gradle assemble
-
Run the application:
$ java -jar build/libs/cities-0.0.1-SNAPSHOT.jar
-
Access the application again using curl. Notice that hypermedia for a new search endpoint has appeared.
$ curl -i "localhost:8080/cities" HTTP/1.1 200 OK Server: Apache-Coyote/1.1 X-Application-Context: application Content-Type: application/hal+json Transfer-Encoding: chunked Date: Tue, 27 May 2014 20:33:52 GMT { "_links" : { "next" : { "href" : "http://localhost:8080/cities?page=1&size=20" }, "self" : { "href" : "http://localhost:8080/cities{?page,size,sort}", "templated" : true }, "search" : { "href" : "http://localhost:8080/cities/search" } }, (Remainder omitted...)
-
Try accessing the new search endpoint using curl:
curl -i "localhost:8080/cities/search" HTTP/1.1 200 OK Server: Apache-Coyote/1.1 X-Application-Context: application Content-Type: application/hal+json Transfer-Encoding: chunked Date: Tue, 27 May 2014 20:38:32 GMT { "_links" : { "postalCode" : { "href" : "http://localhost:8080/cities/search/postalCode{?q,page,size,sort}", "templated" : true }, "state" : { "href" : "http://localhost:8080/cities/search/state{?q,page,size,sort}", "templated" : true }, "name" : { "href" : "http://localhost:8080/cities/search/name{?q,page,size,sort}", "templated" : true }, "nameContains" : { "href" : "http://localhost:8080/cities/search/nameContains{?q,page,size,sort}", "templated" : true } } }
Note that we now have new search endpoints for each of the finders that we added!
-
Let’s try a few of these endpoints. Feel free to substitute your own values for the parameters.
$ curl -i "http://localhost:8080/cities/search/postalCode?q=38672" HTTP/1.1 200 OK Server: Apache-Coyote/1.1 X-Application-Context: application Content-Type: application/hal+json Transfer-Encoding: chunked Date: Tue, 27 May 2014 20:40:13 GMT { "_links" : { "self" : { "href" : "http://localhost:8080/cities/search/postalCode?q=38672{&page,size,sort}", "templated" : true } }, "_embedded" : { "cities" : [ { "name" : "SOUTHAVEN", "county" : "DE SOTO", "stateCode" : "MS", "postalCode" : "38672", "latitude" : "+34.947407", "longitude" : "-089.925792", "_links" : { "self" : { "href" : "http://localhost:8080/cities/17030" } } } ] }, "page" : { "size" : 20, "totalElements" : 1, "totalPages" : 1, "number" : 0 } } $ curl -i "http://localhost:8080/cities/search/name?q=Nesbit" HTTP/1.1 200 OK Server: Apache-Coyote/1.1 X-Application-Context: application Content-Type: application/hal+json Transfer-Encoding: chunked Date: Tue, 27 May 2014 20:42:01 GMT { "_links" : { "self" : { "href" : "http://localhost:8080/cities/search/name?q=Nesbit{&page,size,sort}", "templated" : true } }, "_embedded" : { "cities" : [ { "name" : "NESBIT", "county" : "DE SOTO", "stateCode" : "MS", "postalCode" : "38651", "latitude" : "+34.875133", "longitude" : "-089.985853", "_links" : { "self" : { "href" : "http://localhost:8080/cities/17015" } } } ] }, "page" : { "size" : 20, "totalElements" : 1, "totalPages" : 1, "number" : 0 } } $ curl -i "http://localhost:8080/cities/search/nameContains?q=Palm+Beach&size=1" HTTP/1.1 200 OK Server: Apache-Coyote/1.1 X-Application-Context: application Content-Type: application/hal+json Transfer-Encoding: chunked Date: Tue, 27 May 2014 20:43:48 GMT { "_links" : { "next" : { "href" : "http://localhost:8080/cities/search/nameContains?q=Palm+Beach&page=1&size=1" }, "self" : { "href" : "http://localhost:8080/cities/search/nameContains?q=Palm+Beach&size=1{&page,sort}", "templated" : true } }, "_embedded" : { "cities" : [ { "name" : "WEST PALM BEACH", "county" : "PALM BEACH", "stateCode" : "FL", "postalCode" : "33401", "latitude" : "+26.672643", "longitude" : "-080.070613", "_links" : { "self" : { "href" : "http://localhost:8080/cities/14645" } } } ] }, "page" : { "size" : 1, "totalElements" : 23, "totalPages" : 23, "number" : 0 } }
Next let’s take a look at a few of the ``production ready'' endpoints added by Spring Boot Actuator.
Try out the following endpoints. We’re not including the output here because it can be quite large:
- http://localhost:8080/beans
-
Dumps all of the beans in the Spring context.
- http://localhost:8080/autoconfig
-
Dumps all of the auto-configuraiton performed as part of application bootstrapping.
Searching for DataSource will show the @Conditionals causing the embedded DB to be created:
"DataSourceAutoConfiguration" : [ { "condition" : "OnClassCondition", "message" : "@ConditionalOnClass classes found: org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType" } ], "DataSourceAutoConfiguration.JdbcTemplateConfiguration" : [ { "condition" : "DataSourceAutoConfiguration.DatabaseCondition", "message" : "existing auto database detected" } ], "DataSourceAutoConfiguration.JdbcTemplateConfiguration#jdbcTemplate" : [ { "condition" : "OnBeanCondition", "message" : "@ConditionalOnMissingBean (types: org.springframework.jdbc.core.JdbcOperations; SearchStrategy: all) found no beans" } ], "DataSourceAutoConfiguration.JdbcTemplateConfiguration#namedParameterJdbcTemplate" : [ { "condition" : "OnBeanCondition", "message" : "@ConditionalOnMissingBean (types: org.springframework.jdbc.core.namedparam.NamedParameterJdbcOperations; SearchStrategy: all) found no beans" } ], "DataSourceAutoConfiguration.TomcatConfiguration" : [ { "condition" : "DataSourceAutoConfiguration.TomcatDatabaseCondition", "message" : "found database driver org.hsqldb.jdbcDriver" }, { "condition" : "OnBeanCondition", "message" : "@ConditionalOnMissingBean (types: javax.sql.DataSource; SearchStrategy: all) found no beans" } ], "DataSourceTransactionManagerAutoConfiguration" : [ { "condition" : "OnClassCondition", "message" : "@ConditionalOnClass classes found: org.springframework.jdbc.core.JdbcTemplate,org.springframework.transaction.PlatformTransactionManager" } ], "DataSourceTransactionManagerAutoConfiguration.TransactionManagementConfiguration" : [ { "condition" : "OnBeanCondition", "message" : "@ConditionalOnMissingBean (types: org.springframework.transaction.annotation.AbstractTransactionManagementConfiguration; SearchStrategy: all) found no beans" } ],
- http://localhost:8080/env
-
Dumps the application’s shell environment as well as all Java system properties.
- http://localhost:8080/metrics
-
Dumps all metrics currently being collected by Actuator, primarily response time and access counts for endpoints.
- http://localhost:8080/mappings
-
Dumps all URI request mappings and the controller methods to which they are mapped.
-
Create an application manifest in manifest.yml:
--- applications: - name: cities memory: 512M instances: 1 path: build/libs/cities-0.0.1-SNAPSHOT.jar timeout: 180 # to give time for the data to import
-
Push to Cloud Foundry with a random route to prevent collisions:
$ cf push --random-route ... 1 of 1 instances running App started Showing health and status for app cities in org mstine-org / space demo as admin... OK requested state: started instances: 1/1 usage: 512M x 1 instances urls: cities-undeliverable-iatrochemistry.cf.deepsouthcloud.com state since cpu memory disk #0 running 2014-05-27 04:15:05 PM 0.0% 433M of 512M 128.9M of 1G
-
Access the application at the route provided by CF:
$ curl -i cities-undeliverable-iatrochemistry.cf.deepsouthcloud.com/cities
-
At present we’re still using the in-memory database. Let’s connect to a MySQL database service provided by Cloud Foundry. First we’ll create the service instance:
$ cf create-service p-mysql 100mb-dev cities-db Creating service cities-db in org mstine-org / space demo as admin... OK
-
Next add the service to your application manifest, which will bind the service to our application on the next push. We’ll also add an environment variable to switch on the ``cloud'' profile,
--- applications: - name: cities memory: 512M instances: 1 path: build/libs/cities-0.0.1-SNAPSHOT.jar timeout: 180 services: # Add - cities-db # these env: # four SPRING_PROFILES_ACTIVE: cloud # lines!
You can also accomplish the service binding by explicitly binding the service at the command-line:
$ cf bind-service cities cities-db Binding service cities-db to app cities in org mstine-org / space demo as admin... OK
-
Next we’ll add the MySQL and Spring Cloud dependencies to our Gradle build. First, add the following just above the apply statements in build.gradle:
ext { springCloudVersion = "1.0.0.RC2" }
Next, since we’re using a release candidate for Spring Cloud, add the following to your repositories section:
maven { url "http://repo.spring.io/milestone" }
Finally, comment or remove the hsqldb line add add the following in the dependencies section:
compile("org.springframework.cloud:spring-cloud-spring-service-connector:${springCloudVersion}") compile("org.springframework.cloud:spring-cloud-cloudfoundry-connector:${springCloudVersion}") runtime("mysql:mysql-connector-java:5.1.25")
Since we’ve added new dependencies, re-run gradle idea + or +gradle eclipse to have them added to the IDE classpath.
-
Next, let’s create the package org.example.cities.config and create in that package the class CloudDataSourceConfig. Add the following code:
@Profile("cloud") @Configuration public class CloudDataSourceConfig extends AbstractCloudConfig { @Bean public DataSource dataSource() { return connectionFactory().dataSource(); } }
As before, have the IDE import the appropriate dependencies. The @Profile annotation will cause this class (which becomes Spring configuration when annotated as @Configuration) to be added to the configuration set because of the environment variable we added earlier. With this code, Spring Cloud will detect a bound service that is compatible with DataSource, read the credentials, and then create a DataSource as appropriate (it will throw an exception otherwise).
-
Add the following to src/main/resources/application.properties to cause Hibernate to create the database schema and import data at startup (this is done automatically for embedded databases, not for custom DataSources). Other Hibernate native properties can be set in a similar fashion:
spring.jpa.hibernate.ddl-auto=create
-
Build the application:
$ gradle assemble
-
Re-push the application:
$ cf push
-
Take a look at the env endpoint again to see the service bound in VCAP_SERVICES:
$ curl cities-undeliverable-iatrochemistry.cf.deepsouthcloud.com/env ... "VCAP_SERVICES" : "{\"p-mysql\":[{\"name\":\"cities-db\",\"label\":\"p-mysql\",\"tags\":[\"mysql\",\"relational\"],\"plan\":\"100mb-dev\",\"credentials\":{\"hostname\":\"192. 168.0.61\",\"port\":3306,\"name\":\"cf_84d72bc0_1fb9_427a_b8cc_a6cd7526f3c4\",\"username\":\"qRouPyXXexyXRRxo\",\"password\":\"JsF1GdLT1mN5WMDS\",\"uri\":\"mysql://qRouPyXXexyXRR xo:[email protected]:3306/cf_84d72bc0_1fb9_427a_b8cc_a6cd7526f3c4?reconnect=true\",\"jdbcUrl\":\"jdbc:mysql://qRouPyXXexyXRRxo:[email protected]:3306/cf_8 4d72bc0_1fb9_427a_b8cc_a6cd7526f3c4\"}}]}", ...
-
Optionally, you can connect to your service from MySQL Workbench and take a look at the data: