diff --git a/README.adoc b/README.adoc new file mode 100644 index 0000000..0e00d74 --- /dev/null +++ b/README.adoc @@ -0,0 +1,22 @@ += Building a Cloud-ready Microservice + +This repository contains a link:demo-script.adoc[script] for demonstrating the construction of a REST microservice using Spring Boot, Spring Data, and Spring Cloud, and deploying the microservice to Cloud Foundry. + +The repository also contains the completed example, ready for deployment to Cloud Foundry. + +To build and deploy the completed application, you will need to: + +* install http://www.gradle.org/installation[Gradle] +* install the http://docs.cloudfoundry.org/devguide/installcf/install-go-cli.html[Cloud Foundry CLI] +* use the Cloud Foundry CLI to http://docs.cloudfoundry.org/devguide/installcf/whats-new-v6.html#login[log into your Cloud Foundry system] + +Once these prerequisites are met, you can follow these steps to build and deploy the microservice to Cloud Foundry: + +[source,bash] +---- +$ gradle assemble +$ cf create-service p-mysql 100mb-dev cities-db +$ cf push --random-route +---- + +See the link:demo-script.adoc[script] for additional information on using and testing the microservice. \ No newline at end of file diff --git a/README.md b/README.md deleted file mode 100644 index 5672851..0000000 --- a/README.md +++ /dev/null @@ -1,4 +0,0 @@ -spring-boot-cities -================== - -A simple spring-boot sample application. diff --git a/build.gradle b/build.gradle index 5a5e867..de321ad 100644 --- a/build.gradle +++ b/build.gradle @@ -5,7 +5,7 @@ buildscript { } dependencies { classpath("org.springframework.boot:spring-boot-gradle-plugin:1.0.2.RELEASE") - classpath("org.cloudfoundry:cf-gradle-plugin:1.0.2") + classpath("org.cloudfoundry:cf-gradle-plugin:1.0.3") } } @@ -19,17 +19,13 @@ repositories { maven { url "http://repo.spring.io/milestone" } } -ext { - springCloudVersion = "0.9.8" -} - dependencies { compile("org.springframework.boot:spring-boot-starter-web") compile("org.springframework.boot:spring-boot-starter-data-jpa") compile("org.springframework.boot:spring-boot-starter-actuator") - compile("org.springframework.data:spring-data-rest-webmvc") - compile("org.springframework.cloud:spring-service-connector:${springCloudVersion}") - compile("org.springframework.cloud:cloudfoundry-connector:${springCloudVersion}") + compile("org.springframework.boot:spring-boot-starter-data-rest") + compile("org.springframework.cloud:spring-cloud-spring-service-connector:1.0.0.RELEASE") + compile("org.springframework.cloud:spring-cloud-cloudfoundry-connector:1.0.0.RELEASE") runtime("org.hsqldb:hsqldb") runtime("mysql:mysql-connector-java:5.1.25") @@ -40,25 +36,20 @@ task wrapper(type: Wrapper) { gradleVersion = '1.12' } -tasks."cf-push".dependsOn assemble +tasks.cfPush.dependsOn assemble cloudfoundry { - target = "https://api.cf.scottfrederick.io" + target = "https://api.cf.mycloud.com" space = "development" - application = "cities" - file = file("${jar.archivePath}") - host = "cities" - domain = "apps.scottfrederick.io" memory = 512 - instances = 1 trustSelfSignedCerts = true services { "cities-db" { label = "p-mysql" - plan = "100mb" + plan = "100mb-dev" } } } diff --git a/demo-script.adoc b/demo-script.adoc new file mode 100644 index 0000000..34bc9df --- /dev/null +++ b/demo-script.adoc @@ -0,0 +1,802 @@ += Building a Cloud-ready Microservice + +This document is intended to be used as a script for demonstrating the construction of a database-backed REST microservice based on http://projects.spring.io/spring-boot/[Spring Boot], http://projects.spring.io/spring-data/[Spring Data], and http://projects.spring.io/spring-cloud/[Spring Cloud], and deployment of the service to http://www.cloudfoundry.org[Cloud Foundry]. It can also be used as a self-guided tutorial. + +== Prerequisites + +* http://www.gradle.org/installation[Gradle] +** can also be installed via http://brew.sh/[Homebrew] (+brew install gradle+) or http://gvmtool.net/[GVM] (+gvm install gradle+) +* An IDE or text editor +* An account on a http://www.gopivotal.com/platform-as-a-service/pivotal-cf[Pivotal CF] system with a MySQL service available +** the script can be modified for other Cloud Foundry systems or other database services as required + +== Steps + +=== Initializing the Application + +. Browse to http://start.spring.io[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:: Maven 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 generate project files for your favorite IDE by running +gradle idea+ or +gradle eclipse+. Then open the project in your editor or IDE of choice. + +. Add a runtime dependency on the http://hsqldb.org/[HyperSQL in-memory database] to +build.gradle+: ++ +[source,groovy] +---- +dependencies { + // ... + runtime("org.hsqldb:hsqldb") +} +---- + +. Create the package +org.example.cities.domain+ 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: ++ +[source,java] +---- +@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 on the class and its fields. You'll need to use your 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: ++ +[source,java] +---- +@RepositoryRestResource(collectionResourceRel = "cities", path = "cities") +public interface CityRepository extends PagingAndSortingRepository { +} +---- + +. Add JPA and REST Repository support to the +org.example.cities.Application+ class that was generated by Spring Initializr. ++ +[source,java] +---- +@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: ++ +[source,bash] +---- +$ gradle assemble +---- + +. Run the application: ++ +[source,bash] +---- +$ 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 microservice. The application's +DataSource+ is created automatically by Spring Boot using the in-memory database because no other +DataSource+ was detected in the project. ++ +[source,bash] +---- +$ 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. + +==== Importing Data + +. Add this link:src/main/resources/import.sql[import.sql file] to +src/main/resources+. This is a rather large dataset containing all of the postal codes in the United States and its territories. This file will automatically be picked up by Hibernate and imported into the in-memory database. + +. Build the application: ++ +[source,bash] +---- +$ gradle assemble +---- + +. Run the application: ++ +[source,bash] +---- +$ 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+. ++ +[source,bash] +---- +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: ++ +[source,bash] +---- +$ 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. + +==== Adding Search + +. Let's add some additional finder methods to +CityRepository+: ++ +[source,java] +---- +@RestResource(path = "name", rel = "name") +Page findByNameIgnoreCase(@Param("q") String name, Pageable pageable); + +@RestResource(path = "nameContains", rel = "nameContains") +Page findByNameContainsIgnoreCase(@Param("q") String name, Pageable pageable); + +@RestResource(path = "state", rel = "state") +Page findByStateCodeIgnoreCase(@Param("q") String stateCode, Pageable pageable); + +@RestResource(path = "postalCode", rel = "postalCode") +Page findByPostalCode(@Param("q") String postalCode, Pageable pageable); +---- + +. Build the application: ++ +[source,bash] +---- +$ gradle assemble +---- + +. Run the application: ++ +[source,bash] +---- +$ 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. ++ +[source,bash] +---- +$ 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...) +---- + +. Access the new +search+ endpoint using +curl+: ++ +[source,bash] +---- +$ 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. + +. Try a few of these endpoints. Feel free to substitute your own values for the parameters. ++ +[source,bash] +---- +$ curl -i "http://localhost:8080/cities/search/postalCode?q=75202" +$ curl -i "http://localhost:8080/cities/search/name?q=Boston" +$ curl -i "http://localhost:8080/cities/search/nameContains?q=Fort&size=1" +---- ++ +Next let's take a look at a few of the http://docs.spring.io/spring-boot/docs/current-SNAPSHOT/reference/htmlsingle/#production-ready[``production ready''] endpoints added by Spring Boot Actuator. + +==== Using Spring Boot Actuator + +Try out the following endpoints. The output is omitted 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-configuration performed as part of application bootstrapping. ++ +Searching for +DataSource+ will show the ++@Conditional++s causing the embedded DB to be created: ++ +[source,javascript] +---- +"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. + +=== Pushing to Cloud Foundry + +. Make sure you have used the http://docs.cloudfoundry.org/devguide/installcf/[+cf+ CLI] to http://docs.cloudfoundry.org/devguide/installcf/whats-new-v6.html#login[log into your Cloud Foundry service]. + +. Create an application manifest in +manifest.yml+: ++ +[source,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: ++ +[source,bash] +---- +$ cf push --random-route + +... + +1 of 1 instances running + +App started + +Showing health and status for app cities... +OK + +requested state: started +instances: 1/1 +usage: 512M x 1 instances +urls: cities-undeliverable-iatrochemistry.cf.mycloud.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: ++ +[source,bash] +---- +$ curl -i cities-undeliverable-iatrochemistry.cf.mycloud.com/cities +---- + +==== Using Spring Cloud + +. 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: ++ +[source,bash] +---- +$ cf create-service p-mysql 100mb-dev cities-db +Creating service cities-db... +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, ++ +[source,yml] +---- +--- +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: ++ +[source,bash] +---- +$ cf bind-service cities cities-db +Binding service cities-db to app cities... +OK +---- + +. Next we'll add Spring Cloud and MySQL dependencies to our Gradle build. Comment or remove the +hsqldb+ line add add the following in the +dependencies+ section: ++ +[source,groovy] +---- +dependencies { + // .... + compile("org.springframework.cloud:spring-cloud-spring-service-connector:1.0.0.RELEASE") + compile("org.springframework.cloud:spring-cloud-cloudfoundry-connector:1.0.0.RELEASE") + 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: ++ +[source,java] +---- +@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 +SPRING_PROFILES_ACTIVE+ environment variable we added earlier. You can still run the application locally (with the default profile) using the embedded database. ++ +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 ++DataSource++s. Other Hibernate native properties can be set in a similar fashion: ++ +[source,java] +---- +spring.jpa.hibernate.ddl-auto=create +---- + +. Build the application: ++ +[source,bash] +---- +$ gradle assemble +---- + +. Re-push the application: ++ +[source,bash] +---- +$ cf push +---- + +. Take a look at the +env+ endpoint again to see the service bound in +VCAP_SERVICES+: ++ +[source,bash] +---- +$ curl cities-undeliverable-iatrochemistry.cf.mycloud.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:JsF1GdLT1mN5WMDS@192.168.0.61:3306/cf_84d72bc0_1fb9_427a_b8cc_a6cd7526f3c4?reconnect=true\",\"jdbcUrl\":\"jdbc:mysql://qRouPyXXexyXRRxo:JsF1GdLT1mN5WMDS@192.168.0.61:3306/cf_8 +4d72bc0_1fb9_427a_b8cc_a6cd7526f3c4\"}}]}", +... +---- +The application is now running against a MySQL database. + +==== Customizing the +DataSource+ + +. You can customize the database connection that Spring Cloud creates with a few lines of code. Change the +dataSource+ method in +CloudDataSourceConfig+ to add some pooling and connection configuration: ++ +[source,java] +---- +@Bean +public DataSource dataSource() { + PooledServiceConnectorConfig.PoolConfig poolConfig = + new PooledServiceConnectorConfig.PoolConfig(20, 200); + + DataSourceConfig.ConnectionConfig connectionConfig = + new DataSourceConfig.ConnectionConfig("characterEncoding=UTF-8"); + DataSourceConfig serviceConfig = new DataSourceConfig(poolConfig, connectionConfig); + + return connectionFactory().dataSource("cities-db", serviceConfig); +} +---- + +. Build the application: ++ +[source,bash] +---- +$ gradle assemble +---- + +. Re-push the application: ++ +[source,bash] +---- +$ cf push +---- + +== Wrapping It Up + +You now have a fully functional REST microservice backed by a MySQL database running on Cloud Foundry, consisting of four Java classes with no boilerplate code or configuration. + +Spring Boot, Spring Data JPA, Spring Data REST, and Spring Cloud provide the framework and scaffolding, allowing you to write only domain-specific code. Production-ready endpoints are provided by Spring Boot to help manage and introspect into the application. + +Cloud Foundry provides a fast, easy, and efficient platform for deploying and scaling the microservice and connecting to a managed database. diff --git a/manifest.yml b/manifest.yml index 8a9d343..756e916 100644 --- a/manifest.yml +++ b/manifest.yml @@ -1,8 +1,11 @@ --- applications: -- name: spring-boot-cities +- name: cities memory: 512M instances: 1 - host: cities-${random-word} - domain: cfapps.io path: build/libs/spring-boot-cities.jar + timeout: 180 + services: + - cities-db + env: + SPRING_PROFILES_ACTIVE: cloud \ No newline at end of file diff --git a/src/main/java/com/example/cities/CitiesApplication.java b/src/main/java/com/example/cities/Application.java similarity index 87% rename from src/main/java/com/example/cities/CitiesApplication.java rename to src/main/java/com/example/cities/Application.java index f62623e..24ac793 100644 --- a/src/main/java/com/example/cities/CitiesApplication.java +++ b/src/main/java/com/example/cities/Application.java @@ -13,8 +13,8 @@ @EnableAutoConfiguration @EnableJpaRepositories @Import(RepositoryRestMvcConfiguration.class) -public class CitiesApplication { +public class Application { public static void main(String[] args) { - SpringApplication.run(CitiesApplication.class, args); + SpringApplication.run(Application.class, args); } } diff --git a/src/main/java/com/example/cities/config/CloudDataSourceConfig.java b/src/main/java/com/example/cities/config/CloudDataSourceConfig.java index aae6664..571d1c2 100644 --- a/src/main/java/com/example/cities/config/CloudDataSourceConfig.java +++ b/src/main/java/com/example/cities/config/CloudDataSourceConfig.java @@ -1,6 +1,8 @@ package com.example.cities.config; import org.springframework.cloud.config.java.AbstractCloudConfig; +import org.springframework.cloud.service.PooledServiceConnectorConfig; +import org.springframework.cloud.service.relational.DataSourceConfig; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Profile; @@ -12,6 +14,11 @@ public class CloudDataSourceConfig extends AbstractCloudConfig { @Bean public DataSource dataSource() { - return connectionFactory().dataSource(); + PooledServiceConnectorConfig.PoolConfig poolConfig = new PooledServiceConnectorConfig.PoolConfig(20, 200); + DataSourceConfig.ConnectionConfig connectionConfig = + new DataSourceConfig.ConnectionConfig("sessionVariables=sql_mode='ANSI';characterEncoding=UTF-8"); + DataSourceConfig serviceConfig = + new DataSourceConfig(poolConfig, connectionConfig); + return connectionFactory().dataSource("cities-db", serviceConfig); } }