{{announcement.body}}
{{announcement.title}}

Developing A Spring Boot Application for Kubernetes Cluster: A Tutorial [Part 3]

DZone 's Guide to

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.

· Cloud Zone ·
Free Resource

This 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

This class is the main entry point to web layer. It starts up the application listening at the default port or the port supplied at the command line. The root URL of the service layer should also be defined at startup.
@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.

Image titleTo 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.

Docker Hub private repository for web-service.

Topics:
cloud ,spring boot ,installation and configuration ,web service ,java ,tutorial

Opinions expressed by DZone contributors are their own.

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}