持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第5天,点击查看活动详情
- 拦截器
- 全局声明
- 文件上传/下载
- HttpMessageConverter
- SpringMvc的测试;
SpringMvc 除了常用的Controller 控制器以外,还有很多组件是需要我们了解的。
自定义拦截器
拦截器可以处理在请求前后的业务逻辑,类似 Servlet 的Filter。实现上,一般是继承 HandlerInterceptorAdapter 类 或者 实现 HandlerInterceptor 即可实现自定义的拦截器。
然后通过在实现 WebMvcConfigurer 的实现类中重写 addInterceptors 方法,将自定义的拦截器bean注册到SpringMvc的拦截器链中。
- 继承 HandlerInterceptorAdapter 类,自定义拦截器
public class XssInterceptor extends HandlerInterceptorAdapter {
/**
* 请求前处理
*/
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
System.out.println("拦截器前置方法");
long beginTime = System.currentTimeMillis();
request.setAttribute("beginTime",beginTime);
return super.preHandle(request, response, handler);
}
/**
* 请求后处理
* @param request
* @param response
* @param handler 执行方法
* @param modelAndView 模型视图
* @throws Exception
*/
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
long beginTime = (Long)request.getAttribute("beginTime");
request.removeAttribute("beginTime");
System.out.println("请求耗时 :" + (System.currentTimeMillis() - beginTime) );
}
}
- 在mvc的配置类中,重写addInterceptor 方法,注册自定义的拦截器;
@EnableWebMvc
@Configuration
@ComponentScan(value = "com.example.controller")
public class WebMvcConfig implements WebMvcConfigurer {
/**
* 视图解析器的bean配置
* @return
*/
@Bean
public ViewResolver viewResolver() {
System.out.println("viewResolver init");
InternalResourceViewResolver resolver = new InternalResourceViewResolver();
/**
* 如果需要在jsp中使用jstl标签的话,需要加上这个视图,且要用这个引用,否则会报找不到方法的500错误
* 项目中使用JSTL,SpringMVC会把视图由InternalView转换为JstlView。
* 若使用Jstl的fmt标签,需要在SpringMVC的配置文件中配置国际化资源文件。
* 需要引入 jstl.jar和standard.jar
*/
// resolver.setViewClass(org.springframework.web.servlet.view.JstlView.class);
resolver.setPrefix("/WEB-INF/page/");
resolver.setSuffix(".jsp");
resolver.setExposeContextBeansAsAttributes(true);
return resolver;
}
@Bean
public XssInterceptor initXssInterceptor() {
return new XssInterceptor();
}
@Override
public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) {
configurer.enable();
}
/**
* 静态资源的配置
*/
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
System.out.println("addResourceHandlers init....");
registry.addResourceHandler("/css/**").addResourceLocations("/WEB-INF/statics/css/");
registry.addResourceHandler("/js/**").addResourceLocations("/WEB-INF/statics/js/");
registry.addResourceHandler("/image/**").addResourceLocations("WEB-INF/statics/image/");
}
/**
* 注册拦截器
*/
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(initXssInterceptor());
}
}
全局声明配置
通过使用注解@ControllerAdvice 可以将对于控制器的全局配置都放在一个位置里,注解了@Controller 的类的方法可使用@ExceptionHandler、@InitBinder、@ModelAttritbute 注解到方法上,这对所有注解了@RequestMapping 的控制器内的方法有效。
- @ExceptionHandler:用于全局处理控制器里的异常;
- @InitBinder :用来设置 WebDataBinder,WebDataBinder 用来自动绑定前台请求参数到Model 中;
- @ModelAttribute:用于绑定键值对到Model对象中,在 @ControllerAdvice中注解表示所有的@RequestMappering注解的方法都能获取到该键值对;
常用于处理全局异常的配置。
文件的上传与下载
SpringMvc有着文件上传解析的接口,MultipartResolver
,里面定义了判断请求是否为文件上传且解析Http请求为文件类型。 默认有两个实现:CommonsMultipartResolver
和 StandardServletMultipartResolver
。具体可在MultipartResolver
接口中查看注释。
这里以CommonsMultipartResolver
实现来写一个demo。
- 引入依赖
CommonsMultipartResolver
实现依赖于commons-fileupload包,需要额外引入
<!-- 上传 -->
<dependency>
<groupId>commons-fileupload</groupId>
<artifactId>commons-fileupload</artifactId>
<version>1.3.1</version>
</dependency>
<!-- 非必须,方便文件的写入-->
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.8.0</version>
</dependency>
- 配置文件上传解析的bean
这里用了CommonsMultipartResolver实现
@Bean
public MultipartResolver multipartResolver() {
CommonsMultipartResolver resolver = new CommonsMultipartResolver();
resolver.setMaxUploadSize(100 * 1024 * 1024); // 10M
return resolver;
}
- 编写上传接口
@Controller
public class UploadController {
/**
* 页面跳转
*/
@RequestMapping(value = "/toUpload")
public String toUpload() {
return "upload";
}
/**
* 上传接口
* @param file 使用 MultipartFile 作为参数属性,接收multipart/form-data类型的参数文件
*/
@ResponseBody
@RequestMapping(value = "/upload",method = RequestMethod.POST)
public String uploadFile(MultipartFile file) {
try {
FileUtils.writeByteArrayToFile(new File("E:\\code\\log\\" + file.getOriginalFilename()),file.getBytes());
return "ok";
}catch (Exception e) {
e.printStackTrace();
return "wrong";
}
}
}
- 前端页面测试
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<body>
<div class="icon-upload">
<form action="upload" enctype="multipart/form-data" method="post">
<input type="file" name="file"/><br/>
<input type="submit" value="上传"/>
</form>
</div>
</body>
</html>
原理可在前端控制器DispatcherServlet
中查看控制流程。简单来说就是会先判断是否存在 multipartResolver
的bean,然后判断该请求是否为文件上传,如果是的话,将文件解析为参数。然后就是SpringMvc的请求流程,如先找到HandlerAdpter、执行各拦截器的方法等。最后会清除用于文件上传的资源,例如用于存储上传文件的存储。(貌似是存储在硬盘的某个临时文件中的)
HttpMessageConverter
HttpMessageConverter 是用来处理request和reponse里的数据的。Spring内部有大量的HttpMessageConverter实现类,在WebMvcConfigurationSupport#addDefaultHttpMessageConverters中可以看到。常用来解决请求时参数对方法参数的转换等。
自定义HttpMessageConverter
假设场景是解析特定字符串为某个对象,如参数的格式是x-y这样的格式,而在接口方法中需要转换为DemoObj(x,y)这样的实例对象,那么就需要自定义转换器类实现了。
1. 继承AbstractHttpMessageConverter
public class DemoMessageConverter extends AbstractHttpMessageConverter<DemoObj> {
@Override
protected boolean supports(Class<?> clazz) {
return DemoObj.class.isAssignableFrom(clazz); // 判断当前参数对象是否为DemoObj这个类
}
/**、
* 处理请求数据,即将x-y转为DemoObj对象
*/
@Override
protected DemoObj readInternal(Class<? extends DemoObj> clazz, HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException {
String tmp = StreamUtils.copyToString(inputMessage.getBody(), Charset.forName("UTF-8")); // 将请求体转为String类型
String[] tmpArr = tmp.split("-"); // 该请求参数的格式为x-y
return new DemoObj(Integer.parseInt(tmpArr[0]),tmpArr[1]); // 转换为目标对象
}
/**
* 处理返回数据,即将DemoObj转为 x-y 的格式
*/
@Override
protected void writeInternal(DemoObj demoObj, HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException {
String result = demoObj.getId() + "-" + demoObj.getName();
outputMessage.getBody().write(result.getBytes());
}
}
2. WebMvcConfig下新增消息转换器
这里用了Java配置,且用了extendMessageConverters方法新增消息转换器。 注册转换器还有个方法:configureMessageConverters,该方法会覆盖掉SpringMvc默认注册的多个HttpMessageConverter。
@EnableWebMvc
@Configuration
@ComponentScan(value = "com.example.controller")
public class WebMvcConfig implements WebMvcConfigurer {
//... 省略其它配置
/**
* 新增消息处理器
* @param converters
*/
@Override
public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
converters.add(demoMessageConverter());
}
@Bean
public DemoMessageConverter demoMessageConverter() {
return new DemoMessageConverter();
}
}
3. Controller
@RestController
public class ConvertDemoController {
@RequestMapping(value = "/convert")
public DemoObj convert(@RequestBody DemoObj demoObj) {
System.out.println("请求demo转换器");
return demoObj;
}
}
类型转换器Convertor
Convertor 是SpringMvc 提供给我们使用的类型转换器接口,通过自定义的类型转换器就可实现在请求之前对参数进行类型转换;
自定义类型转换器
假设需要自定义请求的格式,需要将特殊的String字符串解析成Java Bean,那么就可以利用自定义类型转换器做请求参数的解析并转化为bean。
1. 实现Converter接口
实现 Formatter 接口,重写parse 和 print 方法。
Formatter 只能将String 转换为另一种Java类型。
JavaBean的格式是这样的。
public class Student {
private int sid;
private String name;
private int age;
private LocalDate createTime;
// 省略 set get...
}
1. 实现 Formatter
-
StringToLocalDateFormatter 类型转换器
目的是将请求参数中的String日期转换为LocalDate的类型。
public class StringToLocalDateFormatter implements Formatter<LocalDate> {
private DateTimeFormatter formatter;
private String datePattern;
public StringToLocalDateFormatter(String datePattern) {
this.datePattern = datePattern;
this.formatter = DateTimeFormatter.ofPattern(datePattern);
}
/**
* 利用指定的 Local 将一个String解析成目标类型
*/
@Override
public LocalDate parse(String source, Locale time) throws ParseException {
System.out.println("StringToLocalDateFormatter converter..");
return LocalDate.parse(source,DateTimeFormatter.ofPattern(datePattern));
}
@Override
public String print(LocalDate date, Locale locale) {
return date.format(formatter);
}
}
2. Java配置新增自定义的类型转换器
@EnableWebMvc
@Configuration
@ComponentScan(value = "com.example.controller")
public class WebMvcConfig implements WebMvcConfigurer {
@Bean
public StringToLocalDateFormatter stringToLocalDateFormatter() {
return new StringToLocalDateFormatter("yyyy-MM-dd");
}
@Override
public void addFormatters(FormatterRegistry registry) {
registry.addFormatter(stringToLocalDateFormatter());
}
}
3. 接口请求
@RestController
@RequestMapping("/student")
public class StudentController {
@GetMapping(value = "/converter")
public String getStudentInfo(Student student) {
System.out.println(student);
return "success:" + student;
}
}
当使用 localhost:8080/student/converter?sid=122&name=zhangsan&age=34&createTime=2021-11-07 访问接口时,即可将String 的日期转换为LocalDate的格式,同时注入Student的对象中。
相关Resolver
Resolver 解析器,除开常见的视图解析器(InternalResourceViewResolver
) 以外,参数解析器 以及 返回值解析器也是SpringMvc 常用的组件。
HandlerMethodArgumentResolver
请求参数解析器,包含以下两个方法:
- supportsParameter:判断是否支持解析;返回true表示进入解析方法;
- resolveArgument:解析参数,注入值;
注:在常用的HandlerAdapter:RequestMappingHandlerAdapter#getDefaultArgumentResolvers
方法中可以看到很多默认的参数解析器。
以获取用户登录态为例
1. 自定义注解
@Login,方法注解,注解在方法上表明需要进行登录拦截
@Target(value ={ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Login {
}
@LoginUser,参数注解,用于解析请求注入到方法参数
/**
* 登录用户信息
*/
@Target(value = {ElementType.PARAMETER}) //作用在参数上
@Retention(RetentionPolicy.RUNTIME) //运行时检查
public @interface LoginUser {
}
2. 自定义拦截器
自定义权限(token)拦截器,拦截方法上注解了 @Login的方法,解析请求头的数据,放入到请求属性中,用于后面的参数解析注入;
/**
* 权限(token)验证
*/
@Component
public class AuthorizationInterceptor extends HandlerInterceptorAdapter {
@Autowired
private JwtUtils jwtUtils;
public static final String USER_KEY = "userId";
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
Login annotation;
if(handler instanceof HandlerMethod) {
annotation = ((HandlerMethod) handler).getMethodAnnotation(Login.class);
}else{
return true;
}
if(annotation == null){
return true;
}
//获取用户凭证
String token = request.getHeader(jwtUtils.getHeader());
if(StringUtils.isBlank(token)){
token = request.getParameter(jwtUtils.getHeader());
}
//凭证为空
if(StringUtils.isBlank(token)){
throw new RRException(jwtUtils.getHeader() + "不能为空", HttpStatus.UNAUTHORIZED.value());
}
Claims claims = jwtUtils.getClaimByToken(token);
if(claims == null || jwtUtils.isTokenExpired(claims.getExpiration())){
throw new RRException(jwtUtils.getHeader() + "失效,请重新登录", HttpStatus.UNAUTHORIZED.value());
}
//设置userId到request里,后续根据userId,获取用户信息
request.setAttribute(USER_KEY, Integer.valueOf(claims.getSubject()));
return true;
}
}
3. 自定义方法参数解析器
/**
* 有@LoginUser注解的方法参数,注入当前登录用户
*/
@Component
@Slf4j
public class LoginUserHandlerMethodArgumentResolver implements HandlerMethodArgumentResolver {
@Autowired
private UserService userService;
@Override
public boolean supportsParameter(MethodParameter parameter) {
return parameter.getParameterType().isAssignableFrom(User.class) && parameter.hasParameterAnnotation(LoginUser.class);
}
@Override
public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer container,
NativeWebRequest request, WebDataBinderFactory factory) throws Exception {
//获取用户ID
Object object = request.getAttribute(AuthorizationInterceptor.USER_KEY, RequestAttributes.SCOPE_REQUEST);
if(object == null){
return null;
}
log.info("获取到的用户ID为{}",object);
//获取用户信息
User user = userService.getUserById((Integer)object);
return user;
}
}
- mvc配置 自定义拦截器和参数解析器
@Configuration
public class WebMvcConfig extends WebMvcConfigurerAdapter {
@Autowired
private AuthorizationInterceptor authorizationInterceptor;
@Autowired
private LoginUserHandlerMethodArgumentResolver loginUserHandlerMethodArgumentResolver;
@Override
public void addInterceptors(InterceptorRegistry registry) {
// 拦截user路径下的请求
registry.addInterceptor(authorizationInterceptor).addPathPatterns("/user/**");
}
@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) {
argumentResolvers.add(loginUserHandlerMethodArgumentResolver);
}
}
- 在Handler上使用注解,实现用户登录态的自动注入;
@Login
@GetMapping(value = "/user/devices")
public R getDeviceList(@LoginUser User user){
// do something
}
注解了 @LoginUser 的 User对象,将从请求头中自动注入当前登录态的用户信息。
HandlerMethodReturnValueHandler
返回值解析处理器
在Controller方法中加上@ResponseBody可以将返回值解析为Json 格式,而这即是实现了HandlerMethodReturnValueHandler
接口的。
实现类在RequestResponseBodyMethodProcessor
public interface HandlerMethodReturnValueHandler {
/**
* 是否支持该类型,当返回true时表示使用该实现
* param returnType:返回类型
**/
boolean supportsReturnType(MethodParameter returnType);
/***
* 当supportsReturnType返回true时调用该方法,处理返回值
*/
void handleReturnValue(@Nullable Object returnValue, MethodParameter returnType,
ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws Exception;
}
测试
SpringMvc的测试需要Spring的上下文,以及Servlet相关的一些模拟对象,如MockMvc、MockHttpServletRequest、MockHttpServletResponse、MockHttpSession等。
添加依赖
<!-- 测试 -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>${spring-version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>
相关注解
-
@RunWith(SpringJUnit4ClassRunner.class)
表示使用Spring集成的Juint4测试。
-
@ContextConfiguration(locations = "classpath:application-context.xml")
表示指定Spring上下文的环境,这里指定的xml文件。也可以指向Java配置。
测试
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = "classpath:application-context.xml")
@WebAppConfiguration // 表示加载的ApplicationContext是一个WebApplicationContext
public class StudentControllerTest {
private MockMvc mockMvc; // 模拟的mvc对象
@Autowired
private StudentService service;
@Autowired
WebApplicationContext wac; // web的应用上下文
@Autowired
MockHttpSession session;
@Autowired
MockHttpServletRequest request;
@Before
public void setUp() {
this.mockMvc = MockMvcBuilders.webAppContextSetup(this.wac).build();
}
@Test
public void testIndexPage() throws Exception {
mockMvc.perform(MockMvcRequestBuilders.get("/index"))
.andExpect(MockMvcResultMatchers.status().isOk())
.andExpect(MockMvcResultMatchers.view().name("page"));
}