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
-
Copy the command line application above to a new folder.
- Edit the
pom.xml .
-
Change the artifact ID to
boot-web
<artifactId>boot-web</artifactId>
-
Change the name to
boot-web
<name>boot-web</name>
- Add the dependency for the starter-web to the pom file.
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
- Add the dependency for the starter-tomcat to the pom file.
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
</dependency>
- Edit the
SimpleBean.java file in the com/bytesizebook folder.
- 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.
-
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 {
...
}
-
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);
}
}
-
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 .
-
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.
- Modify the class with the
@Controller annotation.
@Controller
public class Index {
...
}
-
Public web pages for the application belong in the
src/main/webapp folder.
- Create the
webapp folder in src/main folder.
- 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>
-
Additional resources for the application belong in the
src/main/resources folder.
-
Create the resources folder.
-
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.
- 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
-
Start the application with the maven command:
mvn spring-boot:run
- 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.
-
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;
}
- 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.
- 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();
}
- 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;
}
}
- 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();
}
...
- 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>
- Install the new packages with the maven command
mvn clean install
- 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
- Create a folder named
data/ch3/restructured in the same folder that
contains the configuration file.
- 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();
}
- 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.
- 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
- 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.
- 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 {
...
}
- 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 {
...
}
- 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;
}
-
Add a member variable for the bean and annotate it with
Autowired
and Qualifier .
@Autowired
@Qualifier("requestDefaultBean")
RequestData data;
- Add a method that returns the bean. The JSPs use this method to access the bean
public RequestData getData() {
return data;
}
- 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;
}
-
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;
}
-
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
- Recreate the views from the last chapter in the
src/main/webapp/WEB-INF/views/ch3/restructured folder.
- Replace
${helper.data.hobby} with ${data.hobby}
in each view.
-
Rename the views so the first letter is lowercase.
Execute
- Install the new packages with
mvn clean install
- Run the application with
mvn spring-boot:run
- Open a browser and enter the address for you server, including the path
to the controller.
localhost:8080/ch3/restructured/Controller
-
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
-
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
-
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>
-
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
-
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());
}
-
Create a similar test for the aversion.
-
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());
}
-
Add a similar test for the valid aversion method.
- Test the data with maven.
mvn test
Controller
-
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;
}
}
-
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<>();
-
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;
-
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);
}
-
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());
}
-
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());
}
- Add addtional test methods that verify the actions performed by
each of the other buttons.
Debugging Profile
-
Add a profile section to the pom file.
-
In a profile, include the
spring-boot-maven-plugin , which
can send arguments to the JVM.
-
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>
- Enable the profile with maven.
mvn -Pdebug-suspend spring-boot:run
- To run the application without debugging, omit the profile.
mvn spring-boot:run
|