Developing A Spring Boot Application for Kubernetes Cluster: A Tutorial [Part 3]
Almost at the end. In the third post of this four-part series, we take a look at how to begin constructing and testing the web layer.
Join the DZone community and get the full member experience.
Join For FreeThis is the third part of a four-part series. Check out part 1 and part 2 here.
Web Layer
The file and folder structure is similar to service layer.
|_ Dockerfile
|_ pom.xml
|_ src
|_____ main
|_________ java
|______________ com
|__________________ demo
|______________________ kubernetes
|_________________________________ springcloud
|_____________________________________________ web
|_________________________________________________ WebService.java
|_________________________________________________ WebController.java
|__________________________________________________WebServer.java
|_________ resources
|___________________ web-server.yml
|___________________ logback.xml
WebService.java
This class encapsulates the service layer we had just reviewed. It provides two methods, public String getZipInfo()
and public String getNearbyZipcodesWithinDistance()
, each forwarding the original request to the corresponding REST call, /zipcodeservice/info/{zipcode}
and /zipcodeservice/nearby/{zipcode}/{distance}
, respectively of the zip code service via the RestTemplate object.
@Service
public class WebService {
@Autowired
protected RestTemplate restTemplate;
protected String serviceUrl;
protected Logger logger = Logger.getLogger(WebService.class.getName());
/**
* The serviceURL parameter corresponds to the zipcode service.
*
* @param String
* serviceURL
*/
public WebService(String serviceURL) {
this.serviceUrl = "http://" + serviceURL;
}
public String getZipInfo(String zipcode) {
return restTemplate.getForObject(serviceUrl +
"/zipcodeservice/info/{zipcode}", String.class, zipcode);
}
public String getNearbyZipcodesWithinDistance(String zipcode, String distance) {
return restTemplate.getForObject(serviceUrl +
"/zipcodeservice/nearby/{zipcode}/{distance}", String.class,
zipcode, distance);
}
}
WebController.java
This class is a @RestController. It implements the REST calls for the web layer. It forwards each REST call to the corresponding method of the WebService object, reviewed above.
The class starts with initializing the Logger and WebService objects.
@RestController
public class WebController {
@Autowired
protected WebService service;
protected Logger logger = Logger.getLogger(WebController.class.getName());
@Autowired
public WebController(WebService service) {
logger.info("WebController initiated");
this.service = service;
}
...
The REST calls /zip/getZipcodeInfo/{zipcode}
and /zip/getNearbyZipcodes/{zipcode}/{distance}
are forwarded to WebService.getZipInfo()
and WebService.getNearbyZipcodesWithinDistance()
, respectively. The JSON response is processed by the com.google.gson.Gson parser and formatted as HTML before returned to the consumer. (In most cases one would use a template engine such as Thyme to construct HTML, however, for simplicity we added HTML tags inline.)
...
@RequestMapping(value = "/zip/getZipcodeInfo/{zipcode}",
produces = { "text/html" })
public String zipInfo(@PathVariable("zipcode") String zipcode) {
Gson gson = new Gson();
String response = service.getZipInfo(zipcode);
logger.info(response);
ZipCodeInfo info = gson.fromJson(response,ZipCodeInfo.class);
StringBuilder result = new StringBuilder();
result.append("<html><body>");
if(info != null){
result.append(info.toString());
}
result.append("</body></html>");
return result.toString();
}
@RequestMapping(value = "/zip/getNearbyZipcodes/{zipcode}/{distance}",
produces = { "text/html" })
public String zipDistance(@PathVariable("zipcode") String zipcode,
@PathVariable("distance") String distance) {
Gson gson = new Gson();
String response = service.getNearbyZipcodesWithinDistance(zipcode,
distance);
logger.info(response);
ZipCodeWrapper zipCodes = gson.fromJson(response,ZipCodeWrapper.class);
StringBuilder result = new StringBuilder();
result.append("<html><body>");
if(zipCodes != null){
result.append(zipCodes.toString());
}
result.append("</body></html>");
return result.toString();
}
}
We also define additional classes as Java beans to hold structured data. Those are ZipCode
, ZipCodeWrapper
(array representation of ZipCode
) and ZipCodeInfo
, which also contains two nested classes CityState
and TimeZone
. All those classes are defined inside WebController.java
.
class ZipCode {
String zip_code;
String distance;
String city;
String state;
public String getZip_code() {
return zip_code;
}
public void setZip_code(String zip_code) {
this.zip_code = zip_code;
}
public String getDistance() {
return distance;
}
public void setDistance(String distance) {
this.distance = distance;
}
public String getCity() {
return city;
}
public void setCity(String city) {
this.city = city;
}
public String getState() {
return state;
}
public void setState(String state) {
this.state = state;
}
public String toString() {
return "<p>zip_code=" + zip_code + ", distance=" + distance +
" miles, city=" + city + ", state=" + state;
}
}
class ZipCodeWrapper{
ZipCode[] zip_codes;
public ZipCode[] getZip_codes() {
return zip_codes;
}
public void setZip_codes(ZipCode[] zip_codes) {
this.zip_codes = zip_codes;
}
public String toString() {
StringBuilder strBldr = new StringBuilder();
strBldr.append("<p>Zip codes:<br>");
for(ZipCode zipCode:zip_codes){
strBldr.append(zipCode);
}
return strBldr.toString();
}
}
class ZipCodeInfo {
String zip_code;
String lat;
String lng;
String city;
String state;
CityState[] acceptable_city_names;
Timezone timezone;
public String getZip_code() {
return zip_code;
}
public void setZip_code(String zip_code) {
this.zip_code = zip_code;
}
public String getLat() {
return lat;
}
public void setLat(String lat) {
this.lat = lat;
}
public String getLng() {
return lng;
}
public void setLng(String lng) {
this.lng = lng;
}
public String getCity() {
return city;
}
public void setCity(String city) {
this.city = city;
}
public String getState() {
return state;
}
public void setState(String state) {
this.state = state;
}
public Timezone getTimezone() {
return timezone;
}
public void setTimezone(Timezone timezone) {
this.timezone = timezone;
}
class Timezone{
String timezone_identifier;
String timezone_abbr;
public String getTimezone_identifier() {
return timezone_identifier;
}
public void setTimezone_identifier(String timezone_identifier) {
this.timezone_identifier = timezone_identifier;
}
public String getTimezone_abbr() {
return timezone_abbr;
}
public void setTimezone_abbr(String timezone_abbr) {
this.timezone_abbr = timezone_abbr;
}
public String toString() {
return "<p>Timezone: " + timezone_identifier + " ("
+ timezone_abbr + ")";
}
}
class CityState{
String city;
String state;
public String getCity() {
return city;
}
public void setCity(String city) {
this.city = city;
}
public String getState() {
return state;
}
public void setState(String state) {
this.state = state;
}
public String toString() {
return "<p>City: " + city + ", State: " + state;
}
}
public CityState[] getAcceptable_city_names() {
return acceptable_city_names;
}
public void setAcceptable_city_names(CityState[] acceptable_city_names) {
this.acceptable_city_names = acceptable_city_names;
}
public String toString() {
StringBuilder strBldr = new StringBuilder();
strBldr.append("<p>Zipcode Information:<p>zip: " + zip_code
+ ", latitude: " + lat + ", longitude: "
+ lng + ", city: " + city + ", state: "
+ state);
strBldr.append(timezone);
strBldr.append("<p>Acceptable City Names:");
for(CityState cityState:acceptable_city_names){
strBldr.append("<p>" + cityState.getCity() + ", "
+ cityState.getState());
}
return strBldr.toString();
}
}
WebServer.java
@SpringBootApplication
@EnableDiscoveryClient
@ComponentScan(useDefaultFilters = false)
public class WebServer {
public static String web_service_url = null;
public static void main(String[] args) {
if (args.length != 1 && args.length != 2) {
errorMessage();
return;
} else if (args.length == 2) {
web_service_url = args[0];
System.setProperty("server.port", args[1]);
} else if (args.length == 1) {
web_service_url = args[0];
}
System.setProperty("spring.config.name", "web-server");
SpringApplication.run(WebServer.class, args);
}
@Bean
RestTemplate restTemplate() {
return new RestTemplate();
}
@Bean
public WebService service() {
return new WebService(web_service_url);
}
@Bean
public WebController patientController() {
return new WebController(service());
}
protected static void errorMessage() {
System.out.println("Usage: java -jar [jar file name] <zip web service url> <port> OR");
System.out.println("Usage: java -jar [jar file name] <zip web service url>");
}
}
web-server.yml
This is our application configuration file where the default port is defined.
spring:
application:
name: web-service
# HTTP Server
server:
port: 3333 # HTTP (Tomcat) port
logback.xml
We skip reviewing this file, which simply configures the logger. (See Git Hub.)
Dockerfile
By default, web layer will start at port 3333. Exclusive to Mac OS, when both the service and web applications are run in the same machine in separate Docker containers, the service URL must be passed as docker.for.mac.localhost:<port> to web layer startup. (This is important only for local testing. When run at the Kubernetes cluster in Amazon EC2 environment, docker.for.mac.localhost pseudo-DNS has no significance!)
FROM openjdk:8-jdk-alpine
VOLUME /tmp
ARG JAR_FILE
COPY ${JAR_FILE} app.jar
ENTRYPOINT ["java","-jar","/app.jar","docker.for.mac.localhost:8081","3333"]
pom.xml
Similar to the service layer, the maven build file utilizes Finchley release for Spring Cloud, and the dockerfile-maven-plugin from com.spotify to build a Docker image from the project, store the image in local Docker repository and push it into the private repository in Docker Hub. It also defines a dependency to Google GSON library.
Fromrepository
element, observe that we use the same private repository as the service layer. The value of tag
and imageTag
give the tag name for the web layer application, web-service.
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.1.RELEASE</version>
</parent>
<groupId>com.demo.kubernetes.springcloud</groupId>
<artifactId>web</artifactId>
<version>0.0.1-SNAPSHOT</version>
<properties>
<start-class>com.demo.kubernetes.springcloud.web.WebServer</start-class>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Finchley.RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<!-- Setup Spring Boot -->
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<!-- Setup Spring MVC & REST, use Embedded Tomcat -->
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<!-- Setup Spring Data common components -->
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-commons</artifactId>
</dependency>
<dependency>
<!-- Testing starter -->
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
</dependency>
<dependency>
<!-- Spring Cloud starter -->
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter</artifactId>
</dependency>
<!-- Gson: Java to Json conversion -->
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>2.8.5</version>
<scope>compile</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<executions>
<execution>
<goals>
<goal>repackage</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>com.spotify</groupId>
<artifactId>dockerfile-maven-plugin</artifactId>
<version>1.3.6</version>
<configuration>
<tag>web-service</tag>
<repository>konuratdocker/spark-examples</repository>
<imageTags>
<imageTag>web-service</imageTag>
</imageTags>
<buildArgs>
<JAR_FILE>target/${project.build.finalName}.jar</JAR_FILE>
</buildArgs>
</configuration>
</plugin>
</plugins>
</build>
</project>
Building the Web Application and Creating Docker Image
In root folder where pom.xml file exists execute
mvn install dockerfile:build -DpushImageTag
to build the application and to store the image locally. To locally run the application for testing purposes, we first start the service application (see previous section):
docker run -it -p 8081:2223 konuratdocker/spark-examples:zipcode-service
and then execute
docker run -it -p <local port>:<target port> konuratdocker/spark-examples:web-service
e.g.
docker run -it -p 3335:3333 konuratdocker/spark-examples:web-service
The target port must be 3333 because that is the port specified in Dockerfile, ENTRYPOINT instruction. The local port could be same as target port or different, 3335 in the example.
You should see the following after docker run
is executed. (Observe the line printed by Tomcat's HTTP NIO connector, ["http-nio-3333"], the target port in Dockerfile.)
. ____ _ __ _ _
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: Spring Boot :: (v2.0.1.RELEASE)
1: INFO WebServer - No active profile set, falling back to default profiles: default
1: INFO Http11NioProtocol - Initializing ProtocolHandler ["http-nio-3333"]
1: INFO StandardService - Starting service [Tomcat]
1: INFO StandardEngine - Starting Servlet Engine: Apache Tomcat/8.5.29
1: INFO AprLifecycleListener - The APR based Apache Tomcat Native library which allows optimal performance in production environments was not found on the java.library.path: [/usr/lib/jvm/java-1.8-openjdk/jre/lib/amd64/server:/usr/lib/jvm/java-1.8-openjdk/jre/lib/amd64:/usr/lib/jvm/java-1.8-openjdk/jre/../lib/amd64:/usr/java/packages/lib/amd64:/usr/lib64:/lib64:/lib:/usr/lib]
1: INFO [/] - Initializing Spring embedded WebApplicationContext
1: INFO WebController - WebController initiated
1: INFO Http11NioProtocol - Starting ProtocolHandler ["http-nio-3333"]
1: INFO NioSelectorPool - Using a shared selector for servlet write/read
1: INFO WebServer - Started WebServer in 7.017 seconds (JVM running for 8.549)
Finally, observe that the local port 8081 for the service layer application coincides with docker.for.mac.localhost:8081 in ENTRYPOINT instruction for the pseudo-DNS:post
for the service layer. This is explained in the below diagram.
To test the web layer, view http://localhost:3335/zip/getZipcodeInfo/33323 in a local browser to see:
<html><body><p>Zipcode Information:<p>zip: 33323, latitude: 26.152028, longitude: -80.320661, city: Fort Lauderdale, state: FL<p>Timezone: America/New_York (EDT)<p>Acceptable City Names:<p>Ft Lauderdale, FL<p>Plantation, FL<p>Sunrise, FL<p>Tamarac, FL</body></html>
Having tested locally, we can push the web layer image to Docker Hub. Log in to Docker Hub via docker login
in development machine. After login succeeds, execute
mvn dockerfile:push -Ddockerfile.useMavenSettingsForAuth=true
You should see something similar to below in the response.
[INFO] Image 9eee428f42bf: Pushed
Now, inspection of the Docker Hub private repository shows the newly pushed image as follows.
Opinions expressed by DZone contributors are their own.
Comments