Merikanto

一簫一劍平生意,負盡狂名十五年

Spring Boot - 02 MVC

This is the second post in a series of posts to cover different aspects of Spring Boot. Please note that the entire post isn’t necessarily only written in English.

In this post, I am going to cover the basics of Spring Web MVC.



Web MVC

How to display model data & process form input


Template view:

  • JSP pages (main/WEB-INF)

  • Thymeleaf (main/resources/templates)


Disable template cache:

  • spring.thymeleaf.cache = false (add this to application.properties)

    ​ (Remember to set back to True in production)

  • Use DevTools (auto disable cache in dev env, auto enable in production env)



Domain

Debug: Also need to add lombok extension in IDE.


Lombok: Auto generate getter & setter methods and constructors, based on all final properties

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Data

// 这个 annotation 可以不要,因为 @Data 会自动创建相关 constructor
@RequiredArgsConstructor
public class Ingredient {

private final String id;
private final String name;
private final Type type;

public static enum Type {
WRAP, PROTEIN, VEGGIES, CHEESE, SAUCE
}
}

Related dependency:

1
2
3
4
5
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>


Controller

Handle HTTP requests from client


Handling GET requests

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// Logger
@Slf4j

// Component scan
@Controller

// at class level, specify the path
@RequestMapping("/design")
public class DesignTacoController {

// Only use @RequestMapping at class level. Use new annotations here
@GetMapping
public String showDesignForm(Model model) {

List<Ingredient> ingredients = Arrays.asList(
new Ingredient("FLTO", "Flour Tortilla", Type.WRAP),
new Ingredient("SRCR", "Sour Cream", Type.SAUCE) );

Type[] types = Ingredient.Type.values();
for (Type type : types)
model.addAttribute(type.toString().toLowerCase(), filterByType(ingredients, type));

model.addAttribute("design", new Taco());

// return view name "design" (ressources/templates/design.html)
return "design";
}

private List<Ingredient> filterByType(List<Ingredient> ingredients, Type type) {
return ingredients.stream()
.filter(x -> x.getType().equals(type))
.collect(Collectors.toList());
}
}

@Slf4j: Simple Logging Facade for Java.

​ Lombok-provided annotation, auto generate logger at runtime


@GetMapping & class-level @RequestMapping:

  • When /design receives a HTTP GET request, showDesignForm() will be called to handle the request

  • @GetMapping: New in Spring 4.3. Old method:

    1
    2
    // lazy way, leave off the method attribute
    @RequestMapping(method = RequestMethod.GET)
  • All request-mapping annotations:

@RequestMapping

@GetMapping

@PostMapping

@PutMapping

@DeleteMapping

@PatchMapping


Model: an object that transfers data between controller & view. ??

​ Data placed in Model attributes is copied into the servlet response attributes, where the view can find them


Handling POST request

Fields in the form are bound to properties of a Taco object

1
2
3
4
5
6
7
8
9
10
11
@PostMapping

// Taco passed as a parameter in processDesign
public String processDesign(Taco taco) {

// log for input data processing
log.info("Processing design: " + taco);

// redirection,直接跳转到 /orders/current
return "redirect:/orders/current";
}


Validate Form Input

The clumsy way: litter the controller with a bunch of if / then


Recommended: Java Bean Validation API (JSR-303)

Validation API & its Hibernate implementation are auto added to the project as transient dependencies in the web starter.


Steps to apply validation:

  • Declare validation rules in domain classes
  • Specify validation should be performed in controller methods with @PostMapping
  • Modify views to display validation errors

Example 1

Taco domain:

1
2
3
4
5
6
7
8
9
10
@Data
public class Taco {

@NotNull
@Size(min=5, message="Name must be at least 5 characters long")
private String name;

@Size(min=1, message="You must choose at least 1 ingredient")
private List<String> ingredients;
}

Taco controller (添加 validation):

1
2
3
4
5
6
7
8
9
10
11
12
13
@PostMapping

// 这里不一样
public String processDesign(@Valid Taco taco, Errors errors) {

// 还有这里,如果出错,redisplay the form
if (errors.hasErrors())
return "design";

// 如果没有出错,再用 log
log.info("Processing design: " + taco);
return "redirect:/orders/current";
}

Example 2

Order excerpt:

1
2
3
4
5
6
7
8
9
10
11
// Only make sure this isn't blank
@NotBlank(message="Name is required")
private String name;

// Annotation here: Hibernate 自带! —— 算法: Luhn algorithm check
@CreditCardNumber(message="Not a valid credit card number")
private String ccNumber;

// Note the regex here! For validating Credit Card exp. date!
@Pattern(regexp="^(0[1-9]|1[0-2])([\\/])([1-9][0-9])$", message="Must be formatted MM/YY")
private String ccExpiration;

Order controller:

1
2
3
4
5
6
7
8
9
10
@PostMapping
public String processDesign(@Valid Taco taco, Errors errors) {

// 这一句是模板
if (errors.hasErrors())
return "design";

log.info("Processing design: " + taco);
return "redirect:/orders/current";
}


View Controllers

The controllers written so far:

  • All annotated with @Controller: auto discovered by Spring component scanning, and instantiated as beans in the context
  • All annotated with @GetMapping, @PostMapping: specify which method should handle what requests

A view controller: does not process data / input, only forward the GET request to a view

Create a new config class for each kind of configuration (e.g. web, data, security)

1
2
3
4
5
6
7
8
9
10
11
12
13
@Configuration

// WebMvcConfigurer interface
public class WebConfig implements WebMvcConfigurer {

// need to override
@Override

// void!
public void addViewControllers(ViewControllerRegistry registry) {
registry.addViewController("/").setViewName("home");
}
}
  • Registry: register one or more view controllers