Chapter 4 Tutorials

Command Line Application

The starter code for this chapter has been moved to the central repository. The new coordinates are different that the ones listed in the book. Always take the latest version of the code. The current version is 1.0.2. The only difference from the book's coordinates is the version:

  • Group ID: com.bytesizebook
  • Artifact ID: spring-boot-java-cli
  • Version: 1.0.2

Use the coordinates of this archetype to generate the starting code, as in Chapter 1. This is one command that appears on one line. The \ character is used to escape the newline character so it can be displayed in the book.

mvn archetype:generate \
    -DgroupId=com.bytesizebook \
    -DartifactId=spring-cli \
    -DpackageName=com.bytesizebook \
    -DarchetypeGroupId=com.bytesizebook \
    -DarchetypeArtifactId=spring-boot-java-cli \
    -DarchetypeVersion=1.0.2
    

The exact values for the archetype can be seen by searching for com.bytesizebook in search.maven.org.

If the archetype is not selected automatically and you are asked to enter a filter, enter spring-boot-java-cli and select the one for bytesizebook.

Once the archetype is generated, run it with the following command. Unfortunately, the typesetter for the book changed -- to - in the text. The correct command is here:

mvn spring-boot:run -Dspring-boot.run.arguments=--program=automobile
    

These commands use a different value for the program variable.

mvn spring-boot:run -Dspring-boot.run.arguments=--program=client

mvn spring-boot:run -Dspring-boot.run.arguments=--program=none

Spring Boot MVC Application

  1. Copy the command line application above to a new folder.
  2. Edit the pom.xml.
    1. Change the artifact ID to boot-web
                    <artifactId>boot-web</artifactId>
    2. Change the name to boot-web
                    <name>boot-web</name>
    3. Add the dependency for the starter-web to the pom file.
              <dependency>
                  <groupId>org.springframework.boot</groupId>
                  <artifactId>spring-boot-starter-web</artifactId>
              </dependency>
    4. Add the dependency for the starter-tomcat to the pom file.
              <dependency>
                  <groupId>org.springframework.boot</groupId>
                  <artifactId>spring-boot-starter-tomcat</artifactId>
              </dependency>
  3. Edit the SimpleBean.java file in the com/bytesizebook folder.
    1. Remove all the packages and contents except for com.bytesizebook.com. They contain the beans that were used in the command line application and are not needed anymore.
    2. Remove the implements clause and extend the class from SpringBootServletInitializer. This is only needed to generate WAR files for deployment.
      @SpringBootApplication
      public class SimpleBean extends SpringBootServletInitializer { 
      ...
      }
    3. Keep the main method and remove all the other methods and variables from the previous application. Only the main method is needed for this application.
      @SpringBootApplication
      public class SimpleBean extends SpringBootServletInitializer {    
          
          public static void main(String[] args) throws Exception {
              SpringApplication.run(SimpleBean.class, args);
          }
      
      }
  4. Java source files belong in the src/main/java folder. An IDE might create an alias for the folder. Netbeans uses the alias Source Packages.
    1. Create a Java file named Index.java in the com/bytesizebook folder. This is the same folder as the one that contains the SimpleBean.java file.
    2. Modify the class with the @Controller annotation.
      @Controller
      public class Index {
         ...
      }
  5. Public web pages for the application belong in the src/main/webapp folder.
    1. Create the webapp folder in src/main folder.
    2. Create a file named index.html with the following text:
      <!DOCTYPE html>
      <html>
          <head>
              <title>Spring Boot Application</title>
              <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
          </head>
          <body>
              <h1>Spring Boot Application</h1>
          </body>
      </html></pre>
        
  6. Additional resources for the application belong in the src/main/resources folder.
    1. Create the resources folder.
    2. Create a new file named application.properties in the resources folder. This file can be used to define properties that change the behavior of the application.
    3. By default, all the files in this web application will start from the root URL of the web server. To use a name that starts all URLs for this web application, create a property for context.name. All resources from this web application will have this name as a preface to its URL. To agree with the URLs that are listed in the book, set the name to /boot-web
      context.name=/boot-web
  7. Start the application with the maven command:
    mvn spring-boot:run
  8. Access the application through a web browser with the URL:
    http://localhost:8080/boot-web/
    Be sure to include the final / or the page will not be found.

    Scope Example

    The main configuration file for the application is SimpleBean.java in the com/bytesizebook folder. For organization, controllers will be added to a sub-folder named controller and data beans will be added to a sub-folder named data.

    Note: the examples in the book used an additional folder named web. These tutorials will not include that folder, but will use the simpler structure of all folders and file descending from the folder that contains SimpleBean.java.

    Configuration

    The configuration file must be modified for this chapter and some java classes must be added.

    1. Add a view resolver to the SimpleBean.java file.
          @Bean
          public ViewResolver internalResourceViewResolver() {
              InternalResourceViewResolver bean = new InternalResourceViewResolver();
              bean.setViewClass(JstlView.class);
              bean.setPrefix("/WEB-INF/views/");
              bean.setSuffix(".jsp");
              return bean;
          }
    2. Create a folder named data/ch3/restructured/scope in the same folder that contains the configuration file. The first example implements the different scopes for a bean.
    3. Add a file named RequestData.java for the hobby and aversion interface to the scope folder.
      public interface RequestData {
          
          public void setHobby(String hobby); 
          public String getHobby();
          
          public void setAversion(String aversion);
          public String getAversion();
          
      }
    4. Add a file named RequestDataScope.java that implements the interface from the last step.
      public class RequestDataScope  implements RequestData {
          
          protected String hobby;
          protected String aversion;
      
          @Override
          public void setHobby(String hobby) {
              this.hobby = hobby;
          }
      
          @Override
          public String getHobby() {
              return hobby;
          }
      
          @Override
          public void setAversion(String aversion) {
              this.aversion = aversion;
          }
      
          @Override
          public String getAversion() {
              return aversion;
          }
          
      }
    5. Modify the configuration file SimpleBean.java and add definitions for each of the four scope types.

      This section only shows how to define scopes, it does not have an example to use them. The next example is a full example that uses similar configuration to declare a bean. The example will use a request scoped bean, but could have used a session scoped bean. A second example will be developed later that shows the problems with using the singleton and prototype scopes.

          ...
          @Bean("singleScopeBean")
          RequestDataScope getSingleScopeBean() {
              return new RequestDataScope();
          }
          
          @Bean("protoScopeBean")
          @Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
          RequestDataScope getProtoScopeBean() {
              return new RequestDataScope();
          }
          
          @Bean("requestScopeBean")
          @RequestScope
          RequestDataScope getRequestScopeBean() {
              return new RequestDataScope();
          }
          
          @Bean("sessionScopeBean")
          @SessionScope
          RequestDataScope getSessionScopeBean() {
              return new RequestDataScope();
          }
          ...            
    6. Add two new dependencies to the pom file for the application.
              <dependency>
                  <groupId>org.apache.tomcat.embed</groupId>
                  <artifactId>tomcat-embed-jasper</artifactId>
                  <scope>provided</scope>
              </dependency>
              <dependency>
                  <groupId>javax.servlet</groupId>
                  <artifactId>jstl</artifactId>
              </dependency>
      
                  
    7. Install the new packages with the maven command
      mvn clean install
    8. This example shows how to configure spring for beans and to create a bean. Similar configuration will be used in the next example for its bean. In the next chapter, after additional Spring features are added to the application, an example that uses singleton scope will be created to show the problem with singleton scoped beans.

    Restructured Controller

    This example will rework the example from the last chapter with minimal changes so it will work with Spring.

    Bean

    1. Create a folder named data/ch3/restructured in the same folder that contains the configuration file.
    2. Add a file named RequestData.java for the hobby and aversion interface to the restructured folder.
      public interface RequestData {
          
          public void setHobby(String hobby); 
          public String getHobby();
          
          public void setAversion(String aversion);
          public String getAversion();
          
      }
    3. Add a file named RequestDataDefault.java that implements the interface from the last step. The file is the same as the last bean from chapter 3 that implemented default validation.
    4. Modify the configuration file SimpleBean.java and add a definition for the bean that will use request scope.
          @Bean("requestDefaultBean")
          @RequestScope
          RequestDataDefault getRequestDefaultBean() {
              return new RequestDataDefault();
          }  

    Controller

    1. Add a Java class named ControllerHelper.java to the controller/ch3/restructured folder that descends from the folder that contains the configuration file SimpleBean.java. This class is replacing the controller helper class from the last chapter.
    2. The class does not need the base class from chapter 3. The base class was used to make the request accessible throughout the application. That function will be handled by Spring.
      public class ControllerHelper { 
          ...
      }
    3. Mark the class with the Controller annotation and the RequestMapping annotations. The first allows Spring to find the controller, the second defines the URL that is used to access the class.
      @Controller
      @RequestMapping("/ch3/restructured/Controller")
      public class ControllerHelper {
          ...
      }
    4. Add a method that modifies the address for a view. Part of the address is handled by the view resolver, but each controller can modify the address further.
          String viewLocation(String view) {
              return "ch3/restructured/" + view;
          }
    5. Add a member variable for the bean and annotate it with Autowired and Qualifier.
          @Autowired
          @Qualifier("requestDefaultBean")
          RequestData data;
    6. Add a method that returns the bean. The JSPs use this method to access the bean
          public RequestData getData() {
              return data;
          }
    7. Add a method that handles GET requests. The return value is the name of the view to display. The request object will be available throughout the method, simply by adding a parameter for it. Spring will bind the request object to the parameter when the method is called. The details for translating a button name to a view name will be done in the following step.
          @GetMapping
          public String doGet(HttpServletRequest request) {
              ...
              return address;
          }
    8. Add the code that will add the bean to the session and fills the bean from the request. This is the same code as was used in Chapter 3 to perform the same task.
          @GetMapping
          public String doGet(HttpServletRequest request) {
              
              request.getSession().setAttribute("data", data);
      
              data.setHobby(request.getParameter("hobby"));
              data.setAversion(request.getParameter("aversion"));
      
              ...
      
              return address;
          }
    9. Add the code that will translate a button name to a view name. This is the same code as was used in Chapter 3 to perform the same task.
          @GetMapping
          public String doGet(HttpServletRequest request) {
              
              request.getSession().setAttribute("data", data);
      
              data.setHobby(request.getParameter("hobby"));
              data.setAversion(request.getParameter("aversion"));
      
              String address;
      
              if (request.getParameter("processButton") != null) {
                  address = viewLocation("process");
              } else if (request.getParameter("confirmButton") != null) {
                  address = viewLocation("confirm");
              } else {
                  address = viewLocation("edit");
              }
      
              return address;
          }

    Views

    1. Recreate the views from the last chapter in the src/main/webapp/WEB-INF/views/ch3/restructured folder.
    2. Replace ${helper.data.hobby} with ${data.hobby} in each view.
    3. Rename the views so the first letter is lowercase.

    Execute

    1. Install the new packages with mvn clean install
    2. Run the application with mvn spring-boot:run
    3. Open a browser and enter the address for you server, including the path to the controller. localhost:8080/ch3/restructured/Controller
    4. If you have the spring.mvc.servlet.path=/boot-web set in the application properties, then the URL will be localhost:8080/boot-web/ch3/restructured/Controller
    5. This example is the simplest translation of the code from Chapter 3 into Spring. The next chapter will transform this code so that it uses many features of Spring.

    Testing

    1. Add a dependency for testing to the pom file. Omit old-style JUnit.
              <dependency>
                  <groupId>org.springframework.boot</groupId>
                  <artifactId>spring-boot-starter-test</artifactId>
                  <scope>test</scope>
                  <exclusions>
                      <exclusion>
                          <groupId>org.junit.vintage</groupId>
                          <artifactId>junit-vintage-engine</artifactId>
                      </exclusion>
                  </exclusions>
              </dependency>
    2. Test classes will be placed in the src/test/java folder. Create a test class for the bean named RequestDataDefaultTest with the same package name as is used in the bean. Mark a method with the BeforeEach annotation for initializing data.
      public class RequestDataDefaultRequestTest {
          
          RequestDataDefault data;
          
          @BeforeEach
          public void init() {
              data = new RequestDataDefault();
          }
      
          ...
      }

    Bean

    1. Add a test for the hobby. It uses the annotations ParmeterizedTest and CsvSource to set many values to test, instead of a separate test for each.
          @ParameterizedTest
          @CsvSource({
              "bowling, bowling",
              "skiing, skiing",
              ",Strange Hobby",
              "'', Strange Hobby",
              "time travel, Strange Hobby"
          })
          void testGetHobby(String value, String expected) {
              data.hobby = value;
              assertEquals(expected, data.getHobby());
          }
    2. Create a similar test for the aversion.
    3. Add a test for the validation method.
          @ParameterizedTest
          @CsvSource({
              "bowling, true",
              "skiing, true",
              ",false",
              "'', false",
              "time travel, false"
          })
          public void testIsValidHobby(String value, boolean valid) {
              data.hobby = value;
              assertEquals(valid, data.isValidHobby());
          }
    4. Add a similar test for the valid aversion method.
    5. Test the data with maven.
      mvn test

    Controller

    1. Create a class that configures the test environment with a mock custom scope container. Place the class in the folder that contains the data and controller test folders.
      public class TestConfig {
       
          @Bean
          public CustomScopeConfigurer customScopeConfigurer() {
              CustomScopeConfigurer configurer = new CustomScopeConfigurer();
              configurer.addScope("session", new SimpleThreadScope());
              configurer.addScope("request", new SimpleThreadScope());
              return configurer;
          }
          
      
      }
    2. Create a class to test the controller. It is annotated with SpringBootTest. The annotation does not need an argument if the Spring configuration file is in a parent folder. The annotation that configures a mock environment for the web is AutoConfigureMockMvc. The third annotation indicates the configuration file that initiated the custom scope container. It needs a parameter to locate the the configuration file.

      Allow Spring to autowire the bean and the mock MVC environment. Create two maps that hold values for the tests.

      @SpringBootTest
      @AutoConfigureMockMvc
      @Import({TestConfig.class})
      public class ControllerHelperTest {
      
          @Autowired
          MockMvc mockMvc;
      
          @Autowired
          @Qualifier("requestDefaultBean")
          RequestData data;
      
          private static final MultiValueMap requestParams = new LinkedMultiValueMap<>();
          private static final MultiValueMap nonsenseParams = new LinkedMultiValueMap<>();
    3. Add static variables and instance variables to hold test parameters.
          static String hobbyRequest, aversionRequest, suffix, prefix;
      
          String locationUrl;
          String controllerName;
          String expectedUrl;
          String expectedContent;
          String viewName;
          String buttonName;
          String buttonValue;
    4. The method annotated with BeforeAll must be static, so can only access static variables. The method annotated with BeforeEach has access to instance variables and is called before each test method. Most are values to compare against the expected values set by Spring.
          @BeforeAll
          private static void setupAll() {
              suffix = ".jsp";
              prefix = "/WEB-INF/views/";
              hobbyRequest = "Bowling";
              aversionRequest = "Gutters";
              requestParams.add("hobby", "Bowling");
              requestParams.add("aversion", "Gutters");
              nonsenseParams.add("none", "none");
          }
      
          @BeforeEach
          private void setupEach() {
              locationUrl = "/ch3/restructured/";
              controllerName = "Controller";
              expectedUrl = "ch3/restructured/";
              viewName = "edit";
              expectedContent = "Edit Page";
              buttonName = "none";
              buttonValue = "none";
              data.setHobby(null);
              data.setAversion(null);
          }
    5. Many tests are similar. Create a helper method that performs the central actions when a button is clicked.
          private void makeRequestTestContent(
                  String locationUrl,
                  String controllerName,
                  String expectedUrl,
                  String viewName,
                  String buttonName,
                  String buttonValue,
                  MultiValueMap passedParms
          ) throws Exception {
      
              mockMvc.perform(get(locationUrl + controllerName)
                  .param(buttonName, buttonValue)
                  .params(passedParms)
              ).andDo(print())
                  .andExpect(status().isOk())
                  .andExpect(forwardedUrl(prefix + expectedUrl + viewName + suffix))
                  .andDo(MockMvcResultHandlers.print());
          }
    6. Add a method to test the results of the doGet method when data is in the query string.
      
      
          @Test
          public void testDoGetConfirmWithButton() throws Exception {
              expectedUrl = "ch3/restructured/";
              viewName = "confirm";
              expectedContent = "Confirm Page";
              buttonName = "confirmButton";
              buttonValue = "Confirm";
              makeRequestTestContent(
                  locationUrl,
                  controllerName,
                  expectedUrl,
                  viewName,
                  buttonName,
                  buttonValue,
                  requestParams
              );  
              assertEquals(hobbyRequest, data.getHobby());
              assertEquals(aversionRequest, data.getAversion());
          }
    7. Add addtional test methods that verify the actions performed by each of the other buttons.

    Debugging Profile

    1. Add a profile section to the pom file.
    2. In a profile, include the spring-boot-maven-plugin, which can send arguments to the JVM.
    3. Configure the plugin so it sends the debugging commands from Chapter 1 to the JVM.
      <profiles>
        <profile>        
          <id>debug-suspend</id>
          <build>
            <plugins>
              <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId> 
                <configuration>
                  <jvmArguments>
                    -agentlib:jdwp=transport=dt_socket,server=y,address=8002,suspend=y
                  </jvmArguments>
                </configuration>
                </plugin>
            </plugins>
          </build>
    4. Enable the profile with maven.
      mvn -Pdebug-suspend spring-boot:run
    5. To run the application without debugging, omit the profile.
      mvn spring-boot:run

Contact the author