Skip to content

SpringMVC教程 - 18 全注解开发

在 Servlet 2.5 及以前,Web 容器不会扫描你的类路径,你的项目中有没有 Servlet、Filter、Listener 它都不知道。

只能通过 web.xml 告诉容器:我有哪些组件。

例如必须这样声明:

xml
<!-- DispatcherServlet 配置 -->
<servlet>
    <servlet-name>springmvc</servlet-name>
    <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
</servlet>

<servlet-mapping>
    <servlet-name>springmvc</servlet-name>
    <url-pattern>/</url-pattern>
</servlet-mapping>

而从 Servlet 3.0 开始,容器启动时会自动扫描 classpath 中的类:

  • 扫描是否存在 @WebServlet
  • 扫描是否存在 @WebFilter
  • 扫描是否存在 @WebListener
  • 扫描是否存在实现 ServletContainerInitializer 接口的类

也就是说 web.xml 已经不是必须的了,Spring 提供了一个 SpringServletContainerInitializer 类,它就实现了 ServletContainerInitializer 接口,所以在容器启动时,就会调用 SpringServletContainerInitializer 中的 onStartup() 方法。而在 onStartup() 方法中调用了 WebApplicationInitializer 接口,所以我们只要写一个类,实现 WebApplicationInitializer 接口,就可以在容器启动时注册 DispatcherServlet,无需 web.xml

而 SpringMVC 帮我们提供了一个继承自 WebApplicationInitializer 的抽象类 AbstractAnnotationConfigDispatcherServletInitializer ,它帮我们把 SpringMVC 的初始化逻辑都准备好了,我们只要继承它,然后填写几个配置类就可以了。

下面就不使用 XML,从 0 开始,搭建一个使用全注解方式开发的项目。


18.1 新建项目

这里我们直接使用 IDEA 新建一个 J2EE 的项目,会自动帮我们生成 web.xml。

待会我们将 web.xml 删掉就好了。

可以只用 Maven 模板来创建,可以选择 maven-archetype-webapp 的模板,或者直接使用 Jakarta EE 项目。

我这里直接使用 Jakarta EE ,我的 IDEA 版本比较旧,新版本不是这样,但是操作都差不多。

点击 Next,第二步会为项目选择一些依赖,这里我只选择了 Servlet,待会我在 pom.xml 中自己引入依赖:


现在项目建好了,下面引入一下项目的依赖,我删除了创建项目后自动引入的依赖,直接将之前项目引入的所有依赖都添加了进来:

xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.foooor</groupId>
    <artifactId>hello-web</artifactId>
    <version>1.0-SNAPSHOT</version>
    <name>hello-web</name>
    <!-- 打包为 war 包 -->
    <packaging>war</packaging>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <maven.compiler.source>17</maven.compiler.source>
        <maven.compiler.target>17</maven.compiler.target>
    </properties>

    <dependencies>

        <!-- Servlet API -->
        <dependency>
            <groupId>jakarta.servlet</groupId>
            <artifactId>jakarta.servlet-api</artifactId>
            <version>6.1.0</version>
            <!-- Tomcat容器会提供,这里主要是写代码时需要引入,否则会报错 -->
            <scope>provided</scope>
        </dependency>

        <!-- Spring MVC 核心依赖,会包含 Spring 核心依赖 -->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-webmvc</artifactId>
            <version>6.2.11</version>
        </dependency>

        <!-- 引入thymeleaf -->
        <dependency>
            <groupId>org.thymeleaf</groupId>
            <artifactId>thymeleaf-spring6</artifactId>
            <version>3.1.3.RELEASE</version>
        </dependency>

        <!-- Jackson 用于 JSON 序列化 -->
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-databind</artifactId>
            <version>2.20.0</version>
        </dependency>

        <!-- 日志实现,Logback -->
        <dependency>
            <groupId>ch.qos.logback</groupId>
            <artifactId>logback-classic</artifactId>
            <version>1.5.19</version>
        </dependency>

        <!-- Lombok 可以生成 getter/setter 等方法 -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.42</version>
        </dependency>

        <!-- JSR-380 核心依赖,提供 Bean Validation 的标准接口和注解 -->
        <dependency>
            <groupId>jakarta.validation</groupId>
            <artifactId>jakarta.validation-api</artifactId>
            <version>4.0.0-M1</version>
        </dependency>

        <!-- Hibernate Validator 实现(推荐) -->
        <dependency>
            <groupId>org.hibernate.validator</groupId>
            <artifactId>hibernate-validator</artifactId>
            <version>9.1.0.Final</version>
        </dependency>

        <!-- 提供表达式语言支持,用于错误消息插值 -->
        <dependency>
            <groupId>org.glassfish</groupId>
            <artifactId>jakarta.el</artifactId>
            <version>5.0.0-M1</version>
        </dependency>

        <!-- 让 Jackson 支持 Java 8 日期时间类型(LocalDate、LocalDateTime 等) -->
        <dependency>
            <groupId>com.fasterxml.jackson.datatype</groupId>
            <artifactId>jackson-datatype-jsr310</artifactId>
            <version>2.20.1</version>
        </dependency>

    </dependencies>

    <build>
        <plugins>
            <!-- 打包配置 -->
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-war-plugin</artifactId>
                <version>3.4.0</version>
                <configuration>
                    <warName>hello-web</warName> <!-- 可选:自定义 WAR 文件名 -->
                </configuration>
            </plugin>

            <!-- 编译配置 -->
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.11.0</version>
                <configuration>
                    <source>17</source>
                    <target>17</target>
                    <compilerArgs>
                        <arg>-parameters</arg>  <!-- 开启参数名保留,用于反射获取参数名 -->
                    </compilerArgs>
                </configuration>
            </plugin>
        </plugins>
    </build>

</project>

另外删除项目自动生成的的 web.xmlindex.jspHelloServlet

18.2 统一异常处理准备

我们先讲项目中的各种功能的组件都先创建好,然后再配置。

这些类之前都创建和使用过,这里我们重新搭建一下完整的项目。

1 创建自定义错误码类

针对不同的错误信息,在项目中定义统一的错误码。

java
package com.foooor.helloweb.common;

public enum ErrorCode {
    SUCCESS(0, "成功"),
    SYSTEM_ERROR(1000, "系统内部错误"),
    PARAM_ERROR(1001, "参数错误"),
    NO_PERMISSION(1002, "无权限访问"),
    USER_NOT_FOUND(1003, "用户不存在");

    public final int code;
    public final String msg;

    ErrorCode(int code, String msg) {
        this.code = code;
        this.msg = msg;
    }
}

2 统一返回结果类

创建 Result 类,统一 Restful API 返回的数据结构:

java
package com.foooor.helloweb.common;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data  // Lombok 自动生成 getter/setter 等方法
@NoArgsConstructor
@AllArgsConstructor  // Lombok 自动生成全参构造方法
public class Result {

    private int code;
    private String message;
    private Object data;

    public static Result success() {
        return new Result(200, "success", null);
    }

    public static Result success(Object data) {
        return new Result(200, "success", data);
    }

    public static Result error(ErrorCode errorCode) {
        return new Result(errorCode.code, errorCode.msg, null);
    }

    public static Result error(int code, String message) {
        return new Result(code, message, null);
    }

    public static Result error(int code, String message, Object data) {
        return new Result(code, message, data);
    }
  
    public static Result error(ErrorCode errorCode, Object data) {
        return new Result(errorCode.code, errorCode.msg, data);
    }
}

3 自定义业务异常类

创建业务异常类,在代码中,可以根据情况随时抛出异常,然后走统一异常管理流程,返回错误信息。

java
package com.foooor.helloweb.common;

public class BizException extends RuntimeException {

    private ErrorCode errorCode;

    public BizException(ErrorCode errorCode) {
        super(errorCode.msg);
        this.errorCode = errorCode;
    }

    public ErrorCode getErrorCode() {
        return errorCode;
    }

    /**
     * 工厂方法,根据 ErrorCode 创建 BizException
     */
    public static BizException of(ErrorCode errorCode) {
        return new BizException(errorCode);
    }
}

4 创建统一异常处理类

定义统一异常处理类,并交由 Spring 容器管理。

java
package com.foooor.helloweb.common;

import jakarta.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.TypeMismatchException;
import org.springframework.http.converter.HttpMessageConversionException;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.validation.BindException;
import org.springframework.web.bind.MissingRequestValueException;
import org.springframework.web.bind.ServletRequestBindingException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.method.annotation.HandlerMethodValidationException;
import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException;
import org.springframework.web.servlet.ModelAndView;

@Slf4j
@ControllerAdvice
public class GlobalExceptionHandler {

    /**
     * 全局异常处理兜底
     */
    @ExceptionHandler(Exception.class)
    @ResponseBody
    public Object handle(Exception ex,
                         HttpServletRequest request,
                         HandlerMethod handlerMethod) {
        log.error("全局异常:{}", ex);
        return handleException(ErrorCode.SYSTEM_ERROR, request, handlerMethod);
    }

    /**
     * 业务异常处理
     */
    @ExceptionHandler(BizException.class)
    @ResponseBody
    public Object handleBizException(BizException e,
                                     HttpServletRequest request,
                                     HandlerMethod handlerMethod) {
        log.info("业务异常:{}", e);
        return handleException(e.getErrorCode(), request, handlerMethod);
    }

    /**
     * 类型转换异常处理
     */
    @ExceptionHandler({
            HttpMessageNotReadableException.class,
            MissingRequestValueException.class,
            MethodArgumentTypeMismatchException.class,
            HttpMessageConversionException.class,
            HandlerMethodValidationException.class,
            ServletRequestBindingException.class,
            BindException.class,
            TypeMismatchException.class
    })
    @ResponseBody
    public Object handleRequestParamException(Exception ex,
                                              HttpServletRequest request,
                                              HandlerMethod handlerMethod) {
        log.info("参数异常:{}", ex);
        return handleException(ErrorCode.PARAM_ERROR, request, handlerMethod);
    }

    /**
     * 统一异常处理
     */
    private Object handleException(ErrorCode errorCode,
                                   HttpServletRequest request,
                                   HandlerMethod handlerMethod) {
        log.error("SpringMVC 异常:{}", errorCode);

        if (shouldReturnJson(request, handlerMethod)) {
            return Result.error(errorCode);
        }

        // 2.否则返回页面
        ModelAndView mv = new ModelAndView();
        mv.addObject("error", errorCode.msg);
        mv.setViewName("error");   // 视图名称
        return mv;
    }

    /**
     * 判断是否返回 JSON 格式异常信息
     */
    private boolean shouldReturnJson(HttpServletRequest request,
                                     HandlerMethod handlerMethod) {

        // 1. Controller / Method 标注
        if (handlerMethod != null &&
                (handlerMethod.hasMethodAnnotation(ResponseBody.class) ||
                        handlerMethod.getBeanType().isAnnotationPresent(RestController.class))) {
            return true;
        }

        // 2. AJAX 头
        String xhr = request.getHeader("X-Requested-With");
        if ("XMLHttpRequest".equalsIgnoreCase(xhr)) {
            return true;
        }

        // 3. Accept 头
        String accept = request.getHeader("Accept");
        return accept != null && accept.contains("application/json");
    }
}

18.3 拦截器过滤器准备

1 创建拦截器

创建一个拦截器,后面演示一下如何配置拦截器。

java
package com.foooor.helloweb.intercepter;

import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;

@Slf4j
@Component  // 注册为 Spring 组件
public class LogInterceptor implements HandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest request,
                             HttpServletResponse response,
                             Object handler) {
        // 记录开始时间,存入 request
        request.setAttribute("startTime", System.currentTimeMillis());
        return true; // 放行
    }

    @Override
    public void afterCompletion(HttpServletRequest request,
                                HttpServletResponse response,
                                Object handler,
                                Exception ex) {
        // 获取时间
        Long start = (Long) request.getAttribute("startTime");

        // 计算耗时
        long duration = System.currentTimeMillis() - start;

        log.info("----执行耗时:{}, uri:{}, handler:{}, 耗时:{} ms", request.getMethod(), request.getRequestURI(), handler, duration);
    }
}

2 创建过滤器

创建一个过滤器,后面演示一下如何配置过滤器。

java
package com.foooor.helloweb.filter;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.foooor.helloweb.common.ErrorCode;
import com.foooor.helloweb.common.Result;
import jakarta.servlet.Filter;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

import java.io.IOException;

public class GlobalExceptionFilter implements Filter {

    @Override
    public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
            throws IOException {

        HttpServletRequest request = (HttpServletRequest) req;
        HttpServletResponse response = (HttpServletResponse) res;

        try {
            chain.doFilter(req, res);
        } catch (Exception e) {
            if (isAjax(request)) {
                writeJson(response, Result.error(ErrorCode.SYSTEM_ERROR));
            } else {
                // 其他异常,跳转到自定义错误页面
                response.sendRedirect("/error");
            }
        }
    }

    /**
     * 写 JSON 响应
     */
    private void writeJson(HttpServletResponse response, Result result) throws IOException {
        // 1. 一定要先判断 response 是否已经提交
        if (response.isCommitted()) {
            return;
        }
        // 2. 重置响应(避免之前的 HTML / 错误页面内容)
        response.reset();
        // 3. 设置状态码
        response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
        // 4. 设置返回类型和编码
        response.setContentType("application/json;charset=UTF-8");
        // 5. 写 JSON
        ObjectMapper mapper = new ObjectMapper();
        String json = mapper.writeValueAsString(result);
        response.getWriter().write(json);
        response.getWriter().flush();
    }


    /**
     * 判断是否是 AJAX 请求
     */
    private boolean isAjax(HttpServletRequest request) {

        // 1. X-Requested-With(传统 AJAX)
        String xRequestedWith = request.getHeader("X-Requested-With");
        if ("XMLHttpRequest".equalsIgnoreCase(xRequestedWith)) {
            return true;
        }

        // 2. Accept 包含 application/json(fetch / axios)
        String accept = request.getHeader("Accept");
        if (accept != null && accept.contains("application/json")) {
            return true;
        }

        // 3. Content-Type 是 JSON(POST / PUT)
        String contentType = request.getContentType();
        if (contentType != null && contentType.contains("application/json")) {
            return true;
        }

        return false;
    }
}

18.4 创建类型转换器

创建一个类型转换器,后面演示一下如何配置:

java
package com.foooor.helloweb.converter;

import com.foooor.helloweb.pojo.User;
import org.springframework.core.convert.converter.Converter;
import org.springframework.stereotype.Component;

@Component
public class StringToUserConverter implements Converter<String, User> {

    /**
     * 转换字符串到 User 对象
     * 假设字符串格式为 "id,name,email"
     */
    @Override
    public User convert(String source) {
        try {
            // 假设字符串格式为 "id,name,email"
            String[] parts = source.split(",");
            if (parts.length == 3) {
                Long id = Long.parseLong(parts[0]);
                String phoneNumber = parts[1];
                String nickname = parts[2];
                return new User(id, phoneNumber, nickname);
            }
        } catch (Exception e) {
             // 处理转换异常
             e.printStackTrace();
        }
        return null;
    }
}
  • 用于将 user=123,ZhangSan,123@foooor.com 参数解析为用户对象。

涉及到 User 类:

java
package com.foooor.hellospringmvc.pojo;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@AllArgsConstructor
@NoArgsConstructor
public class User {
    private Long id;
    private String name;
    private String email;
}

18.5 创建业务类

1 创建Controller

创建 Controller,跳转到 index.html

java
package com.foooor.helloweb.controller;

import com.foooor.helloweb.common.Result;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ResponseBody;

@Controller
public class IndexController {

    @GetMapping("/")
    public String index() {
        return "index";
    }

    @GetMapping("/hello")
    @ResponseBody
    public Result hello() {
        return Result.success("你好");
    }

}

2 页面准备

webapp/WEB-INFO/template/ 目录下 index.html

html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>首页</title>

</head>

<body>
    Hello For技术栈!
</body>
</html>

好了,该准备的都准备了,下面开始配置。

18.6 创建SpringMVC配置类

创建配置类主要是的代替之前使用的 springmvc-servlet.xml 配置文件。

一般会在项目下创建一个 config 子包,在包下创建 SpringMvcConfig.java (自定义名称)类,如下:

之前在 XML 中配置的都需要在这里进行配置:

java
package com.foooor.helloweb.config;

import com.foooor.helloweb.converter.StringToUserConverter;
import com.foooor.helloweb.intercepter.LogInterceptor;
import org.hibernate.validator.HibernateValidator;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.format.support.FormattingConversionService;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.StringHttpMessageConverter;
import org.springframework.validation.Validator;
import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean;
import org.springframework.web.multipart.support.StandardServletMultipartResolver;
import org.springframework.web.servlet.ViewResolver;
import org.springframework.web.servlet.config.annotation.*;
import org.thymeleaf.spring6.SpringTemplateEngine;
import org.thymeleaf.spring6.templateresolver.SpringResourceTemplateResolver;
import org.thymeleaf.spring6.view.ThymeleafViewResolver;
import org.thymeleaf.templatemode.TemplateMode;
import org.thymeleaf.templateresolver.ITemplateResolver;
import java.nio.charset.StandardCharsets;
import java.util.List;

/**
 * SpringMVC的配置类,用来代替springmvc-servlet.xml
 * 根据需要做如下配置:
 * 1.配置组件扫描
 * 2.开启注解驱动
 * 3.配置拦截器
 * 4.配置视图解析器,包括视图解析器、模板引擎、模板解析器(传统项目)
 * 5.配置静态资源处理(传统项目)
 * 6.配置视图控制器,对于不需要controller处理数据的请求,直接返回视图
 * 7.配置消息转换器,包括字符串消息转换器、JSON消息转换器
 * 8.文件上传配置
 * 9.配置自定义的类型转换服务
 * 10.数据校验器
 */
@Configuration
// 1.配置组件扫描
@ComponentScan(basePackages = "com.foooor.helloweb")
// 2.开启注解驱动
@EnableWebMvc
public class SpringMvcConfig implements WebMvcConfigurer {

    // 注入拦截器
    @Autowired
    private LogInterceptor logInterceptor;

    /**
     * 3.配置拦截器
     */
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        // 注册拦截器,拦截所有请求,除了 /login 和 /register
        registry.addInterceptor(logInterceptor)
                .addPathPatterns("/**")  // 拦截所有请求,可以添加多个拦截.addPathPatterns("/order/**", "/user/**")
                .excludePathPatterns("/login", "/register");  // 添加例外

        // TODO 可以通过 registry.addInterceptor 配置多个拦截器
    }

    /**
     * 4.以下三个方法是用来配置Thymeleaf模板解析器、模板引擎、视图解析器,用于渲染Thymeleaf模板
     * 配置Thymeleaf模板解析器
     */
    @Bean
    public SpringResourceTemplateResolver templateResolver(ApplicationContext applicationContext) {
        SpringResourceTemplateResolver resolver = new SpringResourceTemplateResolver();
        resolver.setApplicationContext(applicationContext);
        resolver.setPrefix("/WEB-INF/template/");  // 模板文件的前缀,也就是模板文件所在的目录
        resolver.setSuffix(".html");  // 模板文件的后缀,也就是模板文件的扩展名
        resolver.setCharacterEncoding("UTF-8");  // 模板文件的字符编码,模板文件本身用什么编码写的
        resolver.setTemplateMode(TemplateMode.HTML);  // 模板文件的模式,这里使用HTML5模式
        resolver.setCacheable(false);  // 模板文件是否缓存,这里设置为false,因为开发阶段需要频繁修改模板文件,改动即可生效,生产环境打开
        return resolver;
    }

    /**
     * 配置Thymeleaf模板引擎
     */
    @Bean
    public SpringTemplateEngine templateEngine(ITemplateResolver templateResolver) {
        SpringTemplateEngine engine = new SpringTemplateEngine();
        engine.setTemplateResolver(templateResolver);
        return engine;
    }

    /**
     * 配置Thymeleaf视图解析器
     */
    @Bean
    public ViewResolver viewResolver(SpringTemplateEngine springTemplateEngine) {
        ThymeleafViewResolver resolver = new ThymeleafViewResolver();
        resolver.setTemplateEngine(springTemplateEngine);
        resolver.setCharacterEncoding("UTF-8");  // 返回给浏览器的响应内容用什么编码
        return resolver;
    }

    /**
     * 5.配置静态资源处理(传统项目)
     * 代替xml中的配置:<mvc:resources mapping="/static/**" location="/static/" />
     */
    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        registry.addResourceHandler("/static/**")
                .addResourceLocations("/static/");
    }

    /**
     * 6.配置视图控制器,对于不需要controller处理数据的请求,直接返回视图
     * 代替xml中的配置:<mvc:view-controller path="/login" view-name="login"/>
     */
    @Override
    public void addViewControllers(ViewControllerRegistry registry) {
        // 访问 /login 时,直接返回 index.html 视图
        registry.addViewController("/login").setViewName("login");
    }

    /**
     * 7.配置消息转换器,因为StringHttpMessageConverter的默认编码是ISO-8859-1,这里将其设置为UTF-8
     * 避免单纯返回字符串的时候出现乱码
     */
    @Override
    public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
        for (HttpMessageConverter<?> converter : converters) {
            if (converter instanceof StringHttpMessageConverter) {
                ((StringHttpMessageConverter) converter)
                        .setDefaultCharset(StandardCharsets.UTF_8);
            }
        }
    }

    /**
     * 8.文件上传配置
     */
    @Bean
    public StandardServletMultipartResolver multipartResolver() {
        return new StandardServletMultipartResolver();
    }

    /**
     * 9.配置自定义的类型转换服务
     */
    @Bean
    public FormattingConversionService conversionService() {
        FormattingConversionService service = new FormattingConversionService();
        service.addConverter(new StringToUserConverter());
        return service;
    }

    // 10.数据校验器
    @Override
    public Validator getValidator() {
        LocalValidatorFactoryBean validator = new LocalValidatorFactoryBean();
        validator.setProviderClass(HibernateValidator.class);
        return validator;
    }

}
  • 使用的时候,根据需要配置就可以了。

18.7 创建Web初始化类

接下来,我们需要创建一个 Web 初始化类,用于代替 web.xml

主要用于注册 DispatcherServlet 和配置 Spring 容器。这个类需要继承AbstractAnnotationConfigDispatcherServletInitializer抽象类。

java
package com.foooor.helloweb.config;

import com.foooor.helloweb.filter.GlobalExceptionFilter;
import jakarta.servlet.Filter;
import jakarta.servlet.MultipartConfigElement;
import jakarta.servlet.ServletRegistration;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.filter.CharacterEncodingFilter;
import org.springframework.web.filter.HiddenHttpMethodFilter;
import org.springframework.web.servlet.support.AbstractAnnotationConfigDispatcherServletInitializer;

import java.io.File;

/**
 * 代替web.xml,所以在这个类中编写的就是web.xml的配置
 */
@Configuration  // 标识是配置文件
public class WebAppInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {

    /**
     * 根配置类,一般是spring的配置类
     */
    @Override
    protected Class<?>[] getRootConfigClasses() {
        // 这里暂时没有用到spring的配置类
        return new Class<?>[0];
    }

    /**
     * servlet配置类,一般是springmvc的配置类
     */
    @Override
    protected Class<?>[] getServletConfigClasses() {
        return new Class<?>[]{SpringMvcConfig.class};
    }

    /**
     * 配置DispatcherServlet的映射路径
     * @return
     */
    @Override
    protected String[] getServletMappings() {
        return new String[]{"/"};
    }

    /**
     * 配置web.xml中的过滤器
     */
    @Override
    protected Filter[] getServletFilters() {
        // 配置字符编码过滤器
        CharacterEncodingFilter characterEncodingFilter = new CharacterEncodingFilter();
        characterEncodingFilter.setEncoding("UTF-8");
        characterEncodingFilter.setForceEncoding(true);

        // 用于处理post请求中的_method参数,支持Restful风格的请求
        HiddenHttpMethodFilter hiddenHttpMethodFilter = new HiddenHttpMethodFilter();

        // 全局异常处理过滤器
        GlobalExceptionFilter globalExceptionFilter = new GlobalExceptionFilter();
        return new Filter[]{characterEncodingFilter, hiddenHttpMethodFilter, globalExceptionFilter};
    }

    /**
     * 配置文件上传的参数
     */
    @Override
    protected void customizeRegistration(ServletRegistration.Dynamic registration) {
        String tempDir = System.getProperty("java.io.tmpdir") + "/upload";
        File dir = new File(tempDir);
        if (!dir.exists()) {
            dir.mkdirs();
        }

        MultipartConfigElement multipartConfig = new MultipartConfigElement(
                tempDir,  // 临时目录(文件太大时会写入这里)
                50 * 1024 * 1024,     // 单个文件最大 50MB
                20 * 1024 * 1024,     // 设置整个表单上传的所有文件总大小的最大值 100MB
                2 * 1024 * 1024       // 超过 2MB 才写入磁盘
        );

        registration.setMultipartConfig(multipartConfig);
        registration.setLoadOnStartup(1);
    }
}
  • AbstractAnnotationConfigDispatcherServletInitializer 有很多方法可以重写,用来做不同的配置。

  • getRootConfigClasses() 返回根容器的配置类,对应Spring的配置,这里我们暂时没有使用 Spring 配置类,所以先空着不配置;

  • getServletConfigClasses() 返回SpringMVC容器的配置类,对应之前的 SpringMVC 配置类springmvc-servlet.xml

  • getServletMappings() 配置DispatcherServlet的映射路径,这里配置为/,表示处理所有请求。


18.8 配置日志文件

添加一下日志配置,后面在项目中可以更方便的打印日志,这里使用 logback 日志框架实现。

在项目的 resources 目录下,创建 logback.xml,配置如下:

xml
<?xml version="1.0" encoding="UTF-8"?>
<configuration>

    <!-- 定义日志文件存放路径,这里指定放在 Tomcat 根目录下的 logs 文件夹 -->
    <property name="LOG_PATH" value="${catalina.base}/logs" />
    <!-- 定义日志文件路径 -->
    <property name="LOG_FILE" value="${LOG_PATH}/hello-web.log" />

    <!-- 定义控制台输出 -->
    <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <pattern>%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n</pattern>
            <charset>UTF-8</charset>
        </encoder>
    </appender>

    <!-- 定义文件输出 -->
    <appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <file>${LOG_FILE}</file>
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <!-- 按天生成新日志文件 -->
            <fileNamePattern>${LOG_PATH}/app.%d{yyyy-MM-dd}.log</fileNamePattern>
            <!-- 保留 30 天的日志 -->
            <maxHistory>30</maxHistory>
        </rollingPolicy>
        <encoder>
            <pattern>%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n</pattern>
            <charset>UTF-8</charset>
        </encoder>
    </appender>

    <!-- 还可以单独指定某些包下的日志级别 -->
    <logger name="com.foooor.helloweb" level="DEBUG" additivity="false">
        <appender-ref ref="CONSOLE" />
        <appender-ref ref="FILE" />
    </logger>

    <!-- root 日志级别 -->
    <root level="INFO">
        <appender-ref ref="CONSOLE" />
        <appender-ref ref="FILE" />
    </root>

</configuration>

18.9 测试

启动项目,测试即可。

内容未完......