spring-web
web配置
在servlet 3.1以后,可以通过代码的方式来配置DispatcherServlet,而不用配置在web.xml中。在Servlet 3.0环境中,容器会在类路径中查找实现javax.servlet.ServletContainerInitializer接口的类,如果发现就会用他来配置Servlet容器。Spring提供了这个接口的实现SpringServletContainerInitializer,这个类又会查找实现了webAppInitializerClasses接口的类,在Spring 3.2中引入了AbstractAnnotationConfigDispatcherServletInitializer来实现该接口,而我们的SpitterWebInitializer继承AbstractAnnotationConfigDispatcherServletInitializer,所以在部署到servlet 3.0容器中时,容器就会自动发现它,并用它来配置servlet。
package com.cooper.spittr.config;
import com.cooper.spittr.web.WebConfig;
import org.springframework.web.servlet.support.AbstractAnnotationConfigDispatcherServletInitializer;
/**
* @description: SpitterWebInitializer
* @author: sunzhilong
* @create: 2021-04-26
**/
public class SpitterWebInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {
@Override
protected Class<?>[] getRootConfigClasses() {
return new Class[]{RootConfig.class};
}
@Override
protected Class<?>[] getServletConfigClasses() {
return new Class[]{WebConfig.class};
}
@Override
protected String[] getServletMappings() {
return new String[]{"/"};
}
}
这里有两个应用上下文DispatcherServlet和ContextLoaderListener。AbstractAnnotationConfigDispatcherServletInitializer会同时创建这两个应用上下文。getServletConfigClasses返回的带有@Configuration注解的类将会用来定义DispatcherServlet应用上下文中的bean,getRootConfigClasses返回的带有@Configuration注解的类将会用来配置ContextLoaderListener创建的应用上下文的bean。
WebConfig
- @Configuration说明这是一个配置类
- @ComponentScan(“com.cooper.spittr.web”)指明了配置类的扫描范围。
- @EnableWebMvc指明要启用Spring MVC。
package com.cooper.spittr.web;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.ViewResolver;
import org.springframework.web.servlet.config.annotation.DefaultServletHandlerConfigurer;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import org.springframework.web.servlet.view.InternalResourceViewResolver;
/**
* @description: WebConfig
* @author: sunzhilong
* @create: 2021-04-26
**/
@Configuration
@EnableWebMvc
@ComponentScan("com.cooper.spittr.web")
public class WebConfig implements WebMvcConfigurer {
// 配置JSP视图解析器,访问的视图解析器最终会加上前缀/WEB-INF/views/,后缀.jsp
@Bean
public ViewResolver viewResolver() {
InternalResourceViewResolver resolver = new InternalResourceViewResolver();
resolver.setPrefix("/WEB-INF/views/");
resolver.setSuffix(".jsp");
return resolver;
}
// 配置静态资源的处理,这样静态资源就会转发到默认的Servlet而不是DispatcherServlet上了
@Override
public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) {
configurer.enable();
}
}
RootConfig
- @Configuration说明这是一个配置类
- @ComponentScan说明了要扫描的范围
- 显然这里要扫描的范围com.cooper.spittr包含了com.cooper.spittr.web
- excludeFilters则将包含的com.cooper.spittr.web排除出去
package com.cooper.spittr.config;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.FilterType;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
/**
* @description: RootConfig
* @author: sunzhilong
* @create: 2021-04-26
**/
@Configuration
@ComponentScan(basePackages = {"com.cooper.spittr"},
excludeFilters = {
@ComponentScan.Filter(type = FilterType.ANNOTATION, value = EnableWebMvc.class)
})
public class RootConfig {
}
控制器
HomeController
实现
这里将/和/homepage的访问都映射到home上,访问这两个的get请求都会被映射成访问资源/WEB-INF/views/home.jsp
package com.cooper.spittr.web;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import static org.springframework.web.bind.annotation.RequestMethod.GET;
/**
* @description: HomeController
* @author: sunzhilong
* @create: 2021-04-27
**/
@Controller
@RequestMapping({"/", "/homepage"})
public class HomeController {
@RequestMapping(method = GET)
public String home(Model model) {
// 视图名是home
return "home";
}
}
测试
这里的测试方式与以前不太一样,这里是使用Spring MVC的方式进行测试的,有了对资源的get请求访问
package com.cooper.spittr.web;
import org.junit.Test;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
import org.springframework.test.web.servlet.result.MockMvcResultMatchers;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
public class HomeControllerTest {
@Test
public void testHomePage() throws Exception {
HomeController controller = new HomeController();
MockMvc mockMvc = MockMvcBuilders.standaloneSetup(controller).build();
mockMvc.perform(MockMvcRequestBuilders.get("/homepage"))
.andExpect(MockMvcResultMatchers.view().name("home"));
}
}
SpittleController
实现
- @Autowired用来在调用构造器的时候自动注入SpittleRepository
- spittles方法直接返回了一个查询结果,没有指定视图名,也没有现实的设定视图模型
- 处理器方法返回对象或者集合时,这个值会放到模型中,模型的key会根据类型推断得出,我们这里返回的是List,视图模型的key会被推断为spittleList
- 逻辑视图的名字会根据请求路径推断得出,这里请求路径是/spittles,因此视图名称会是spittles
- defaultValue 由于查询的蚕食都是String类型的值,defaultValue也需要时String,在绑定到方法的参数时会自动进行转化
- PathVariable 用来使访问的参数作为路径的一部分,而不是通过?的传参方式
package com.cooper.spittr.web;
import com.cooper.spittr.data.SpittleRepository;
import com.cooper.spittr.model.Spittle;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import java.util.List;
/**
* @description: SpittleController
* @author: sunzhilong
* @create: 2021-04-28
**/
@Controller
@RequestMapping("/spittles")
public class SpittleController {
private static final String MAX_LONG_AS_STRING = "9223372036854775807";
private SpittleRepository spittleRepository;
@Autowired
public SpittleController(SpittleRepository spittleRepository) {
this.spittleRepository = spittleRepository;
}
@RequestMapping(method = RequestMethod.GET)
public List<Spittle> spittles(
@RequestParam(value = "max", defaultValue = MAX_LONG_AS_STRING) long max,
@RequestParam(value = "count", defaultValue = "20") int count) {
return spittleRepository.findSpittles(max, count);
}
@RequestMapping(value = "/{spittleId}", method = RequestMethod.GET)
public Spittle spittle(
@PathVariable("spittleId") long spittleId) {
return spittleRepository.findOne(spittleId);
}
}
测试
这次的测试在MockMvc中设置了视图名,这样MockMvc就不会解析返回的视图名,之所以我们要在这里进行设置是因为访问的视图名和请求路径非常相似,MockMvc直接解析会发生失败。很多场景下是没有必要这样设置的。
package com.cooper.spittr.web;
import com.cooper.spittr.data.SpittleRepository;
import com.cooper.spittr.model.Spittle;
import org.hamcrest.CoreMatchers;
import org.junit.Test;
import org.mockito.Mockito;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
import org.springframework.test.web.servlet.result.MockMvcResultMatchers;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.web.servlet.view.InternalResourceView;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
public class SpittleControllerTest {
@Test
public void shouldShowRecentSpittlesDefault() throws Exception {
List<Spittle> expectedSpittles = createSpittleList(20);
SpittleRepository mockRepository = Mockito.mock(SpittleRepository.class);
Mockito.when(mockRepository.findSpittles(Long.MAX_VALUE, 20))
.thenReturn(expectedSpittles);
SpittleController controller = new SpittleController(mockRepository);
// 设置视图名
MockMvc mockMvc = MockMvcBuilders.standaloneSetup(controller)
.setSingleView(new InternalResourceView("/WEB-INF/views/spittles.jsp"))
.build();
// 断言视图名是spittles,key值是spittleList,value是expectedSpittles
mockMvc.perform(MockMvcRequestBuilders.get("/spittles"))
.andExpect(MockMvcResultMatchers.view().name("spittles"))
.andExpect(MockMvcResultMatchers.model().attributeExists("spittleList"))
.andExpect(MockMvcResultMatchers.model().attribute("spittleList",
CoreMatchers.hasItems(expectedSpittles.toArray())));
}
@Test
public void shouldShowRecentSpittlesSpecial() throws Exception {
List<Spittle> expectedSpittles = createSpittleList(50);
SpittleRepository mockRepository = Mockito.mock(SpittleRepository.class);
Mockito.when(mockRepository.findSpittles(23566, 50))
.thenReturn(expectedSpittles);
SpittleController controller = new SpittleController(mockRepository);
MockMvc mockMvc = MockMvcBuilders.standaloneSetup(controller)
.setSingleView(new InternalResourceView("/WEB-INF/views/spittles.jsp"))
.build();
mockMvc.perform(MockMvcRequestBuilders.get("/spittles?max=23566&count=50"))
.andExpect(MockMvcResultMatchers.view().name("spittles"))
.andExpect(MockMvcResultMatchers.model().attributeExists("spittleList"))
.andExpect(MockMvcResultMatchers.model().attribute("spittleList",
CoreMatchers.hasItems(expectedSpittles.toArray())));
}
@Test
public void testSpittle() throws Exception {
Spittle expectedSpittle = new Spittle("Hello", new Date());
SpittleRepository mockRepository = Mockito.mock(SpittleRepository.class);
Mockito.when(mockRepository.findOne(12345)).thenReturn(expectedSpittle);
SpittleController controller = new SpittleController(mockRepository);
MockMvc mockMvc = MockMvcBuilders.standaloneSetup(controller).build();
// 这里没有设置视图名,推断出返回的视图名就会是spittles/12345
mockMvc.perform(MockMvcRequestBuilders.get("/spittles/12345"))
.andExpect(MockMvcResultMatchers.view().name("spittles/12345"))
.andExpect(MockMvcResultMatchers.model().attributeExists("spittle"))
.andExpect(MockMvcResultMatchers.model().attribute("spittle", expectedSpittle));
}
private List<Spittle> createSpittleList(int count) {
List<Spittle> spittles = new ArrayList<Spittle>();
for (int i=0; i < count; i++) {
spittles.add(new Spittle("Spittle " + i, new Date()));
}
return spittles;
}
}
SpitterController
实现
- POST 提交表单数据必须用这种方法
- 这里使用了Valid进行数据的校验
- @Valid 指明要校验的属性
- Errors 必须根子校验参数后边,用来接收校验结果,如果有多个参数校验就要有多个Errors 跟随
- 值得一提的是校验用到的validation-api和hibernate-validator是有版本配套关系的,我在hibernate-validator使用6.1.5和7.0.1时都进行了实验,是不能通过测试用例的
- redirect 是重定向的意思,是一个get方法
package com.cooper.spittr.web;
import com.cooper.spittr.data.SpitterRepository;
import com.cooper.spittr.model.Spitter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.Errors;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import javax.validation.Valid;
import static org.springframework.web.bind.annotation.RequestMethod.GET;
import static org.springframework.web.bind.annotation.RequestMethod.POST;
/**
* @description: SpitterController
* @author: sunzhilong
* @create: 2021-04-29
**/
@Controller
@RequestMapping("/spitter")
public class SpitterController {
private SpitterRepository spitterRepository;
@Autowired
public SpitterController(SpitterRepository spitterRepository) {
this.spitterRepository = spitterRepository;
}
@RequestMapping(value = "/register", method = GET)
public String showRegistrationForm() {
return "registerForm";
}
@RequestMapping(value = "/register", method = POST)
public String processRegistration(@Valid Spitter spitter, Errors errors) {
if (errors.hasErrors()) {
return "registerForm";
}
spitterRepository.save(spitter);
return "redirect:/spitter/" + spitter.getUsername();
}
@RequestMapping(value = "/{username}", method = GET)
public String showSpitterProfile(@PathVariable String username, Model model) {
Spitter spitter = spitterRepository.findByUsername(username);
model.addAttribute(spitter);
return "profile";
}
}
测试
最后一个测试用例,测试了不符合校验参数的情况
package com.cooper.spittr.web;
import com.cooper.spittr.data.SpitterRepository;
import com.cooper.spittr.model.Spitter;
import org.junit.Test;
import org.mockito.Mockito;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
import org.springframework.test.web.servlet.result.MockMvcResultMatchers;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
/**
* @description: SpitterControllerTest
* @author: sunzhilong
* @create: 2021-04-30
**/
public class SpitterControllerTest {
@Test
public void shouldShowRegistration() throws Exception {
SpitterRepository mockRepository = Mockito.mock(SpitterRepository.class);
SpitterController controller = new SpitterController(mockRepository);
MockMvc mockMvc = MockMvcBuilders.standaloneSetup(controller).build();
mockMvc.perform(MockMvcRequestBuilders.get("/spitter/register"))
.andExpect(MockMvcResultMatchers.view().name("registerForm"));
}
@Test
public void shouldProcessRegistration() throws Exception {
SpitterRepository mockRepository = Mockito.mock(SpitterRepository.class);
Spitter unsaved = new Spitter("jbauer", "24hours", "Jack", "Bauer", "jbauer@ctu.gov");
Spitter saved = new Spitter(24L, "jbauer", "24hours", "Jack", "Bauer", "jbauer@ctu.gov");
Mockito.when(mockRepository.save(unsaved)).thenReturn(saved);
SpitterController controller = new SpitterController(mockRepository);
MockMvc mockMvc = MockMvcBuilders.standaloneSetup(controller).build();
mockMvc.perform(MockMvcRequestBuilders.post("/spitter/register")
.param("firstName", "Jack")
.param("lastName", "Bauer")
.param("username", "jbauer")
.param("password", "24hours")
.param("email", "jbauer@ctu.gov"))
.andExpect(MockMvcResultMatchers.redirectedUrl("/spitter/jbauer"));
Mockito.verify(mockRepository, Mockito.atLeastOnce()).save(unsaved);
}
@Test
public void shouldFailValidationWithNoData() throws Exception {
SpitterRepository mockRepository = Mockito.mock(SpitterRepository.class);
SpitterController controller = new SpitterController(mockRepository);
MockMvc mockMvc = MockMvcBuilders.standaloneSetup(controller).build();
mockMvc.perform(MockMvcRequestBuilders.post("/spitter/register"))
.andExpect(MockMvcResultMatchers.status().isOk())
.andExpect(MockMvcResultMatchers.view().name("registerForm"))
.andExpect(MockMvcResultMatchers.model().errorCount(5))
.andExpect(MockMvcResultMatchers.model().attributeHasFieldErrors(
"spitter", "firstName", "lastName", "username", "password", "email"));
}
}
模型
Spitter
这里即使不用email的校验,最终也要在maven配置中配置上hibernate-validator
package com.cooper.spittr.model;
import org.apache.commons.lang3.builder.EqualsBuilder;
import org.apache.commons.lang3.builder.HashCodeBuilder;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;
/**
* @description: Spitter
* @author: sunzhilong
* @create: 2021-04-29
**/
public class Spitter {
private Long id;
@NotNull
@Size(min = 5, max = 16)
private String username;
@NotNull
@Size(min = 5, max = 25)
private String password;
@NotNull
@Size(min = 2, max = 30)
private String firstName;
@NotNull
@Size(min = 2, max = 30)
private String lastName;
@NotNull
// @Email
private String email;
public Spitter() {
}
public Spitter(String username, String password, String firstName, String lastName, String email) {
this(null, username, password, firstName, lastName, email);
}
public Spitter(Long id, String username, String password, String firstName, String lastName, String email) {
this.id = id;
this.username = username;
this.password = password;
this.firstName = firstName;
this.lastName = lastName;
this.email = email;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getFirstName() {
return firstName;
}
public void setFirstName(String firstName) {
this.firstName = firstName;
}
public String getLastName() {
return lastName;
}
public void setLastName(String lastName) {
this.lastName = lastName;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
@Override
public boolean equals(Object that) {
return EqualsBuilder.reflectionEquals(this, that, "firstName", "lastName", "username", "password", "email");
}
@Override
public int hashCode() {
return HashCodeBuilder.reflectionHashCode(this, "firstName", "lastName", "username", "password", "email");
}
}
Spittle
package com.cooper.spittr.model;
import org.apache.commons.lang3.builder.EqualsBuilder;
import org.apache.commons.lang3.builder.HashCodeBuilder;
import java.util.Date;
/**
* @description: Spittle
* @author: sunzhilong
* @create: 2021-04-28
**/
public class Spittle {
private final Long id;
private final String message;
private final Date time;
private Double latitude;
private Double longitude;
public Spittle(String message, Date time) {
this(null, message, time, null, null);
}
public Spittle(Long id, String message, Date time, Double longitude, Double latitude) {
this.id = id;
this.message = message;
this.time = time;
this.longitude = longitude;
this.latitude = latitude;
}
public long getId() {
return id;
}
public String getMessage() {
return message;
}
public Date getTime() {
return time;
}
public Double getLongitude() {
return longitude;
}
public Double getLatitude() {
return latitude;
}
@Override
public boolean equals(Object that) {
return EqualsBuilder.reflectionEquals(this, that, "id", "time");
}
@Override
public int hashCode() {
return HashCodeBuilder.reflectionHashCode(this, "id", "time");
}
}
持久化接口定义
这里并没有对持久化接口的实现,测试用例中都是mock了实现方法
package com.cooper.spittr.data;
import com.cooper.spittr.model.Spitter;
/**
* @description: SpitterRepository
* @author: sunzhilong
* @create: 2021-04-30
**/
public interface SpitterRepository {
Spitter save(Spitter spitter);
Spitter findByUsername(String username);
}
package com.cooper.spittr.data;
import com.cooper.spittr.model.Spittle;
import java.util.List;
/**
* @description: SpittleRepository
* @author: sunzhilong
* @create: 2021-04-28
**/
public interface SpittleRepository {
List<Spittle> findSpittles(long max, int count);
Spittle findOne(long id);
}