├── .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 |
--------------------------------------------------------------------------------