├── .gitignore ├── LICENSE ├── README.adoc ├── mvnw ├── mvnw.cmd ├── pom.xml └── src ├── main ├── java │ └── com │ │ └── example │ │ └── bootifulgcp │ │ └── BootifulGcpApplication.java └── resources │ ├── application.properties │ └── logback-spring.xml └── test └── java └── com └── example └── bootifulgcp └── BootifulGcpApplicationTests.java /.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | !.mvn/wrapper/maven-wrapper.jar 3 | 4 | ### STS ### 5 | .apt_generated 6 | .classpath 7 | .factorypath 8 | .project 9 | .settings 10 | .springBeans 11 | .sts4-cache 12 | 13 | ### IntelliJ IDEA ### 14 | .idea 15 | *.iws 16 | *.iml 17 | *.ipr 18 | 19 | ### NetBeans ### 20 | /nbproject/private/ 21 | /build/ 22 | /nbbuild/ 23 | /dist/ 24 | /nbdist/ 25 | /.nb-gradle/ -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.adoc: -------------------------------------------------------------------------------- 1 | 2 | = Spring Cloud GCP 3 | 4 | Hi Spring fans! In this installment of Spring Tips we will look at https://cloud.spring.io/spring-cloud-gcp[the Spring Cloud GCP project]. The Spring Cloud GCP project endeavors to provide a first class experience for Spring Cloud developers when using the Google Cloud Platform. 5 | 6 | == Getting Started with GCP 7 | 8 | You'll need to sign up for https://console.cloud.google.com/[a GCP account]. In order to use the Spring Cloud GCP project on your local machine, you'll want to https://cloud.google.com/pubsub/docs/quickstart-cli[setup a project on GCP and have the `gcloud` CLI installed] locally. 9 | 10 | 11 | There are a few things to know and do before starting any work with GCP. First, you'll need to login. Run the following command to login to GCP: 12 | 13 | 14 | [source,shell] 15 | ---- 16 | gcloud auth application-default login 17 | ---- 18 | 19 | This will make it easy to provide default credentials supporting work your interactions with the platform. In this case, you're saying that you want to allow certain operations on _your_ behalf. 20 | 21 | Some operations endure independent of a particular user. They might need granular permissions that represent a subset of your privilege. They might be run independent of a given user, as in a batch job or something that runs nightly. In this case, it makes sense to use a _service account_. We'll need to configure a _service account_ later when we look at tracing. 22 | 23 | It's useful to know your _Project ID_. You'll find a lot of incantations need to know this value. I have a little script like this that is run in my `~/.bashrc` and is contributed to every shell. 24 | 25 | 26 | .gcp.sh 27 | [source,shell] 28 | ---- 29 | export PROJECT_ID=$(gcloud config list --format 'value(core.project)') 30 | ---- 31 | 32 | I tend to stick to the CLI in these examples, but most everything you want to do can be done from the web console, too. 33 | 34 | == Getting Started with Spring Cloud GCP 35 | 36 | You'll need, at least for now, the Spring Cloud _and_ Spring Cloud GCP bill of materials (BOM) artifacts. We're going to use Spring Boot 2.0.3, Spring Cloud Finchley RELEASE and Spring Cloud GCP 1.1.0.BUILD-SNAPSHOT. Here's a skeletal project `pom.xml`. 37 | 38 | .pom.xml 39 | [source,xml] 40 | ---- 41 | 42 | 43 | 45 | 4.0.0 46 | 47 | com.example 48 | gcp 49 | 0.0.1-SNAPSHOT 50 | jar 51 | 52 | 53 | org.springframework.boot 54 | spring-boot-starter-parent 55 | 2.0.3.RELEASE 56 | 57 | 58 | 59 | 60 | UTF-8 61 | UTF-8 62 | 1.8 63 | 1.1.0.M1 64 | Finchley.RELEASE 65 | 66 | 67 | 68 | 69 | org.springframework.cloud 70 | spring-cloud-gcp-starter 71 | 72 | 73 | org.projectlombok 74 | lombok 75 | 76 | 77 | org.springframework.boot 78 | spring-boot-starter-test 79 | test 80 | 81 | 82 | 83 | 84 | 85 | 86 | org.springframework.cloud 87 | spring-cloud-dependencies 88 | ${spring-cloud.version} 89 | pom 90 | import 91 | 92 | 93 | org.springframework.cloud 94 | spring-cloud-gcp-dependencies 95 | ${spring-cloud-gcp.version} 96 | pom 97 | import 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | org.springframework.boot 106 | spring-boot-maven-plugin 107 | 108 | 109 | 110 | 111 | 112 | 113 | spring-snapshot 114 | Spring Snapshot 115 | https://repo.spring.io/snapshot 116 | 117 | true 118 | 119 | 120 | 121 | 122 | 123 | ---- 124 | 125 | 126 | == SQL Data and MySQL 127 | 128 | GCP offers managed SQL services including MySQL and PostgreSQL. Let's provision a MySQL instance and use that from a Spring Boot-based application. You'll need to first enable the SQL APIs in Google Cloud: 129 | 130 | ``` 131 | gcloud services enable sqladmin.googleapis.com 132 | ``` 133 | 134 | Then, you'll need to provision a new Google Cloud SQL instance in a particular region. The default is MySQL. (This could take several minutes!) 135 | 136 | ``` 137 | gcloud sql instances create reservations --region=us-central1 138 | ``` 139 | 140 | Then, provision a new database in that Google Cloud SQL instance: 141 | 142 | ``` 143 | gcloud sql databases create reservations --instance reservations 144 | ``` 145 | 146 | There are a couple of ways to establish a secured connection to this CloudSQL instance, e.g. [configure the JDBC driver wit a SSL Socket Factory] and/or using [SQL Proxy](https://cloud.google.com/sql/docs/mysql/connect-external-app#proxy). To make all the configuration easier though, you can add the GCP MySQL starter: `org.springframework.cloud` : `spring-cloud-gcp-starter-sql-mysql`. This starter, and a dash of configuration, can connect your application to the platform. 147 | 148 | What configuration, you say? Well, Spring applications have bean definitions (in Java code) and property definitions (in any number of different property sources, including `.yaml` and `.properties` files and, as we'll see later, GCP's RuntimeConfig mechanism). You can selectively activate particular bean definitions and property definitions with _profiles_. Spring profiles are arbitrary labels that identify distinct bean and property configurations that could be selectively _activated_. In this example, we're using the `mysql` profile. 149 | 150 | Let's look at the configuration for the `mysql` profile, in `src/main/resources/application-mysql.properties`: 151 | 152 | .application-mysql.properties 153 | [source,properties] 154 | ---- 155 | # <1> 156 | spring.cloud.gcp.sql.database-name=reservations 157 | spring.cloud.gcp.sql.instance-connection-name=pgtm-jlong:us-central1:reservations 158 | # <2> 159 | spring.datasource.initialization-mode=always 160 | spring.datasource.hikari.maximum-pool-size=2 161 | ---- 162 | <1> these properties identify to which GCP SQL instance and database the client should connect 163 | <2> tells Spring to initialize the schema by running `src/main/resources/schema.sql`, if present 164 | <3> there are standard Spring Boot properties that configure the Hikari connection pool. HikariCP is the default connection pool in Spring Boot 2 or later. 165 | 166 | Here is the DDL for the MySQL table. In it, we delete the table and then re-create it. 167 | 168 | [source,sql] 169 | ---- 170 | DROP TABLE IF EXISTS reservations; 171 | 172 | CREATE TABLE reservations ( 173 | id BIGINT NOT NULL AUTO_INCREMENT, 174 | name CHAR(128) NOT NULL, 175 | PRIMARY KEY (id) 176 | ); 177 | ---- 178 | 179 | In this example, we're letting Spring Boot configure the database. There are alternative approaches. We could use `gcloud sql connect $GCP_SQL_INSTANCE` where `$GCP_SQL_INSTANCE` should be replaced with your GCP SQL instance name. In this case, the SQL instance name is `reservations`. This will dump you into a MySQL session connected to the remote database in which you can interactively issue the relevant schema DDL. 180 | 181 | 182 | This is a demo, so let's install some sample data as well. You can create a file, `src/main/resources/data.sql`, and Spring will execute that on applicaton startup, or you can use `gcloud sql connect`. Either way, run the following statements. 183 | 184 | 185 | [source,sql] 186 | ---- 187 | insert into reservations(name) values('ray'); 188 | insert into reservations(name) values('josh'); 189 | ---- 190 | 191 | Now you can talk to the database from your Spring application like you would any other SQL database. 192 | 193 | [source,java] 194 | ---- 195 | package com.example.gcp.mysql; 196 | 197 | import lombok.AllArgsConstructor; 198 | import lombok.Data; 199 | import lombok.NoArgsConstructor; 200 | import org.apache.commons.logging.Log; 201 | import org.apache.commons.logging.LogFactory; 202 | import org.springframework.boot.SpringApplication; 203 | import org.springframework.boot.autoconfigure.SpringBootApplication; 204 | import org.springframework.boot.context.event.ApplicationReadyEvent; 205 | import org.springframework.context.annotation.Profile; 206 | import org.springframework.context.event.EventListener; 207 | import org.springframework.jdbc.core.JdbcTemplate; 208 | import org.springframework.jdbc.core.RowMapper; 209 | 210 | import java.util.Collection; 211 | 212 | @Profile("mysql") 213 | @SpringBootApplication 214 | public class MySqlApplication { 215 | 216 | private final Log log = LogFactory.getLog(getClass()); 217 | private final JdbcTemplate template; 218 | private final RowMapper rowMapper = 219 | (rs, rowNum) -> new Reservation(rs.getLong("id"), rs.getString("name")); 220 | 221 | MySqlApplication(JdbcTemplate template) { 222 | this.template = template; 223 | } 224 | 225 | @EventListener(ApplicationReadyEvent.class) 226 | public void ready() { 227 | Collection reservations = this.template.query("select * from reservations", this.rowMapper); 228 | reservations.forEach(reservation -> log.info("reservation: " + reservation.toString())); 229 | } 230 | 231 | public static void main(String args[]) { 232 | SpringApplication.run(MySqlApplication.class, args); 233 | } 234 | } 235 | 236 | @Data 237 | @AllArgsConstructor 238 | @NoArgsConstructor 239 | class Reservation { 240 | private Long id; 241 | private String reservationName; 242 | } 243 | 244 | ---- 245 | 246 | Run the application and confirm that the results are reflected in the output. 247 | 248 | At this point you should be on familiar footing; you can use Spring Data JPA and Spring Data REST, JOOQ, MyBatis, or any of the various JDBC-based technologies, along with Spring Boot, to work with this GCP-managed MySQL instance. 249 | 250 | == NoSQL or NewSQL Data and Google Cloud Spanner 251 | 252 | MySQL and PostgreSQL are familiar friends in an unfamiliar land, but they're not why we're here. No no. Were I you, I'd look at a platform like GCP and take from it the best bits; the parts that have no analog elsewhere. The things that separate it from the other platforms. One such distinguishing feature is https://en.wikipedia.org/wiki/Spanner_(database)[Google Spanner]. Spanner is.. something else entirely. 253 | 254 | Google first revealed Spanner when they introduced F1, a SQL database engine that the Adwords team moved to, _away_ from MySQL ("But Josh!," I hear you exclaim, "Didn't we just deploy to MySQL??"), in 2012. Spanner provides low latency reads, and to a lesser extent writes, globally. Google announced it http://static.googleusercontent.com/media/research.google.com/en//archive/spanner-osdi2012.pdf[in 2012 in a research paper] in which it called Spanner "the first system to distribute data at global scale and support externally-consistent distributed transactions." 255 | 256 | "Spanner is impressive work on one of the hardest distributed systems problems - a globally replicated database that supports externally consistent transactions within reasonable latency bounds," https://www.zdnet.com/article/google-reveals-spanner-the-database-tech-that-can-span-the-planet/[said Andy Gross], principal architect at Basho. 257 | 258 | Spanner is able to offer such a broad amount of geographic redundancy thanks to a method Google has developed of being able to give precise times to applications to let them write, read and replicate data without making mistakes. Spanner's "TrueTime" API depends upon GPS receivers and atomic clocks that have been installed in Google's datacentres to let applications get accurate time readings locally without having to sync globally. 259 | 260 | There are a number of database technologies at Google, such as [Bigtable](https://ai.google/research/pubs/pub27898) (columnar database, great for high throughput writes), and [Megastore](https://ai.google/research/pubs/pub36971) (NoSQL Database). Bigtable only supported eventually consistent replication across datacenters. According to the paper: "At least 300 applications within Google use Megastore (despite its relatively low performance) because its data model is simpler to manage than Bigtable's and because of its support for synchronous replication across datacenters." At the time, applications like GMail, Picasa, Calendar, Android Market and AppEngine relied on Megastore. 261 | 262 | Spanner was designed to be "scalable, multi-version, globally distributed, and synchronously-replicated database". Transactions are a first-class concept in Spanner driven, in part, by their absence in Bigtable. 263 | 264 | "The lack of cross-row transactions in Bigtable led to frequent complaints; Percolator 265 | was in part built to address this failing. Some authors have claimed that general two-phase commit is too expensive to support, because of the performance or availability 266 | problems that it brings. We believe it is better to have application programmers deal with performance problems due to overuse of transactions as bottlenecks arise, rather than always coding around the lack of transactions. Running two-phase commit over Paxos mitigates the availability problems." 267 | 268 | Each of the databases have their use cases. Bigtable (on GCP as [Cloud Bigtable](https://cloud.google.com/bigtable/)) is great for consistent low latency and high throughput workload. While Megastore (on GCP as [Cloud Datastore](https://cloud.google.com/datastore/)) can be used as a managed NoSQL data store with ACID transactions. Spanner (on GCP as [Cloud Spanner](https://cloud.google.com/spanner/)), is meant for horizontalliy scalable, highly available, and strongly consistent RDBMs workloads. 269 | 270 | Well alright! I'm simultaneously interested and intimidated! I _want_ Spanner, but I _don't_ want to have to rack and stack servers and synchronize GPS recivers and atomic clocks. But, _something_ tells me Google'd be happy to do that for me, so let's try it out. 271 | 272 | As before, you'll need to enable the API for Google Cloud GCP Spanner before you can use it: 273 | 274 | [source,shell] 275 | ---- 276 | gcloud services enable spanner.googleapis.com 277 | ---- 278 | 279 | Then, create a new Google Cloud Spanner instance: 280 | 281 | [source,shell] 282 | ---- 283 | gcloud spanner instances create reservations --config=regional-us-central1 \ 284 | --nodes=1 --description="Reservations for everybody" 285 | ---- 286 | 287 | Then, create the database instance: 288 | 289 | [source,shell] 290 | ---- 291 | gcloud spanner databases create reservations --instance=reservations 292 | ---- 293 | 294 | Confirm the Spanner instance is available: 295 | 296 | [source,shell] 297 | ---- 298 | gcloud spanner databases list --instance=reservations 299 | ---- 300 | 301 | Once the instance is `READY`, it's time to create the table. Here's the Spanner DDL. If this looks uncannily like SQL, that's good! It should. Put this DDL into a separate file. I've called it `schema.ddl`. 302 | 303 | .schema.ddl 304 | [source,sql] 305 | ---- 306 | CREATE TABLE reservations ( 307 | id STRING (36) NOT NULL, 308 | name STRING (255) NOT NULL 309 | ) PRIMARY KEY (id); 310 | ---- 311 | 312 | 313 | Register the schema with the database. 314 | 315 | [source,shell] 316 | ---- 317 | gcloud spanner databases ddl update reservations \ 318 | --instance=reservations --ddl="$(<./gcp/src/main/resources/db/schema.ddl )" 319 | ---- 320 | 321 | Now we can read the data from Spanner in our Spring application. The auto-configuration needs a little bit of configuration in order to talk to the right database. 322 | 323 | [source,properties] 324 | ---- 325 | spring.cloud.gcp.spanner.instance-id=reservations 326 | spring.cloud.gcp.spanner.database=reservations 327 | ---- 328 | 329 | We'll use the brand new Spring Data Spanner module that supports common Spring Data idioms when working with Spanner. Add `org.springframework.cloud` : `spring-cloud-gcp-starter-data-spanner` to your Maven build. Let's use a Spring Data repository to make short work of reading with our database. 330 | 331 | [source,java] 332 | ---- 333 | package com.example.gcp.spanner; 334 | 335 | import lombok.AllArgsConstructor; 336 | import lombok.Data; 337 | import lombok.NoArgsConstructor; 338 | import lombok.extern.slf4j.Slf4j; 339 | import org.springframework.boot.SpringApplication; 340 | import org.springframework.boot.autoconfigure.SpringBootApplication; 341 | import org.springframework.boot.context.event.ApplicationReadyEvent; 342 | import org.springframework.cloud.gcp.data.spanner.core.mapping.PrimaryKey; 343 | import org.springframework.cloud.gcp.data.spanner.core.mapping.Table; 344 | import org.springframework.context.event.EventListener; 345 | import org.springframework.data.annotation.Id; 346 | import org.springframework.data.repository.PagingAndSortingRepository; 347 | import org.springframework.data.rest.core.annotation.RepositoryRestResource; 348 | 349 | import java.util.UUID; 350 | import java.util.stream.Stream; 351 | 352 | @Slf4j 353 | @SpringBootApplication 354 | public class SpannerApplication { 355 | 356 | private final ReservationRepository reservationRepository; 357 | 358 | SpannerApplication(ReservationRepository reservationRepository) { 359 | this.reservationRepository = reservationRepository; 360 | } 361 | 362 | @EventListener(ApplicationReadyEvent.class) 363 | public void setup() { 364 | 365 | // <1> 366 | this.reservationRepository.deleteAll(); 367 | 368 | Stream 369 | .of("ray", "josh") 370 | .map(name -> new Reservation(UUID.randomUUID().toString(), name)) 371 | .forEach(this.reservationRepository::save); 372 | this.reservationRepository.findAll().forEach(r -> log.info(r.toString())); 373 | } 374 | 375 | public static void main(String args[]) { 376 | SpringApplication.run(SpannerApplication.class, args); 377 | } 378 | } 379 | 380 | // <2> 381 | @Data 382 | @AllArgsConstructor 383 | @NoArgsConstructor 384 | @Table(name = "reservations") 385 | class Reservation { 386 | 387 | @Id 388 | @PrimaryKey 389 | private String id; 390 | private String name; 391 | } 392 | 393 | // <3> 394 | @RepositoryRestResource 395 | interface ReservationRepository extends PagingAndSortingRepository { 396 | } 397 | ---- 398 | <1> We kick off the application, delete existing data and then write some new data to the database using our Spring Data Spanner-powered repository. 399 | <2> We define the Spring Data Spanner entity using custom mapping annotations, `@Table` and `@PrimaryKey`. 400 | <3> We create a Spring Data repository that is also exposed using Spring Data REST as a REST API. 401 | 402 | This example should look familiar if you've ever used Spring Data. Spring Data Spanner builds upon familiar concepts and patterns - templates, repositories, and entities - to support familiar data access patterns with a very different kind of database. 403 | 404 | == Application Integration with Google Cloud Pub/Sub 405 | 406 | Let's look at application integration with Google Cloud Pub/Sub. Google Cloud Pub/Sub supports a number of classic enterprise application integration use cases at Google scale. The https://cloud.google.com/pubsub/docs/overview[Google Cloud website for Pub/Sub] lists some: 407 | 408 | * **Balancing workloads in network clusters**. For example, a large queue of tasks can be efficiently distributed among multiple workers, such as Google Compute Engine instances. 409 | * **Implementing asynchronous workflows**. For example, an order processing application can place an order on a topic, from which it can be processed by one or more workers. 410 | * **Distributing event notifications**. For example, a service that accepts user signups can send notifications whenever a new user registers, and downstream services can subscribe to receive notifications of the event. 411 | * **Refreshing distributed caches**. For example, an application can publish invalidation events to update the IDs of objects that have changed. 412 | * **Logging to multiple systems**. For example, a Google Compute Engine instance can write logs to the monitoring system, to a database for later querying, and so on. 413 | * **Data streaming from various processes or devices**. For example, a residential sensor can stream data to backend servers hosted in the cloud. 414 | * **Reliability improvement**. For example, a single-zone Compute Engine service can operate in additional zones by subscribing to a common topic, to recover from failures in a zone or region. 415 | 416 | The flow when using Google Cloud Pub/Sub is exactly as you'd expect: a message is sent to a topic in the Pub/Sub broker (hosted in the cloud by GCP) which then persists it for you. Subscribers can either have messages pushed to it (through a webhook) or they can poll for the mesages from the broker. The subscriber receives messages from the broker and acknowledges each one. When a subscriber acknowledges a messaage it is removed from the subscriber's subscription queue. Any client that can speak HTTPS can use this service. There's no other API required. 417 | 418 | The domain model is fairly straightforward if you've ever used any other messaging system (JMS, AMQP, Apache Kafka, Kestrel): a topic is the thing to which messages are published. A subscription represents the stream of messages from a specific topic that are to be delivered to a specific client application. A topic can have multiple subscriptions. A subscription can have many subscribers. If you want to distribute different messages around to different subscribers, then all the subscribers must be subscribing to the same subscription. If you want to publish the same messages to all the subscribers, then each subscriber needs to subscribe to its own subscription. 419 | 420 | Pub/Sub delivery is at-least once. Hence, you must deal with idempotency and/or de-duplicate messages if you cannot process the same message more than once. 421 | 422 | A message stores a combination of data and (optional) attributes that are conducted by Google Cloud Pub/Sub from a publisher to a subscriber. A message attribute, which you might better understand as a _header_, is a key value pair in a message. You might have a header the describes the language of the payload. You might have a header that describes the content-type. 423 | 424 | Let's add Google Cloud Pub/Sub to an application and tie them together. 425 | 426 | As before, we need to enable the Google Cloud Pub/Sub API for use. 427 | 428 | [source,shell] 429 | ---- 430 | gcloud services enable pubsub.googleapis.com 431 | ---- 432 | 433 | You'll then need to create a new topic, `reservations`. 434 | 435 | [source,shell] 436 | ---- 437 | gcloud pubsub topics create reservations 438 | ---- 439 | 440 | The topic represents where we will send messages. We still need to create a subscription that consumes messages from that topic. The following command creates a subscription, `reservations-subscription`, to connect to the `reservations` topic. 441 | 442 | [source,shell] 443 | ---- 444 | gcloud pubsub subscriptions create reservations-subscription --topic=reservations 445 | ---- 446 | 447 | Those pieces in place, we can use them from our application. Add the Spring Cloud GCP Pub/Sub starter, `org.springframework.cloud` : `spring-cloud-gcp-starter-pubsub`, to your build. This introduces auto-configuration for the Google Cloud `PubSubTemplate`. The `PubSubTemplate` should feel familiar if you've ever used the `JmsTemplate` or `KafkaTemplate`. It's an easy-to-use client for producing and consuming messages with Google Cloud Pub/Sub. If you're just getting started with GCP Pub/Sub and messaging in general, a `*Template` object in the Spring universe is a good place to start. 448 | 449 | Let's look at a simple example that publishes a message whenever you issue HTTP `POST` calls to an HTTP endpoint running in the Spring Boot application. Then we'll setup a subscriber to consume the messages sent. 450 | 451 | [source,java] 452 | ---- 453 | package com.example.gcp.pubsub.template; 454 | 455 | import org.springframework.beans.factory.annotation.Value; 456 | import org.springframework.cloud.gcp.pubsub.core.PubSubTemplate; 457 | import org.springframework.context.annotation.Configuration; 458 | import org.springframework.web.bind.annotation.PathVariable; 459 | import org.springframework.web.bind.annotation.PostMapping; 460 | import org.springframework.web.bind.annotation.RestController; 461 | 462 | @Configuration 463 | @RestController 464 | class PublisherConfig { 465 | 466 | private final PubSubTemplate template; 467 | private final String topic; 468 | 469 | PublisherConfig(PubSubTemplate template, 470 | @Value("${reservations.topic:reservations}") String t) { 471 | this.template = template; 472 | this.topic = t; 473 | } 474 | 475 | // <1> 476 | @PostMapping("/publish/{name}") 477 | void publish(@PathVariable String name) { 478 | this.template.publish(this.topic, "Hello " + name + "!"); 479 | } 480 | } 481 | ---- 482 | <1> we use the injected `PubSubTemplate` to send a message - a String - to the configured topic. 483 | 484 | Now, let's look at a simple application that might as easily run in another node that consumes messages from the subscription linked to the topic. 485 | 486 | [source,java] 487 | ---- 488 | package com.example.gcp.pubsub.template; 489 | 490 | import lombok.extern.slf4j.Slf4j; 491 | import org.springframework.beans.factory.annotation.Value; 492 | import org.springframework.boot.context.event.ApplicationReadyEvent; 493 | import org.springframework.cloud.gcp.pubsub.core.PubSubTemplate; 494 | import org.springframework.context.annotation.Configuration; 495 | import org.springframework.context.event.EventListener; 496 | 497 | @Slf4j 498 | @Configuration 499 | class SubscriberConfig { 500 | 501 | private final PubSubTemplate template; 502 | private final String subscription; 503 | 504 | SubscriberConfig(PubSubTemplate template, 505 | @Value("${reservations.subscription:reservations-subscription}") String s) { 506 | this.template = template; 507 | this.subscription = s; 508 | } 509 | 510 | @EventListener(ApplicationReadyEvent.class) 511 | public void start() { 512 | //<1> 513 | this.template.subscribe(this.subscription, (pubsubMessage, ackReplyConsumer) -> { 514 | log.info("consumed new message: [" + pubsubMessage.getData().toStringUtf8() + "]"); 515 | ackReplyConsumer.ack(); 516 | }); 517 | } 518 | } 519 | ---- 520 | <1> Once the application is up and running we explicitly subscribe, connecting our client to the right endpoint. 521 | 522 | This example uses the `PubSubTemplate` (to great effect). It's simple, short and sweet. As integration becomes more complex, however, it becomes useful to decouple components involved in the flow of messages from one system to another. We introduce stages - links in a chain of components - through which messages must pass to arrive at downstream components. This staging allows us to write handling code that can be swapped out, indifferent to the origin or destination of a given messsage. This promotes testing, because components need onlt be written in terms of their immedaite pre- and post-conditions: a componet can say it onlt accepts Spring Framework `Message` types, and nothing else. This interface indirection is _very_ handy, especiallt as we start to tie together real world systems that may handle work at different cadences. It becomes trivial to introduce a broker to buffer work before it reaches downstream components where it may otherwise bottleneck. This approach - of isolating components involved in a messaging flow and introducing a buffer to protect downstream components - is called a _staged event driven architecture_ (SEDA), and it is more valuable now as the world moves to microservices and highly distributed systems than ever. 523 | 524 | Spring Integration is a framework that's designed to promote this indirection. It has at its heart the concept of a `MessageChannel`, which you can think of us an in-memory `Queue`; a pipe through which messages flow. On each side of the `MessageChannel` are sat components. You can imagine one component outputting messages of a certain type and sending them into this `MessageChannel`, oblivious to where it'll go. On the other end is another component that consumes messages of a certain type, utterly oblivious to the origin of any given message. Today there may be one service involved in the production of the message. Tomorrow there may be ten! The upstream and downstream components need not change. This indirection gives us a lot of possibilities. We change routing for a given message, stringing it through different services, splitting it, aggregating it, etc. We can transform other sources of data and adapt them to the messaging flow upstream (that's called an inbound adapter). We can introduce new sinks for the data, adapting the Spring Framework `Message` into the right type (that's called an _outbound adapter_). 525 | 526 | Let's look at Spring Integration and the Google Cloud Pub/Sub inbound and outbound adapters. We'll keep the same approach as before: an HTTP endpoint will publish messages which then get delivered to Google Cloud Pub/Sub. The code could run in different nodes. You'll also need the Spring Integration types on the classpath for this example to work. Add `org.springframework.boot` : `spring-boot-starter-integration` to the build. 527 | 528 | Let's look at a publisher that publishes messages whenever an HTTP POST is made. In this case, the publisher sends requests into a `MessageChannel` which then delivers it to a `PubSubMessageHandler`. Today it's going directly to Pub/Sub, but tomorrow it could go to a database, an FTP server, XMPP, Salesforce, or literallt anything else, and _then_ off to Pub/Sub. 529 | 530 | [source,java] 531 | ---- 532 | package com.example.gcp.pubsub.integration; 533 | 534 | import lombok.extern.slf4j.Slf4j; 535 | import org.springframework.beans.factory.annotation.Value; 536 | import org.springframework.cloud.gcp.pubsub.core.PubSubTemplate; 537 | import org.springframework.cloud.gcp.pubsub.integration.outbound.PubSubMessageHandler; 538 | import org.springframework.context.annotation.Bean; 539 | import org.springframework.context.annotation.Configuration; 540 | import org.springframework.integration.dsl.IntegrationFlow; 541 | import org.springframework.integration.dsl.IntegrationFlows; 542 | import org.springframework.integration.dsl.channel.MessageChannels; 543 | import org.springframework.messaging.SubscribableChannel; 544 | import org.springframework.messaging.support.MessageBuilder; 545 | import org.springframework.web.bind.annotation.PathVariable; 546 | import org.springframework.web.bind.annotation.PostMapping; 547 | import org.springframework.web.bind.annotation.RestController; 548 | 549 | @Slf4j 550 | @RestController 551 | @Configuration 552 | class PublisherConfig { 553 | 554 | private final String topic; 555 | private final PubSubTemplate template; 556 | 557 | public PublisherConfig( 558 | @Value("${reservations.topic:reservations}") String t, 559 | PubSubTemplate template) { 560 | this.topic = t; 561 | this.template = template; 562 | } 563 | 564 | @Bean 565 | IntegrationFlow publisherFlow() { 566 | return IntegrationFlows 567 | .from(this.outgoing()) // <1> 568 | .handle(this.pubSubMessageHandler()) 569 | .get(); 570 | } 571 | 572 | @PostMapping("/publish/{name}") 573 | void publish(@PathVariable String name) { 574 | // <2> 575 | outgoing().send(MessageBuilder.withPayload(name).build()); 576 | } 577 | 578 | @Bean 579 | SubscribableChannel outgoing() { 580 | return MessageChannels.direct().get(); 581 | } 582 | 583 | @Bean 584 | PubSubMessageHandler pubSubMessageHandler() { 585 | return new PubSubMessageHandler(template, this.topic); 586 | } 587 | } 588 | ---- 589 | <1> the `IntegrationFlow` describes, well, the _flow_ of messages in an integration. Messages sent into the `outgoing` `MessageChannel` are delivered to the `PubSubMessageHandler` which then writes it to Google Cloud Pub/Sub using the specified `topic` 590 | <2> In the Spring MVC HTTP endpint we obtain a reference to the `MessageChannel` and publish a message (which we build with the `MessageBuilder`) into it. NB: calling `outgoing()` as I do in this example is fine because Spring memoizes the result of the method invocation; I'll always obtain the same pre-instantiated singleto of the `MessageChannel` bean. 591 | 592 | 593 | On the consumer side, we do the same thing in reverse, adapting incoming messages and then logging them in an `IntegrationFlow`. 594 | 595 | [source,java] 596 | ---- 597 | package com.example.gcp.pubsub.integration; 598 | 599 | import com.google.cloud.pubsub.v1.AckReplyConsumer; 600 | import lombok.extern.slf4j.Slf4j; 601 | import org.springframework.beans.factory.annotation.Value; 602 | import org.springframework.cloud.gcp.pubsub.core.PubSubTemplate; 603 | import org.springframework.cloud.gcp.pubsub.integration.AckMode; 604 | import org.springframework.cloud.gcp.pubsub.integration.inbound.PubSubInboundChannelAdapter; 605 | import org.springframework.cloud.gcp.pubsub.support.GcpPubSubHeaders; 606 | import org.springframework.context.annotation.Bean; 607 | import org.springframework.context.annotation.Configuration; 608 | import org.springframework.integration.dsl.IntegrationFlow; 609 | import org.springframework.integration.dsl.IntegrationFlows; 610 | import org.springframework.integration.dsl.channel.MessageChannels; 611 | import org.springframework.messaging.MessageChannel; 612 | 613 | @Slf4j 614 | @Configuration 615 | class SubscriberConfig { 616 | 617 | private final String subscription; 618 | private final PubSubTemplate template; 619 | 620 | SubscriberConfig( 621 | @Value("${reservations.subscription:reservations-subscription}") String s, 622 | PubSubTemplate t) { 623 | this.subscription = s; 624 | this.template = t; 625 | } 626 | 627 | @Bean // <1> 628 | public PubSubInboundChannelAdapter messageChannelAdapter() { 629 | PubSubInboundChannelAdapter adapter = new PubSubInboundChannelAdapter( 630 | template, this.subscription); 631 | adapter.setOutputChannel(this.incoming()); 632 | adapter.setAckMode(AckMode.MANUAL); 633 | return adapter; 634 | } 635 | 636 | @Bean 637 | MessageChannel incoming() { 638 | return MessageChannels.publishSubscribe().get(); 639 | } 640 | 641 | @Bean 642 | IntegrationFlow subscriberFlow() { 643 | return IntegrationFlows 644 | .from(this.incoming()) //<2> 645 | .handle(message -> { // <3> 646 | log.info("consumed new message: [" + message.getPayload() + "]"); 647 | AckReplyConsumer consumer = message.getHeaders() 648 | .get(GcpPubSubHeaders.ACKNOWLEDGEMENT, AckReplyConsumer.class); 649 | consumer.ack(); 650 | }) 651 | .get(); 652 | } 653 | } 654 | ---- 655 | <1> the `PubSubInboundChannelAdapter` adapts messages from the subscription and sends them into the `incoming` `MessageChannel`. 656 | <2> the `IntegrationFlow` takes incoming messages and routes them to a `MessageHandler` (which we've contributed with lambda syntax) that a) logs the incoming message and b) manually acknowledges the receipt of the message. 657 | 658 | The nice thing about `IntegrationFlow` in both examples is that you can chain calls together. Here, we specify only where a message comes from (`.from()`) and what handles it (`.handle()`), but we could as easily after the `.handle()` call also route, split, transform, etc., the messages. The message sent as the output of one component (the adapters, the message handlers, transformers, etc.) become the input to any downstream components. 659 | 660 | 661 | == Google Cloud Runtime Config 662 | 663 | So far we've looked at some simple examples with all but the most trivial of configuration. Where there was configuration, we specified it in `application.properties`. This approach works but there are limitations. What about centrality (making a single config value accessible to a number of other clients), security (storing secrets securely), live reconfiguration, and auditing and journaling? There are a number of other solutions out there that address some or all of these use cases including Apache Zookeeper, Hashicorp Consul, Hashicorp Vault (for secrets management, specifically), and - of course - the Spring Cloud Config Server. All fine choices, but you'd better have a recipe for scaling out and securing these pieces of infrastructure. GCP offers an alternative, Google Cloud RuntimeConfig, that you can use with no change to existing code, thanks to the power of Spring's abstractions. 664 | 665 | Let's look at how to establish a configuration value and then reference that value from our application. We'll also look at how to later update that configuration live, without restarting the application. 666 | 667 | First, we'll need to enable this API. 668 | 669 | [source,shell] 670 | ---- 671 | gcloud services enable runtimeconfig.googleapis.com 672 | ---- 673 | 674 | Let's think through how we want to use this configuration. We'll probably have configuration values that make sense when running the application on our local machines. Values that we can source from a built-in `application.properties` or `application.yaml`. These are the default values that apply to the application. There are going to be some values that are visible only in production -locators, credentials, etc. - that are unique to production. These values might be visible when running under the `cloud` profile, for example. We're going to source those values - when running under the `cloud` profile - from Google Cloud Runtime Config. This way we can selectively override important values. 675 | 676 | We must first create a runtime configuration, and then add a variable value tot hat configuration. 677 | 678 | [source,shell] 679 | ---- 680 | gcloud beta runtime-config configs create reservations_cloud 681 | ---- 682 | 683 | Then, register a variable (`greeting`) and that variable's value (`Hello GCP`) in the just-created runtime config. 684 | 685 | [source,shell] 686 | ---- 687 | gcloud beta runtime-config configs variables set greeting "Hello GCP" --config-name reservations_cloud 688 | ---- 689 | 690 | We can enumerate all the configuration for a given config set like this: 691 | 692 | [source,shell] 693 | ---- 694 | gcloud beta runtime-config configs variables list --config-name=reservations_cloud 695 | ---- 696 | 697 | Spring Cloud GCP will need to do its work _before_ most of the Spring application is running since it is a property source that feeds values into other configuration. Thus, any configuration that it requires to do its work must be accessible earlier than the configuration in the usual suspects like `application.properties`. It is a convention in Spring Cloud that such configuration live in `bootstrap.properties`. Let's disable Spring Cloud GCP Runtime Config when running on the local machine, without any particular Spring profile active. 698 | 699 | .src/main/resources/bootstrap.properties 700 | [source,properties] 701 | ---- 702 | spring.cloud.gcp.config.enabled=false 703 | ---- 704 | 705 | When we're running in production, in, say, Cloud Foundry, we'll want to activate the `cloud` profile, at which point the Spring Cloud GCP Runtime Config client will kick in and source config from GCP. Spring Boot is smart about loading any profile-specific configuration in adition to default configuration. You need only suffix your configuration file with `-${YOUR_PROFILE}`: e.g.: `application-foo.properties`, or `bootstrap-bar.yml` for Spring profiles `foo` and `bar` respectively. Let's configure Spring Cloud GCP for when the `cloud` profile is active. 706 | 707 | .src/main/resources/bootstrap-cloud.properties 708 | [source,java] 709 | ---- 710 | spring.cloud.gcp.config.enabled=true 711 | spring.cloud.gcp.config.name=reservations 712 | spring.cloud.gcp.config.profile=cloud 713 | ---- 714 | 715 | NOTE: The combination of `${spring.cloud.gcp.config.name}_${spring.cloud.gcp.config.profile}` forms `reservations_cloud`, which is the name of the Runtime Configuration we've just created. 716 | 717 | We will configure some overall properties that will serve as the defaults, absent any overrides, in `src/main/resources/application.properties`. 718 | 719 | .src/main/resources/application.properties 720 | [source,properties] 721 | ---- 722 | # <1> 723 | management.endpoint.health.show-details=always 724 | management.endpoints.web.exposure.include=* 725 | # <2> 726 | greeting = Hello ${user.name} (running on ${os.name} ${os.version})! 727 | ---- 728 | <1> we want to, for this DEMO, expose all the Actuator endpoints to be able to interrogate them. In any other context, configure security. 729 | <2> we want to include all the Actuator endpoints 730 | 731 | Let's turn now to the Java code. You'll need to add the following dependencies to your build: `org.springframework.boot` : `spring-boot-starter-web`, `org.springframework.boot` : `spring-boot-starter-actuator`, `org.springframework.cloud` : `spring-cloud-gcp-starter-config`. We add the Spring Cloud GCP dependency to get the correct configuration for the Runtime Config support. We add Spring Boot Actuator so we have access to a few operational endpoints, `/actuator/env` and `/actuator/refresh`. 732 | 733 | Let's see some code! 734 | 735 | [source,java] 736 | ---- 737 | package com.example.gcp.runtimeconfig; 738 | 739 | import org.springframework.beans.factory.annotation.Value; 740 | import org.springframework.boot.SpringApplication; 741 | import org.springframework.boot.autoconfigure.SpringBootApplication; 742 | import org.springframework.cloud.context.config.annotation.RefreshScope; 743 | import org.springframework.web.bind.annotation.GetMapping; 744 | import org.springframework.web.bind.annotation.RestController; 745 | 746 | @SpringBootApplication 747 | public class RuntimeConfigApplication { 748 | 749 | @RefreshScope // <1> 750 | @RestController 751 | public static class GreetingsRestController { 752 | 753 | private final String greetings; 754 | 755 | // <2> 756 | GreetingsRestController(@Value("${greeting}") String greetings) { 757 | this.greetings = greetings; 758 | } 759 | 760 | @GetMapping("/greeting") 761 | String greetings() { 762 | return this.greetings; 763 | } 764 | } 765 | 766 | public static void main(String[] args) { 767 | SpringApplication.run(RuntimeConfigApplication.class, args); 768 | } 769 | } 770 | ---- 771 | <1> this annotation supports revising and refreshing the configuration for this bean. We can trigger a refresh event and observe updated configuration in the bean 772 | <2> we're injecting the key from the property file or from GCP Runtime Config. Code-wise, it's exactly the same. 773 | 774 | Run this program with no profile active and you should see something like `Hello jlong!` when you hit the endpoint at `http://localhost:8080/greeting`.Hit this environment Actuator endpoint (`http://localhost:8080/actuator/env`) and you will find no mention of our GCP Runtime Config configuration. Now, run the program with the `cloud` profile active and hit the `/greeting` endpoint again and you'll see something like `Hello GCP` reflected in the console output. Hit the `/actuator/env` endpoint and you'll see an entry for `bootstrapProperties:spring-cloud-gcp` containing our Runtime Config values. 775 | 776 | TIP: you can change the active profile by specifying `-Dspring.profiles.active=foo,bar` for profiles `foo` and `bar` when running the application. 777 | 778 | I like our application so far, but the greeting sounds so stiff! I'd love to change it, but don't want to stop and start each application instance. Here we can take advantage of the `/actuator/refresh` endpoint to _refresh_ our node's configuration after updating the value in the Runtime Config configuration. Let's change the value to something less formal, like `Hi, GCP`. 779 | 780 | [source,shell] 781 | ---- 782 | gcloud beta runtime-config configs variables set greeting "Hi, GCP" --config-name reservations_cloud 783 | ---- 784 | 785 | The configuration has been changed in the GCP Runtime Config, but that change isn't visible, at least not by default, to our application. We need to force the Spring Boot to refresh its local configuration, drawing the configuration from the Runtime Config service. Issue an (empty) HTTP POST command to the `/actuator/refresh` endpoint and then hit the `/greeting` endpoint to see the updated value. 786 | 787 | [source,shell] 788 | ---- 789 | # <1> 790 | curl http://localhost:8080/greeting 791 | > Hello GCP 792 | 793 | # <2> 794 | gcloud beta runtime-config configs variables set greeting "Hi GCP" --config-name reservations_cloud 795 | curl -H"content-type: application/json" -d{} http://localhost:8080/actuator/refresh 796 | curl http://localhost:8080/greeting 797 | > Hi GCP 798 | ---- 799 | <1> inspect the old value 800 | <2> change the value and then force the client to refresh its configuration. You'll be able to confirm the update. 801 | 802 | 803 | == Tracing with Google Cloud Stack Driver 804 | 805 | As we move more and more applications to the cloud, and introduce more and more microservices, the complexity of understanding what's gone wrong - and _where??_ - grows. Distributed tracing addresses this problem. Distributed tracing, in theory, is a simple chore. For every request that enters or exits the system.. for every ingres or egress int he system, attach a UUID if one isnt already present and if it is present then propagate it. Unfortunately, this sort of logic is tedious and hard to get right as requests move from one node to another, synchronously and asynchrously, across thread and network boundaries. Spring Cloud Sleuth addresses this problem and provides an SPI into which backend distributed tracing systems, like OpenZipkin and Google Cloud Stack Driver, can plugin. 806 | 807 | As with all GCP APIs, we must first enable this one. 808 | 809 | [source,shell] 810 | ---- 811 | gcloud services enable cloudtrace.googleapis.com 812 | ---- 813 | 814 | We're going to setup a trivial REST API and a trivial REST client, and use the Spring Cloud GCP Stack Driver support to make short work of tracing those interactions. 815 | 816 | Let's first look at our trivial REST API. Start a new project (using the skeletal `pom.xml` from above) and add `org.springframework.boot` : `spring-boot-starter-web` and `org.springframework.cloud` : `spring-cloud-gcp-starter-trace`. Our REST API (well, endpoint, anyway) will return a "greetings, _a name here_!" whenever `http://localhost:8080/greeting/{id}}` is invoked. Here's the code for the service, first: 817 | 818 | [source,java] 819 | ---- 820 | package com.example.gcp.trace; 821 | 822 | import org.springframework.boot.SpringApplication; 823 | import org.springframework.boot.autoconfigure.SpringBootApplication; 824 | import org.springframework.web.bind.annotation.GetMapping; 825 | import org.springframework.web.bind.annotation.PathVariable; 826 | import org.springframework.web.bind.annotation.RestController; 827 | 828 | @RestController 829 | @SpringBootApplication 830 | public class TraceServiceApplication { 831 | 832 | @GetMapping("/greeting/{id}") 833 | String greet(@PathVariable String id) { 834 | return "greetings, " + id + "!"; 835 | } 836 | 837 | public static void main(String args[]) { 838 | SpringApplication.run(TraceServiceApplication.class, args); 839 | } 840 | } 841 | ---- 842 | 843 | The configuration is arguably more interesting. 844 | 845 | [source,properties] 846 | ---- 847 | # <1> 848 | spring.cloud.gcp.trace.enabled=true 849 | 850 | # <2> 851 | spring.sleuth.sampler.probability=1 852 | spring.sleuth.web.skipPattern=(^cleanup.*|.+favicon.*) 853 | 854 | # <3> 855 | server.port=8081 856 | 857 | # <4> 858 | spring.application.name=trace-service 859 | ---- 860 | <1> we are opting-in to the trace support for Spring Cloud GCP. You could disable it when running the code on localhost but enable it in production with this flag. 861 | <2> these properties tell Spring Cloud Sleuth to trace everything (a "probability" of 1.0 means 100% of all observed requests will be sampled and traced). 862 | <3> if you're running this demo on the same machine then you'll want to avoid port conflicts in the client 863 | <4> `spring.application.name` is our application's logical name and it can be used in distinguishing it from other applications in trace trees, service registries, etc. 864 | 865 | The client lobs a hundred HTTP requests when the application starts up. The `RestTemplate` it uses has been post-processed by the Spring Cloud Sleuth auto-configuration to intercept and trace all HTTP calls. 866 | 867 | [source,java] 868 | ---- 869 | package com.example.gcp.trace; 870 | 871 | import lombok.extern.slf4j.Slf4j; 872 | import org.springframework.boot.SpringApplication; 873 | import org.springframework.boot.autoconfigure.SpringBootApplication; 874 | import org.springframework.boot.context.event.ApplicationReadyEvent; 875 | import org.springframework.cloud.sleuth.annotation.NewSpan; 876 | import org.springframework.context.annotation.Bean; 877 | import org.springframework.context.event.EventListener; 878 | import org.springframework.stereotype.Component; 879 | import org.springframework.web.client.RestTemplate; 880 | 881 | import java.util.stream.IntStream; 882 | 883 | @Slf4j 884 | @SpringBootApplication 885 | public class TraceClientApplication { 886 | 887 | @Component 888 | public static class Client { 889 | 890 | private final RestTemplate restTemplate; 891 | 892 | public Client(RestTemplate restTemplate) { 893 | this.restTemplate = restTemplate; 894 | } 895 | 896 | @EventListener(ApplicationReadyEvent.class) 897 | @NewSpan("client") // <1> 898 | public void before() { 899 | IntStream 900 | .range(0, 100) 901 | .mapToObj(i -> 902 | restTemplate 903 | .getForEntity("http://localhost:8081/greeting/{id}", String.class, i) 904 | .getBody()) 905 | .forEach(response -> log.info("result: " + response)); 906 | } 907 | } 908 | 909 | @Bean 910 | RestTemplate restTemplate() { 911 | return new RestTemplate(); 912 | } 913 | 914 | public static void main(String args[]) { 915 | SpringApplication.run(TraceClientApplication.class, args); 916 | } 917 | } 918 | ---- 919 | <1> the client is a straightforward use of `RestTemplate` to connect to our service. If we wanted to send 100 requests with no shared parent span, we wouldn't need `@NewSpan`. If we'd had 100 requests arrive from the outside and hit an HTTP endpoint in the client and that endpoint then resulted in 100 requests going to the service, we'd have a shared overarching span. A single trace with multiple spans. 920 | 921 | 922 | And the configuration for this node is virtually identical to that of the service. 923 | 924 | [source,properties] 925 | ---- 926 | # <1> 927 | spring.cloud.gcp.trace.enabled=true 928 | # <2> 929 | spring.sleuth.sampler.probability=1 930 | spring.sleuth.web.skipPattern=(^cleanup.*|.+favicon.*) 931 | # <3> 932 | spring.application.name=trace-client 933 | # <4> 934 | server.port=8080 935 | ---- 936 | <1> enable Spring Cloud GCP tracing.. 937 | <2> ensure that all requests are traced 938 | <3> give our client a logical name 939 | <4> and start on a different port than the service 940 | 941 | In order to see this in action, you'll need to start the service, then the client, and then make your way over to the https://console.cloud.google.com/[Google Cloud Console]. Click on the "Hamburger" menu on the left hand side of the screen and click on STACKDRIVER -> TRACE. There you'll be given the ability to inspect the requests that just flew through your services. 942 | 943 | .Looking at trace information in the Google Cloud Console 944 | image::images/stackdriver-trace.png[] 945 | 946 | Stackdriver is the umbrella name for a host of services including monitoring, tracing, and - and this is _so wicked cool!_ - live debugging of running applications. You could easily spend a lot more time - you _should_! - in this section of the console. Suffice it to say that Google is _gets_ observability and that's reflected in their services. 947 | 948 | == Using Other Google Cloud APIs 949 | 950 | The Spring Cloud GCP project strives to provide integrations with Spring and some of the GCP services that map well to Spring. But GCP is _vast_! There are a good deal many other services out there that you can consume via https://github.com/GoogleCloudPlatform/google-cloud-java/[their direct Java SDK] or even through their REST APIs, directly. Spring Cloud GCP can make working with those APIs a bit easier, too! In this section, we're going to integrate with the Google Cloud Vision API which supports analyzing images and doing feature detection. 951 | 952 | As always, you will need to enable the API: 953 | 954 | [source,shell] 955 | ---- 956 | gcloud services enable vision.googleapis.com 957 | ---- 958 | 959 | When you use the auto-configurations in Spring Cloud GCP they conveniently obtain the required OAuth scopes to work with a given API on your behalf, and you never need to worry about it. We'll need to do this work ourselves for other services. This is easy enough, thankfully. Use the `spring.cloud.gcp.credentials.scopes` property to obtain a general, platform-wide, catch-all scope that can be used to request permission for all basic Google Cloud Platform APIs. 960 | 961 | .src/main/resources/applications.properties 962 | [source,java] 963 | ---- 964 | spring.cloud.gcp.credentials.scopes=https://www.googleapis.com/auth/cloud-platform 965 | ---- 966 | 967 | And.. that's it! Now you can use the API as you like. Let's standup a simple REST API to which you can post an image as a multipart file upload and have the Google Cloud Vision API do feature detection. 968 | 969 | [source,java] 970 | ---- 971 | package com.example.gcp.vision; 972 | 973 | import com.google.api.gax.core.CredentialsProvider; 974 | import com.google.cloud.vision.v1.*; 975 | import com.google.protobuf.ByteString; 976 | import lombok.extern.slf4j.Slf4j; 977 | import org.springframework.boot.SpringApplication; 978 | import org.springframework.boot.autoconfigure.SpringBootApplication; 979 | import org.springframework.context.annotation.Bean; 980 | import org.springframework.web.bind.annotation.PostMapping; 981 | import org.springframework.web.bind.annotation.RequestParam; 982 | import org.springframework.web.bind.annotation.RestController; 983 | import org.springframework.web.multipart.MultipartFile; 984 | 985 | import java.io.IOException; 986 | import java.util.Collections; 987 | 988 | @SpringBootApplication 989 | public class VisionApplication { 990 | 991 | // <1> 992 | @Bean 993 | ImageAnnotatorClient imageAnnotatorClient( 994 | CredentialsProvider credentialsProvider) throws IOException { 995 | ImageAnnotatorSettings settings = ImageAnnotatorSettings 996 | .newBuilder() 997 | .setCredentialsProvider(credentialsProvider) 998 | .build(); 999 | return ImageAnnotatorClient.create(settings); 1000 | } 1001 | 1002 | @Slf4j 1003 | @RestController 1004 | public static class ImageAnalyzerRestController { 1005 | 1006 | private final ImageAnnotatorClient client; 1007 | 1008 | private final Feature feature = Feature 1009 | .newBuilder() 1010 | .setType(Feature.Type.LABEL_DETECTION) // <2> 1011 | .build(); 1012 | 1013 | ImageAnalyzerRestController(ImageAnnotatorClient client) { 1014 | this.client = client; 1015 | } 1016 | 1017 | @PostMapping("/analyze") 1018 | String analyze(@RequestParam MultipartFile image) throws IOException { 1019 | // <3> 1020 | byte[] data = image.getBytes(); 1021 | ByteString imgBytes = ByteString.copyFrom(data); 1022 | Image img = Image.newBuilder().setContent(imgBytes).build(); 1023 | AnnotateImageRequest request = AnnotateImageRequest 1024 | .newBuilder() 1025 | .addFeatures(this.feature) 1026 | .setImage(img) 1027 | .build(); 1028 | BatchAnnotateImagesResponse responses = this.client 1029 | .batchAnnotateImages(Collections.singletonList(request)); 1030 | AnnotateImageResponse reply = responses.getResponses(0); 1031 | return reply.toString(); 1032 | } 1033 | } 1034 | 1035 | public static void main(String args[]) { 1036 | SpringApplication.run(VisionApplication.class, args); 1037 | } 1038 | } 1039 | ---- 1040 | <1> we're configuring the Google Cloud Vision client manually. This is more work than you might do if you had a Spring Boot starter, but it's definitely not bad! 1041 | <2> what kind of analysis do we want the client to do? 1042 | <3> Spring MVC can turn multipart file uploads into a `MultipartFile` from which we can easily extract bytes to feed into this API. 1043 | 1044 | You can POST an image to this endpoint using `curl` or any other general purpose HTTP client. Here's how it would work with `curl`: 1045 | 1046 | [source,shell] 1047 | ---- 1048 | curl -F "image=@/home/jlong/Desktop/soup.jpg" http://localhost:8080/analyze 1049 | ---- 1050 | 1051 | There are a _zillion_ other APIs with whom you might work! Here we are only just beginning to scratch the surface of what's possible. Check https://cloud.google.com/products/[out this service catalog]! There are things like Google Cloud DataStore, Google Storage, Firebase, BigQuery, Apigee, video streaming services, IoT services, machine learning, Google Tensorflow, Google Dataflow, Google Cloud AutoML, Cloud Natural Language, Cloud Speech-to-Text, Cloud Text-to-Speech, Genomics APIs, Video Intelligence, and _so_ much more. 1052 | 1053 | == To Production 1054 | 1055 | As we've worked through these examples we've relied on the default authentication of the Google Cloud SDKs and Spring Cloud GCP working with a local installation of the Google Cloud SDK and the `gcloud` CLI. Spring Cloud GCP's auto-configuration configures a `DefaultCredentialsProvider` that looks for _some_ way to authenticate. Everything's worked fine on our local machine so far because we ran the interactive authentication prompt and confirmed on the Google Cloud website the linking of our authorities to this application. But what if you want to run tests in a CI environment or deploy the application to a cloud platform? 1056 | 1057 | We need a way to convey our authorization when running in these environments. We can do this on Google Cloud using _service accounts_. The https://cloud.google.com/docs/authentication/production#auth-cloud-implicit-java[process is explained here]. The gist is that you will need to describe, as narrowly as possible, the privileges your application can have using a Google Cloud service account and then render those credentials into a file which we will feed to the application. 1058 | 1059 | [source,shell] 1060 | ---- 1061 | 1062 | PROJECT_ID=$(gcloud config list --format 'value(core.project)') 1063 | 1064 | # <1> 1065 | NAME=gcp-service-app 1066 | 1067 | # <2> 1068 | gcloud iam service-accounts create $NAME 1069 | 1070 | # <3> 1071 | gcloud projects add-iam-policy-binding $PROJECT_ID --member "serviceAccount:${NAME}@${PROJECT_ID}.iam.gserviceaccount.com" --role "roles/owner" 1072 | 1073 | # <4> 1074 | gcloud iam service-accounts keys create ${NAME}.json --iam-account ${NAME}@${PROJECT_ID}.iam.gserviceaccount.com 1075 | 1076 | ---- 1077 | <1> the name is arbitrary. You might use a name that reflects the nature of your application. 1078 | <2> create the service account giving it a name 1079 | <3> add a role - `roles/owner` - to our service binding. You would do well to be more granular in assignment of your roles. 1080 | <4> generate a file, `gcp-service-app.json`, that contains the key. 1081 | 1082 | The result of this process should be a `.json` file, `gcp-service-app.json`, on your local machine. 1083 | 1084 | WARNING: That `.json` file is a secret! Do _not_ share or lose track of it! 1085 | 1086 | You need to tell the Spring Cloud application where to find this credential. You can pass it to Spring Cloud GCP as a file location or a Base 64-encoded `String`. Let's do the latter. We can use the `spring.cloud.gcp.credentials.encoded-key` to convey the Base64-encoded contents of the file we just generated. 1087 | 1088 | This is a secret value, and it will differ from one environment to another. We don't want it checked into version control, in `src/main/resources/application.properties`, and even if we did we'd still need to maintain different environment-specific values. Spring Boot lets us provide overrides when running the application. We could use `application.properties` or `application.yml` files. We could stash thm in environment-specific Runtime Config. We could provide them as `-D` arguments or environment variables when running the application. So could easily say `java -Dspring.cloud.gcp.credentials.encoded-key=... -jar ..` to provide a value that either contributes a new value to the application or overrides the existing value in `src/main/resources/application.properties`. 1089 | 1090 | Environment variables are a more natural fit for platforms like Cloud Foundry or Heroku where we don't necessarily want to have control over _how_ an application is run, over its `java` incantation. The platform Buildpack does that for us. Let's look at how we could run the application locally. 1091 | 1092 | .run.sh 1093 | [source,shell] 1094 | ---- 1095 | #!/bin/bash 1096 | 1097 | # <1> 1098 | export SPRING_CLOUD_GCP_CREDENTIALS_ENCODED_KEY=$( cat $GCP_SERVICE_ACCOUNT_KEY_FILE | base64 -w0 ) 1099 | export SPRING_CLOUD_GCP_PROJECT_ID=$(gcloud config list --format 'value(core.project)') 1100 | 1101 | mvn clean spring-boot:run 1102 | ---- 1103 | <1> when you run this script, set the environment variable `SPRING_CLOUD_GCP_CREDENTIALS_ENCODED_KEY` before running the application to point to the location of your service account `.json` file. 1104 | 1105 | On my machine, I was able to run that script like this: `GCP_SERVICE_ACCOUNT_KEY_FILE=/home/jlong/keys/gcp-service-account.json ./run.sh`. Your local path for the `.json` file will most likely vary. 1106 | 1107 | You can translate those environment variables as needed for your particular CI environment. Once your application has been tested and integrated, it's off to production! For me, production is Cloud Foundry (running on top of Google Cloud, no less). Here's a script that deploys the application to the Cloud Foundry instance in which I'm authenticated. 1108 | 1109 | .deploy.sh 1110 | [source,shell] 1111 | ---- 1112 | #!/usr/bin/env bash 1113 | 1114 | mvn -DskipTests=true clean package 1115 | 1116 | # <1> 1117 | cf d -f $APP_NAME 1118 | 1119 | # <2> 1120 | cf push --no-start --random-route -p $JAR $APP_NAME 1121 | cf set-env $APP_NAME SPRING_CLOUD_GCP_CREDENTIALS_ENCODED_KEY "$( cat $GCP_SERVICE_ACCOUNT_KEY_FILE | base64 -w0 )" 1122 | cf set-env $APP_NAME SPRING_CLOUD_GCP_PROJECT_ID $(gcloud config list --format 'value(core.project)') 1123 | 1124 | # <3> 1125 | cf restart $APP_NAME 1126 | ---- 1127 | <1> warning! this script **deletes** the existing application. You don't need to, but it ensures things are cleanly reset :) 1128 | <2> here we push the application, giving it a random route, and tell Cloud Foundry to _not_ start it. Then, we specify environment variables for the application. 1129 | <3> finally, we start the application now that we're done securing it. 1130 | 1131 | TIP: In both `run.sh` and `deploy.sh` we used `base64 -w0` to encode the `.json` service account file in Base64. One of us is running on Linux and the other on OS X. The `-w0` operand ensures that the Base64 encoded file isn't hard-wrapped, that it is one contiguous line of text, when running on Linux against the `GNU` `base64` utility. 1132 | -------------------------------------------------------------------------------- /mvnw: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # ---------------------------------------------------------------------------- 3 | # Licensed to the Apache Software Foundation (ASF) under one 4 | # or more contributor license agreements. See the NOTICE file 5 | # distributed with this work for additional information 6 | # regarding copyright ownership. The ASF licenses this file 7 | # to you under the Apache License, Version 2.0 (the 8 | # "License"); you may not use this file except in compliance 9 | # with the License. You may obtain a copy of the License at 10 | # 11 | # http://www.apache.org/licenses/LICENSE-2.0 12 | # 13 | # Unless required by applicable law or agreed to in writing, 14 | # software distributed under the License is distributed on an 15 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 16 | # KIND, either express or implied. See the License for the 17 | # specific language governing permissions and limitations 18 | # under the License. 19 | # ---------------------------------------------------------------------------- 20 | 21 | # ---------------------------------------------------------------------------- 22 | # Maven2 Start Up Batch script 23 | # 24 | # Required ENV vars: 25 | # ------------------ 26 | # JAVA_HOME - location of a JDK home dir 27 | # 28 | # Optional ENV vars 29 | # ----------------- 30 | # M2_HOME - location of maven2's installed home dir 31 | # MAVEN_OPTS - parameters passed to the Java VM when running Maven 32 | # e.g. to debug Maven itself, use 33 | # set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 34 | # MAVEN_SKIP_RC - flag to disable loading of mavenrc files 35 | # ---------------------------------------------------------------------------- 36 | 37 | if [ -z "$MAVEN_SKIP_RC" ] ; then 38 | 39 | if [ -f /etc/mavenrc ] ; then 40 | . /etc/mavenrc 41 | fi 42 | 43 | if [ -f "$HOME/.mavenrc" ] ; then 44 | . "$HOME/.mavenrc" 45 | fi 46 | 47 | fi 48 | 49 | # OS specific support. $var _must_ be set to either true or false. 50 | cygwin=false; 51 | darwin=false; 52 | mingw=false 53 | case "`uname`" in 54 | CYGWIN*) cygwin=true ;; 55 | MINGW*) mingw=true;; 56 | Darwin*) darwin=true 57 | # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home 58 | # See https://developer.apple.com/library/mac/qa/qa1170/_index.html 59 | if [ -z "$JAVA_HOME" ]; then 60 | if [ -x "/usr/libexec/java_home" ]; then 61 | export JAVA_HOME="`/usr/libexec/java_home`" 62 | else 63 | export JAVA_HOME="/Library/Java/Home" 64 | fi 65 | fi 66 | ;; 67 | esac 68 | 69 | if [ -z "$JAVA_HOME" ] ; then 70 | if [ -r /etc/gentoo-release ] ; then 71 | JAVA_HOME=`java-config --jre-home` 72 | fi 73 | fi 74 | 75 | if [ -z "$M2_HOME" ] ; then 76 | ## resolve links - $0 may be a link to maven's home 77 | PRG="$0" 78 | 79 | # need this for relative symlinks 80 | while [ -h "$PRG" ] ; do 81 | ls=`ls -ld "$PRG"` 82 | link=`expr "$ls" : '.*-> \(.*\)$'` 83 | if expr "$link" : '/.*' > /dev/null; then 84 | PRG="$link" 85 | else 86 | PRG="`dirname "$PRG"`/$link" 87 | fi 88 | done 89 | 90 | saveddir=`pwd` 91 | 92 | M2_HOME=`dirname "$PRG"`/.. 93 | 94 | # make it fully qualified 95 | M2_HOME=`cd "$M2_HOME" && pwd` 96 | 97 | cd "$saveddir" 98 | # echo Using m2 at $M2_HOME 99 | fi 100 | 101 | # For Cygwin, ensure paths are in UNIX format before anything is touched 102 | if $cygwin ; then 103 | [ -n "$M2_HOME" ] && 104 | M2_HOME=`cygpath --unix "$M2_HOME"` 105 | [ -n "$JAVA_HOME" ] && 106 | JAVA_HOME=`cygpath --unix "$JAVA_HOME"` 107 | [ -n "$CLASSPATH" ] && 108 | CLASSPATH=`cygpath --path --unix "$CLASSPATH"` 109 | fi 110 | 111 | # For Mingw, ensure paths are in UNIX format before anything is touched 112 | if $mingw ; then 113 | [ -n "$M2_HOME" ] && 114 | M2_HOME="`(cd "$M2_HOME"; pwd)`" 115 | [ -n "$JAVA_HOME" ] && 116 | JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" 117 | # TODO classpath? 118 | fi 119 | 120 | if [ -z "$JAVA_HOME" ]; then 121 | javaExecutable="`which javac`" 122 | if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then 123 | # readlink(1) is not available as standard on Solaris 10. 124 | readLink=`which readlink` 125 | if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then 126 | if $darwin ; then 127 | javaHome="`dirname \"$javaExecutable\"`" 128 | javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" 129 | else 130 | javaExecutable="`readlink -f \"$javaExecutable\"`" 131 | fi 132 | javaHome="`dirname \"$javaExecutable\"`" 133 | javaHome=`expr "$javaHome" : '\(.*\)/bin'` 134 | JAVA_HOME="$javaHome" 135 | export JAVA_HOME 136 | fi 137 | fi 138 | fi 139 | 140 | if [ -z "$JAVACMD" ] ; then 141 | if [ -n "$JAVA_HOME" ] ; then 142 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 143 | # IBM's JDK on AIX uses strange locations for the executables 144 | JAVACMD="$JAVA_HOME/jre/sh/java" 145 | else 146 | JAVACMD="$JAVA_HOME/bin/java" 147 | fi 148 | else 149 | JAVACMD="`which java`" 150 | fi 151 | fi 152 | 153 | if [ ! -x "$JAVACMD" ] ; then 154 | echo "Error: JAVA_HOME is not defined correctly." >&2 155 | echo " We cannot execute $JAVACMD" >&2 156 | exit 1 157 | fi 158 | 159 | if [ -z "$JAVA_HOME" ] ; then 160 | echo "Warning: JAVA_HOME environment variable is not set." 161 | fi 162 | 163 | CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher 164 | 165 | # traverses directory structure from process work directory to filesystem root 166 | # first directory with .mvn subdirectory is considered project base directory 167 | find_maven_basedir() { 168 | 169 | if [ -z "$1" ] 170 | then 171 | echo "Path not specified to find_maven_basedir" 172 | return 1 173 | fi 174 | 175 | basedir="$1" 176 | wdir="$1" 177 | while [ "$wdir" != '/' ] ; do 178 | if [ -d "$wdir"/.mvn ] ; then 179 | basedir=$wdir 180 | break 181 | fi 182 | # workaround for JBEAP-8937 (on Solaris 10/Sparc) 183 | if [ -d "${wdir}" ]; then 184 | wdir=`cd "$wdir/.."; pwd` 185 | fi 186 | # end of workaround 187 | done 188 | echo "${basedir}" 189 | } 190 | 191 | # concatenates all lines of a file 192 | concat_lines() { 193 | if [ -f "$1" ]; then 194 | echo "$(tr -s '\n' ' ' < "$1")" 195 | fi 196 | } 197 | 198 | BASE_DIR=`find_maven_basedir "$(pwd)"` 199 | if [ -z "$BASE_DIR" ]; then 200 | exit 1; 201 | fi 202 | 203 | ########################################################################################## 204 | # Extension to allow automatically downloading the maven-wrapper.jar from Maven-central 205 | # This allows using the maven wrapper in projects that prohibit checking in binary data. 206 | ########################################################################################## 207 | if [ -r "$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" ]; then 208 | if [ "$MVNW_VERBOSE" = true ]; then 209 | echo "Found .mvn/wrapper/maven-wrapper.jar" 210 | fi 211 | else 212 | if [ "$MVNW_VERBOSE" = true ]; then 213 | echo "Couldn't find .mvn/wrapper/maven-wrapper.jar, downloading it ..." 214 | fi 215 | jarUrl="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.4.2/maven-wrapper-0.4.2.jar" 216 | while IFS="=" read key value; do 217 | case "$key" in (wrapperUrl) jarUrl="$value"; break ;; 218 | esac 219 | done < "$BASE_DIR/.mvn/wrapper/maven-wrapper.properties" 220 | if [ "$MVNW_VERBOSE" = true ]; then 221 | echo "Downloading from: $jarUrl" 222 | fi 223 | wrapperJarPath="$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" 224 | 225 | if command -v wget > /dev/null; then 226 | if [ "$MVNW_VERBOSE" = true ]; then 227 | echo "Found wget ... using wget" 228 | fi 229 | wget "$jarUrl" -O "$wrapperJarPath" 230 | elif command -v curl > /dev/null; then 231 | if [ "$MVNW_VERBOSE" = true ]; then 232 | echo "Found curl ... using curl" 233 | fi 234 | curl -o "$wrapperJarPath" "$jarUrl" 235 | else 236 | if [ "$MVNW_VERBOSE" = true ]; then 237 | echo "Falling back to using Java to download" 238 | fi 239 | javaClass="$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.java" 240 | if [ -e "$javaClass" ]; then 241 | if [ ! -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then 242 | if [ "$MVNW_VERBOSE" = true ]; then 243 | echo " - Compiling MavenWrapperDownloader.java ..." 244 | fi 245 | # Compiling the Java class 246 | ("$JAVA_HOME/bin/javac" "$javaClass") 247 | fi 248 | if [ -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then 249 | # Running the downloader 250 | if [ "$MVNW_VERBOSE" = true ]; then 251 | echo " - Running MavenWrapperDownloader.java ..." 252 | fi 253 | ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$MAVEN_PROJECTBASEDIR") 254 | fi 255 | fi 256 | fi 257 | fi 258 | ########################################################################################## 259 | # End of extension 260 | ########################################################################################## 261 | 262 | export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"} 263 | if [ "$MVNW_VERBOSE" = true ]; then 264 | echo $MAVEN_PROJECTBASEDIR 265 | fi 266 | MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" 267 | 268 | # For Cygwin, switch paths to Windows format before running java 269 | if $cygwin; then 270 | [ -n "$M2_HOME" ] && 271 | M2_HOME=`cygpath --path --windows "$M2_HOME"` 272 | [ -n "$JAVA_HOME" ] && 273 | JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` 274 | [ -n "$CLASSPATH" ] && 275 | CLASSPATH=`cygpath --path --windows "$CLASSPATH"` 276 | [ -n "$MAVEN_PROJECTBASEDIR" ] && 277 | MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"` 278 | fi 279 | 280 | WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain 281 | 282 | exec "$JAVACMD" \ 283 | $MAVEN_OPTS \ 284 | -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ 285 | "-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ 286 | ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" 287 | -------------------------------------------------------------------------------- /mvnw.cmd: -------------------------------------------------------------------------------- 1 | @REM ---------------------------------------------------------------------------- 2 | @REM Licensed to the Apache Software Foundation (ASF) under one 3 | @REM or more contributor license agreements. See the NOTICE file 4 | @REM distributed with this work for additional information 5 | @REM regarding copyright ownership. The ASF licenses this file 6 | @REM to you under the Apache License, Version 2.0 (the 7 | @REM "License"); you may not use this file except in compliance 8 | @REM with the License. You may obtain a copy of the License at 9 | @REM 10 | @REM http://www.apache.org/licenses/LICENSE-2.0 11 | @REM 12 | @REM Unless required by applicable law or agreed to in writing, 13 | @REM software distributed under the License is distributed on an 14 | @REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | @REM KIND, either express or implied. See the License for the 16 | @REM specific language governing permissions and limitations 17 | @REM under the License. 18 | @REM ---------------------------------------------------------------------------- 19 | 20 | @REM ---------------------------------------------------------------------------- 21 | @REM Maven2 Start Up Batch script 22 | @REM 23 | @REM Required ENV vars: 24 | @REM JAVA_HOME - location of a JDK home dir 25 | @REM 26 | @REM Optional ENV vars 27 | @REM M2_HOME - location of maven2's installed home dir 28 | @REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands 29 | @REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a key stroke before ending 30 | @REM MAVEN_OPTS - parameters passed to the Java VM when running Maven 31 | @REM e.g. to debug Maven itself, use 32 | @REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 33 | @REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files 34 | @REM ---------------------------------------------------------------------------- 35 | 36 | @REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' 37 | @echo off 38 | @REM set title of command window 39 | title %0 40 | @REM enable echoing my setting MAVEN_BATCH_ECHO to 'on' 41 | @if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% 42 | 43 | @REM set %HOME% to equivalent of $HOME 44 | if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") 45 | 46 | @REM Execute a user defined script before this one 47 | if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre 48 | @REM check for pre script, once with legacy .bat ending and once with .cmd ending 49 | if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat" 50 | if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd" 51 | :skipRcPre 52 | 53 | @setlocal 54 | 55 | set ERROR_CODE=0 56 | 57 | @REM To isolate internal variables from possible post scripts, we use another setlocal 58 | @setlocal 59 | 60 | @REM ==== START VALIDATION ==== 61 | if not "%JAVA_HOME%" == "" goto OkJHome 62 | 63 | echo. 64 | echo Error: JAVA_HOME not found in your environment. >&2 65 | echo Please set the JAVA_HOME variable in your environment to match the >&2 66 | echo location of your Java installation. >&2 67 | echo. 68 | goto error 69 | 70 | :OkJHome 71 | if exist "%JAVA_HOME%\bin\java.exe" goto init 72 | 73 | echo. 74 | echo Error: JAVA_HOME is set to an invalid directory. >&2 75 | echo JAVA_HOME = "%JAVA_HOME%" >&2 76 | echo Please set the JAVA_HOME variable in your environment to match the >&2 77 | echo location of your Java installation. >&2 78 | echo. 79 | goto error 80 | 81 | @REM ==== END VALIDATION ==== 82 | 83 | :init 84 | 85 | @REM Find the project base dir, i.e. the directory that contains the folder ".mvn". 86 | @REM Fallback to current working directory if not found. 87 | 88 | set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% 89 | IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir 90 | 91 | set EXEC_DIR=%CD% 92 | set WDIR=%EXEC_DIR% 93 | :findBaseDir 94 | IF EXIST "%WDIR%"\.mvn goto baseDirFound 95 | cd .. 96 | IF "%WDIR%"=="%CD%" goto baseDirNotFound 97 | set WDIR=%CD% 98 | goto findBaseDir 99 | 100 | :baseDirFound 101 | set MAVEN_PROJECTBASEDIR=%WDIR% 102 | cd "%EXEC_DIR%" 103 | goto endDetectBaseDir 104 | 105 | :baseDirNotFound 106 | set MAVEN_PROJECTBASEDIR=%EXEC_DIR% 107 | cd "%EXEC_DIR%" 108 | 109 | :endDetectBaseDir 110 | 111 | IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig 112 | 113 | @setlocal EnableExtensions EnableDelayedExpansion 114 | for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a 115 | @endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% 116 | 117 | :endReadAdditionalConfig 118 | 119 | SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" 120 | set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" 121 | set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain 122 | 123 | set DOWNLOAD_URL="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.4.2/maven-wrapper-0.4.2.jar" 124 | FOR /F "tokens=1,2 delims==" %%A IN (%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties) DO ( 125 | IF "%%A"=="wrapperUrl" SET DOWNLOAD_URL=%%B 126 | ) 127 | 128 | @REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central 129 | @REM This allows using the maven wrapper in projects that prohibit checking in binary data. 130 | if exist %WRAPPER_JAR% ( 131 | echo Found %WRAPPER_JAR% 132 | ) else ( 133 | echo Couldn't find %WRAPPER_JAR%, downloading it ... 134 | echo Downloading from: %DOWNLOAD_URL% 135 | powershell -Command "(New-Object Net.WebClient).DownloadFile('%DOWNLOAD_URL%', '%WRAPPER_JAR%')" 136 | echo Finished downloading %WRAPPER_JAR% 137 | ) 138 | @REM End of extension 139 | 140 | %MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* 141 | if ERRORLEVEL 1 goto error 142 | goto end 143 | 144 | :error 145 | set ERROR_CODE=1 146 | 147 | :end 148 | @endlocal & set ERROR_CODE=%ERROR_CODE% 149 | 150 | if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost 151 | @REM check for post script, once with legacy .bat ending and once with .cmd ending 152 | if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat" 153 | if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd" 154 | :skipRcPost 155 | 156 | @REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' 157 | if "%MAVEN_BATCH_PAUSE%" == "on" pause 158 | 159 | if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE% 160 | 161 | exit /B %ERROR_CODE% 162 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | com.example 8 | bootiful-gcp 9 | 0.0.1-SNAPSHOT 10 | jar 11 | 12 | bootiful-gcp 13 | Demo project for Spring Boot 14 | 15 | 16 | org.springframework.boot 17 | spring-boot-starter-parent 18 | 2.1.0.RELEASE 19 | 20 | 21 | 22 | 23 | UTF-8 24 | UTF-8 25 | 26 | 1.8 27 | Greenwich.M3 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | org.springframework.cloud 36 | spring-cloud-gcp-starter-trace 37 | 38 | 39 | org.springframework.cloud 40 | spring-cloud-gcp-starter-logging 41 | 42 | 43 | 44 | com.google.cloud 45 | google-cloud-vision 46 | 47 | 48 | 49 | org.springframework.cloud 50 | spring-cloud-gcp-starter-data-spanner 51 | 52 | 53 | org.springframework.cloud 54 | spring-cloud-gcp-starter-sql-mysql 55 | 56 | 57 | 58 | org.springframework.boot 59 | spring-boot-starter-actuator 60 | 61 | 62 | org.springframework.boot 63 | spring-boot-starter-jdbc 64 | 65 | 66 | org.springframework.boot 67 | spring-boot-starter-web 68 | 69 | 70 | org.springframework.cloud 71 | spring-cloud-gcp-starter 72 | 73 | 74 | org.springframework.cloud 75 | spring-cloud-gcp-starter-pubsub 76 | 77 | 78 | org.springframework.cloud 79 | spring-cloud-gcp-starter-storage 80 | 81 | 82 | org.springframework.cloud 83 | spring-cloud-starter-sleuth 84 | 85 | 86 | 87 | org.projectlombok 88 | lombok 89 | true 90 | 91 | 92 | org.springframework.boot 93 | spring-boot-starter-test 94 | test 95 | 96 | 97 | 98 | 99 | 100 | 101 | org.springframework.cloud 102 | spring-cloud-dependencies 103 | ${spring-cloud.version} 104 | pom 105 | import 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | org.springframework.boot 114 | spring-boot-maven-plugin 115 | 116 | 117 | 118 | 119 | 120 | 121 | spring-milestones 122 | Spring Milestones 123 | https://repo.spring.io/milestone 124 | 125 | false 126 | 127 | 128 | 129 | 130 | 131 | 132 | -------------------------------------------------------------------------------- /src/main/java/com/example/bootifulgcp/BootifulGcpApplication.java: -------------------------------------------------------------------------------- 1 | package com.example.bootifulgcp; 2 | 3 | import com.google.api.gax.core.CredentialsProvider; 4 | import com.google.cloud.vision.v1.*; 5 | import com.google.protobuf.ByteString; 6 | import lombok.AllArgsConstructor; 7 | import lombok.Data; 8 | import lombok.NoArgsConstructor; 9 | import lombok.extern.log4j.Log4j2; 10 | import org.springframework.beans.factory.annotation.Value; 11 | import org.springframework.boot.SpringApplication; 12 | import org.springframework.boot.autoconfigure.SpringBootApplication; 13 | import org.springframework.boot.context.event.ApplicationReadyEvent; 14 | import org.springframework.cloud.gcp.data.spanner.core.mapping.Table; 15 | import org.springframework.cloud.gcp.data.spanner.repository.SpannerRepository; 16 | import org.springframework.cloud.gcp.pubsub.core.publisher.PubSubPublisherTemplate; 17 | import org.springframework.cloud.gcp.pubsub.core.subscriber.PubSubSubscriberTemplate; 18 | import org.springframework.context.annotation.Bean; 19 | import org.springframework.context.event.EventListener; 20 | import org.springframework.core.io.Resource; 21 | import org.springframework.jdbc.core.JdbcTemplate; 22 | import org.springframework.stereotype.Component; 23 | import org.springframework.util.FileCopyUtils; 24 | import org.springframework.web.bind.annotation.GetMapping; 25 | import org.springframework.web.bind.annotation.PathVariable; 26 | import org.springframework.web.bind.annotation.RestController; 27 | import org.springframework.web.client.RestTemplate; 28 | 29 | import java.time.Instant; 30 | import java.util.Collection; 31 | import java.util.Collections; 32 | import java.util.List; 33 | import java.util.UUID; 34 | import java.util.stream.Collectors; 35 | import java.util.stream.Stream; 36 | 37 | @SpringBootApplication 38 | public class BootifulGcpApplication { 39 | 40 | @Bean 41 | RestTemplate restTemplate() { 42 | return new RestTemplate(); 43 | } 44 | 45 | @Bean 46 | ImageAnnotatorClient imageAnnotatorClient(CredentialsProvider cp) throws Exception { 47 | ImageAnnotatorSettings build = ImageAnnotatorSettings.newBuilder() 48 | .setCredentialsProvider(cp) 49 | .build(); 50 | return ImageAnnotatorClient.create(build); 51 | } 52 | 53 | public static void main(String[] args) { 54 | SpringApplication.run(BootifulGcpApplication.class, args); 55 | } 56 | } 57 | 58 | 59 | @Component 60 | @Log4j2 61 | class PubsubDemo { 62 | 63 | private final PubSubPublisherTemplate publisher; 64 | private final PubSubSubscriberTemplate subscriber; 65 | 66 | PubsubDemo(PubSubPublisherTemplate publisher, PubSubSubscriberTemplate subscriber) { 67 | this.publisher = publisher; 68 | this.subscriber = subscriber; 69 | } 70 | 71 | @EventListener(ApplicationReadyEvent.class) 72 | public void demo() throws Exception { 73 | 74 | this.subscriber.subscribe("reservations-subscription", msg -> { 75 | ByteString data = msg.getPubsubMessage().getData(); 76 | String stringUtf8 = data.toStringUtf8(); 77 | log.info("message: " + stringUtf8); 78 | msg.ack(); 79 | }); 80 | 81 | this.publisher 82 | .publish("reservations", "Hello @ " + Instant.now().toString()); 83 | 84 | } 85 | } 86 | 87 | @Component 88 | @Log4j2 89 | class VisionDemo { 90 | 91 | private final Resource resource; 92 | private final ImageAnnotatorClient imageAnnotatorClient; 93 | 94 | VisionDemo( 95 | @Value("gs://pgtm-jlong-bucket/cat.jpg") Resource cat, 96 | ImageAnnotatorClient imageAnnotatorClient) { 97 | this.resource = cat; 98 | this.imageAnnotatorClient = imageAnnotatorClient; 99 | } 100 | 101 | @EventListener(ApplicationReadyEvent.class) 102 | public void demo() throws Exception { 103 | 104 | byte[] catBytes = FileCopyUtils 105 | .copyToByteArray(this.resource.getInputStream()); 106 | 107 | AnnotateImageRequest build = AnnotateImageRequest 108 | .newBuilder() 109 | .addFeatures(Feature.newBuilder().setType(Feature.Type.LABEL_DETECTION)) 110 | .setImage(Image.newBuilder().setContent(ByteString.copyFrom(catBytes))) 111 | .build(); 112 | 113 | BatchAnnotateImagesResponse response = 114 | this.imageAnnotatorClient.batchAnnotateImages(Collections.singletonList(build)); 115 | 116 | log.info(response); 117 | } 118 | 119 | } 120 | 121 | @Component 122 | @Log4j2 123 | class SpannerDemo { 124 | 125 | private final ReservationRepository reservationRepository; 126 | 127 | SpannerDemo(ReservationRepository reservationRepository) { 128 | this.reservationRepository = reservationRepository; 129 | } 130 | 131 | @EventListener(ApplicationReadyEvent.class) 132 | public void demo() throws Exception { 133 | this.reservationRepository.deleteAll(); 134 | Stream.of("Ray", "Josh", "Olga", "Violetta", "Cornelia", "Dave", "Mark", "Madhura", "Andy") 135 | .map(name -> new Reservation(UUID.randomUUID().toString(), name)) 136 | .map(this.reservationRepository::save) 137 | .forEach(log::info); 138 | } 139 | 140 | } 141 | 142 | interface ReservationRepository extends SpannerRepository { 143 | } 144 | 145 | @Table(name = "reservations") 146 | @Data 147 | @AllArgsConstructor 148 | @NoArgsConstructor 149 | class Reservation { 150 | private String id; 151 | private String name; 152 | } 153 | 154 | 155 | @Component 156 | @Log4j2 157 | class MySqlDemo { 158 | 159 | @Data 160 | @AllArgsConstructor 161 | @NoArgsConstructor 162 | private static class Reservation { 163 | private Long id; 164 | private String name; 165 | } 166 | 167 | private final JdbcTemplate jdbcTemplate; 168 | 169 | MySqlDemo(JdbcTemplate jdbcTemplate) { 170 | this.jdbcTemplate = jdbcTemplate; 171 | } 172 | 173 | @EventListener(ApplicationReadyEvent.class) 174 | public void demo() throws Exception { 175 | 176 | List reservationList = this.jdbcTemplate.query("select * from reservations", 177 | (rs, rowNum) -> new Reservation(rs.getLong("id"), rs.getString("name"))); 178 | reservationList.forEach(log::info); 179 | } 180 | } 181 | 182 | 183 | @RestController 184 | @Log4j2 185 | class GreetingsRestController { 186 | 187 | private final RestTemplate restTemplate; 188 | 189 | GreetingsRestController(RestTemplate restTemplate) { 190 | this.restTemplate = restTemplate; 191 | } 192 | 193 | @GetMapping("/greet/{name}") 194 | String greet(@PathVariable String name) { 195 | log.info("greeting " + name + '.'); 196 | return "hello, " + name + "!"; 197 | } 198 | 199 | @GetMapping("/client") 200 | Collection client() { 201 | return Stream.of("Ray", "Dave", "Bob", "Paul", "Tammie", "Kimly", "Holden", "Cornelia") 202 | .map(this::call) 203 | .collect(Collectors.toList()); 204 | } 205 | 206 | private String call(String name) { 207 | return this.restTemplate 208 | .getForEntity("http://localhost:8080/greet/{name}", String.class, name) 209 | .getBody(); 210 | } 211 | 212 | } 213 | -------------------------------------------------------------------------------- /src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | # 2 | # MySQL 3 | spring.cloud.gcp.sql.instance-connection-name=pgtm-jlong:us-central1:reservations 4 | spring.cloud.gcp.sql.database-name=reservations 5 | spring.datasource.hikari.maximum-pool-size=2 6 | # 7 | # Spanner 8 | spring.cloud.gcp.spanner.instance-id=reservations-demo 9 | spring.cloud.gcp.spanner.database=reservations 10 | # 11 | # Sleuth 12 | spring.sleuth.sampler.probability=1.0 -------------------------------------------------------------------------------- /src/main/resources/logback-spring.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/test/java/com/example/bootifulgcp/BootifulGcpApplicationTests.java: -------------------------------------------------------------------------------- 1 | package com.example.bootifulgcp; 2 | 3 | import org.junit.Test; 4 | import org.junit.runner.RunWith; 5 | import org.springframework.boot.test.context.SpringBootTest; 6 | import org.springframework.test.context.junit4.SpringRunner; 7 | 8 | @RunWith(SpringRunner.class) 9 | @SpringBootTest 10 | public class BootifulGcpApplicationTests { 11 | 12 | @Test 13 | public void contextLoads() { 14 | } 15 | 16 | } 17 | --------------------------------------------------------------------------------