SpringMVC 开发避坑指南:十大常见问题深度解析与解决方案

引言

在Java Web开发领域,SpringMVC作为一款主流的Web框架,凭借其强大的功能和便捷的开发体验深受开发者喜爱。然而,在实际使用过程中,开发者常常会遇到各种各样的"坑"。本文将针对SpringMVC开发中常见的十大问题,结合实际案例和代码,深入剖析问题产生的原因,并提供详细的解决方案,帮助大家在开发过程中少走弯路。

一、自定义异常总看不懂?是设计逻辑出了问题吗?

在SpringMVC项目中,当业务逻辑变得复杂时,使用自定义异常可以更清晰地处理不同类型的错误情况。但有时开发者会发现自定义异常难以理解,这往往是因为异常设计逻辑不够清晰。

问题场景

假设我们正在开发一个电商系统,在用户下单时需要检查库存是否充足。当库存不足时,希望抛出一个自定义的InsufficientStockException异常。但在实际调试过程中,发现异常信息混乱,难以定位问题根源。

原因分析

自定义异常设计不规范,没有合理继承已有的异常体系,或者异常信息没有包含足够的上下文信息,导致在捕获和处理异常时无法准确判断异常情况。

解决方案

  1. 定义自定义异常类,合理继承RuntimeExceptionException。例如:
java 复制代码
// 继承RuntimeException,定义库存不足异常
public class InsufficientStockException extends RuntimeException {
    public InsufficientStockException(String message) {
        super(message);
    }
}
  1. 在业务逻辑中使用自定义异常。以库存检查为例:
java 复制代码
@Service
public class OrderService {
    private int stock = 10; // 模拟库存数量

    public void placeOrder(int quantity) {
        if (quantity > stock) {
            // 库存不足时抛出自定义异常
            throw new InsufficientStockException("库存不足,无法下单");
        }
        // 正常下单逻辑
    }
}
  1. 使用全局异常处理器统一处理异常,让异常信息更清晰易读。
java 复制代码
@RestControllerAdvice
public class GlobalExceptionHandler {
    @ExceptionHandler(InsufficientStockException.class)
    public String handleInsufficientStockException(InsufficientStockException e) {
        return "错误信息:" + e.getMessage();
    }
}

二、自定义异常不生效?为何还在报500错误?

开发者定义好自定义异常并配置了异常处理器后,有时会发现自定义异常并没有按照预期处理,页面仍然显示500错误。

问题场景

在上述电商系统中,配置好InsufficientStockException及其处理器后,下单时库存不足依然显示500错误页面。

原因分析

  1. 异常处理器配置错误,没有被Spring容器正确扫描到。
  2. 异常没有被正确抛出,在抛出异常之前可能被其他代码捕获处理。
  3. 全局异常处理器的优先级问题,其他优先级更高的异常处理机制先拦截了异常。

解决方案

  1. 确保异常处理器所在的类被@RestControllerAdvice@ControllerAdvice注解标注,并且所在的包被Spring容器扫描。例如,在Spring Boot项目的启动类上添加@ComponentScan注解,扫描包含异常处理器的包:
java 复制代码
@SpringBootApplication
@ComponentScan(basePackages = {"com.example.demo"})
public class DemoApplication {
    public static void main(String[] args) {
        SpringApplication.run(DemoApplication.class, args);
    }
}
  1. 检查业务代码中异常抛出的逻辑,确保异常能够顺利抛出到全局异常处理器。
  2. 如果存在多个异常处理器,调整其优先级。可以通过实现Ordered接口,重写getOrder方法来设置优先级,数值越小优先级越高:
java 复制代码
@RestControllerAdvice
public class GlobalExceptionHandler implements Ordered {
    @ExceptionHandler(InsufficientStockException.class)
    public String handleInsufficientStockException(InsufficientStockException e) {
        return "错误信息:" + e.getMessage();
    }

    @Override
    public int getOrder() {
        return 1; // 设置优先级
    }
}

三、时间格式转换失败?POST请求的"陷阱"注意到了吗?

在处理包含日期时间类型参数的POST请求时,经常会遇到时间格式转换失败的问题。

问题场景

前端通过POST请求发送一个包含日期时间字段的数据到后端,后端使用@RequestBody接收数据并绑定到实体类中,但在转换过程中出现Failed to convert property value of type 'java.lang.String' to required type 'java.util.Date'错误。

原因分析

  1. 前端发送的日期时间格式与后端期望的格式不一致。
  2. SpringMVC默认的日期时间格式转换配置不符合需求。
  3. 在POST请求中,@RequestBody解析数据时,对于日期时间类型的转换规则与GET请求不同,需要额外配置。

解决方案

  1. 在实体类的日期时间字段上使用@DateTimeFormat注解指定日期时间格式。例如:
java 复制代码
public class Order {
    private Long id;
    // 指定日期时间格式为"yyyy-MM-dd HH:mm:ss"
    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private Date orderTime;

    // 省略getter和setter方法
}
  1. 配置SpringMVC的日期时间格式化。在Spring Boot项目中,可以在application.properties文件中添加以下配置:
properties 复制代码
spring.jackson.date-format=yyyy-MM-dd HH:mm:ss
spring.jackson.time-zone=GMT+8
  1. 如果上述方法无效,可以自定义一个Converter来处理日期时间格式转换。例如:
java 复制代码
@Component
public class CustomDateConverter implements Converter<String, Date> {
    private static final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

    @Override
    public Date convert(String source) {
        try {
            return sdf.parse(source);
        } catch (ParseException e) {
            throw new IllegalArgumentException("日期格式转换失败", e);
        }
    }
}

然后在配置类中注册这个转换器:

java 复制代码
@Configuration
public class WebConfig implements WebMvcConfigurer {
    @Autowired
    private CustomDateConverter customDateConverter;

    @Override
    public void addFormatters(FormatterRegistry registry) {
        registry.addConverter(customDateConverter);
    }
}

四、调试断点失效?是不是被多个Filter"拦截"了?

在调试SpringMVC项目时,有时会发现设置的断点无法进入,导致调试工作无法正常进行。

问题场景

在控制器方法中设置了断点,启动调试模式后,请求到达该方法时断点没有生效,直接跳过执行后续代码。

原因分析

  1. 项目中存在多个Filter,请求在到达控制器之前被其他Filter拦截处理,导致无法进入断点所在的控制器方法。
  2. Filter的配置顺序不合理,某些Filter在处理请求时消耗了请求资源,使得后续请求无法正常处理。
  3. 断点设置的位置存在问题,例如在静态方法或没有被Spring容器管理的类中设置断点。

解决方案

  1. 检查项目中的Filter配置,确保没有不必要的Filter拦截请求。可以通过在Filter的doFilter方法中添加日志输出,查看请求是否经过该Filter:
java 复制代码
@Component
public class CustomFilter implements Filter {
    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
    }

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        System.out.println("请求进入CustomFilter");
        filterChain.doFilter(servletRequest, servletResponse);
        System.out.println("请求离开CustomFilter");
    }

    @Override
    public void destroy() {
    }
}
  1. 调整Filter的顺序,确保关键的Filter在合适的位置执行。可以通过实现Ordered接口,重写getOrder方法来设置Filter的执行顺序:
java 复制代码
@Component
public class CustomFilter implements Filter, Ordered {
    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
    }

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        filterChain.doFilter(servletRequest, servletResponse);
    }

    @Override
    public void destroy() {
    }

    @Override
    public int getOrder() {
        return 1; // 设置Filter执行顺序
    }
}
  1. 确保断点设置在被Spring容器管理的类和方法中,并且方法不是静态方法。

五、Request输入流读取后消失?响应体处理遗漏了?

在处理请求和响应时,可能会遇到Request输入流读取一次后无法再次读取,或者响应体处理不当导致数据丢失的问题。

问题场景

在一个需要多次读取Request输入流的场景中,第一次读取后,后续读取操作获取到的输入流为空。在处理响应时,发现响应数据没有按照预期输出。

原因分析

  1. HttpServletRequest的输入流默认只能读取一次,读取后流会被关闭或重置,导致后续无法再次读取。
  2. 在响应体处理过程中,没有正确设置响应头信息,或者没有将数据正确写入响应体。
  3. 存在其他代码在处理请求或响应过程中,意外关闭了输入流或响应流。

解决方案

  1. 自定义一个可以重复读取的HttpServletRequest包装类。例如:
java 复制代码
public class CachedBodyHttpServletRequest extends HttpServletRequestWrapper {
    private final byte[] body;

    public CachedBodyHttpServletRequest(HttpServletRequest request) throws IOException {
        super(request);
        body = IOUtils.toByteArray(request.getInputStream());
    }

    @Override
    public ServletInputStream getInputStream() throws IOException {
        final ByteArrayInputStream bais = new ByteArrayInputStream(body);
        return new ServletInputStream() {
            @Override
            public boolean isFinished() {
                return bais.available() == 0;
            }

            @Override
            public boolean isReady() {
                return true;
            }

            @Override
            public void setReadListener(ReadListener readListener) {
            }

            @Override
            public int read() throws IOException {
                return bais.read();
            }
        };
    }

    @Override
    public BufferedReader getReader() throws IOException {
        return new BufferedReader(new InputStreamReader(getInputStream()));
    }
}

然后在Filter中使用这个包装类来替换原始的HttpServletRequest

java 复制代码
@Component
public class RequestBodyCacheFilter implements Filter {
    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
    }

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        HttpServletRequest httpServletRequest = (HttpServletRequest) servletRequest;
        CachedBodyHttpServletRequest cachedBodyHttpServletRequest = new CachedBodyHttpServletRequest(httpServletRequest);
        filterChain.doFilter(cachedBodyHttpServletRequest, servletResponse);
    }

    @Override
    public void destroy() {
    }
}
  1. 正确处理响应体,设置响应头信息并将数据写入响应体。例如:
java 复制代码
@RestController
public class HelloController {
    @GetMapping("/hello")
    public void hello(HttpServletResponse response) throws IOException {
        response.setContentType("application/json;charset=UTF-8");
        PrintWriter writer = response.getWriter();
        writer.write("{\"message\":\"Hello, World!\"}");
        writer.flush();
        writer.close();
    }
}
  1. 检查项目中所有涉及请求和响应处理的代码,确保没有意外关闭输入流或响应流的操作。

六、参数绑定总出错?是类型转换规则没吃透吗?

在SpringMVC中进行参数绑定时,经常会出现参数类型转换错误的问题,导致请求无法正确处理。

问题场景

前端传递一个字符串类型的参数,后端控制器方法期望接收一个整数类型的参数,但在绑定过程中出现Failed to convert value of type 'java.lang.String' to required type 'java.lang.Integer'错误。

原因分析

  1. 前端传递的参数类型与后端控制器方法参数类型不匹配,并且SpringMVC无法自动进行正确的类型转换。
  2. 自定义的类型转换规则没有生效,或者类型转换规则定义错误。
  3. 参数名称不一致,导致SpringMVC无法正确匹配参数。

解决方案

  1. 确保前端传递的参数类型与后端控制器方法参数类型兼容,并且SpringMVC支持自动类型转换。如果不支持自动转换,可以使用@RequestParam注解的required属性设置为false,避免参数不存在时抛出异常:
java 复制代码
@GetMapping("/user")
public String getUser(@RequestParam(value = "age", required = false) Integer age) {
    if (age == null) {
        return "年龄参数未传递";
    }
    return "用户年龄为:" + age;
}
  1. 对于复杂的类型转换,可以自定义类型转换器。例如,将字符串转换为自定义的User对象:
java 复制代码
public class User {
    private String name;
    private int age;

    // 省略getter和setter方法
}

@Component
public class UserConverter implements Converter<String, User> {
    @Override
    public User convert(String source) {
        String[] parts = source.split(",");
        User user = new User();
        user.setName(parts[0]);
        user.setAge(Integer.parseInt(parts[1]));
        return user;
    }
}

然后在配置类中注册这个转换器:

java 复制代码
@Configuration
public class WebConfig implements WebMvcConfigurer {
    @Autowired
    private UserConverter userConverter;

    @Override
    public void addFormatters(FormatterRegistry registry) {
        registry.addConverter(userConverter);
    }
}
  1. 检查参数名称是否一致,确保前端传递的参数名与后端控制器方法中@RequestParam@RequestBody注解指定的参数名相同。

七、表单提交乱码?编码配置环节是否疏忽了?

在处理表单提交时,有时会出现提交的数据在后端显示为乱码的情况。

问题场景

用户在前端表单中输入中文内容并提交,后端接收到的中文内容显示为乱码。

原因分析

  1. 前端表单的accept-charset属性没有正确设置,或者设置的编码格式与后端不一致。
  2. SpringMVC的编码过滤器配置错误,没有对请求进行正确的编码处理。
  3. 服务器的默认编码设置与项目要求的编码不一致。

解决方案

  1. 在前端表单中设置accept-charset属性为UTF-8
html 复制代码
<form action="/submit" method="post" accept-charset="UTF-8">
    <input type="text" name="username" />
    <input type="submit" value="提交" />
</form>
  1. 在Spring Boot项目中,配置CharacterEncodingFilter来处理请求编码。在application.properties文件中添加以下配置:
properties 复制代码
spring.http.encoding.charset=UTF-8
spring.http.encoding.enabled=true
spring.http.encoding.force=true
  1. 如果上述配置无效,可以自定义一个Filter来处理编码问题:
java 复制代码
@Component
public class EncodingFilter implements Filter {
    private static final String ENCODING = "UTF-8";

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
    }

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        servletRequest.setCharacterEncoding(ENCODING);
        servletResponse.setCharacterEncoding(ENCODING);
        filterChain.doFilter(servletRequest, servletResponse);
    }

    @Override
    public void destroy() {
    }
}
  1. 检查服务器的默认编码设置,确保与项目要求的编码一致。例如,在Tomcat服务器中,可以在conf/server.xml文件中设置URIEncoding="UTF-8"
xml 复制代码
<Connector port="8080" protocol="HTTP/1.1"
           connectionTimeout="20000"
           redirectPort="8443"
           URIEncoding="UTF-8"/>

八、拦截器拦截范围不对?匹配规则真的设置正确了?

在使用拦截器对请求进行拦截处理时,可能会出现拦截范围不符合预期的问题。

问题场景

配置了一个拦截器用于拦截所有的用户请求进行权限验证,但某些请求却没有被拦截到;或者不应该被拦截的请求反而被拦截了。

原因分析

  1. 拦截器的addPathPatternsexcludePathPatterns方法设置的匹配规则不正确,没有准确覆盖需要拦截或排除的请求路径。
  2. 拦截器的注册顺序问题,导致部分请求在拦截器生效之前就已经被处理。
  3. 路径匹配规则中使用的通配符(如***)理解错误,导致匹配范围不准确。

解决方案

1. 定义拦截器类,实现 HandlerInterceptor 接口,在 preHandle 方法中进行拦截逻辑处理:

java 复制代码
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

public class PermissionInterceptor implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        // 简单示例:判断请求中是否包含特定参数作为权限验证
        String authToken = request.getParameter("authToken");
        if (authToken == null || !"valid_token".equals(authToken)) {
            response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "权限不足");
            return false;
        }
        return true;
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
    }
}

2. 在配置类中注册拦截器,并设置拦截和排除路径:

java 复制代码
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new PermissionInterceptor())
               .addPathPatterns("/user/**") // 拦截所有以 /user/ 开头的请求
               .excludePathPatterns("/user/login", "/user/register"); // 排除登录和注册请求
    }
}

3. 若存在多个拦截器,通过实现 Ordered 接口控制执行顺序:

java 复制代码
import org.springframework.core.Ordered;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

public class AnotherInterceptor implements HandlerInterceptor, Ordered {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        // 拦截逻辑
        return true;
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
    }

    @Override
    public int getOrder() {
        return 2; // 数值越小优先级越高,假设 PermissionInterceptor 优先级为 1
    }
}

并在配置类中注册该拦截器,这样就能按顺序执行拦截逻辑。

九、视图解析失败?模板引擎配置出问题了吗?

在使用模板引擎(如 Thymeleaf、Freemarker)时,常常会遇到视图解析失败的情况,页面无法正确渲染。

问题场景

在 SpringMVC 项目中集成了 Thymeleaf 模板引擎,控制器方法返回视图名称后,页面显示 Whitelabel Error Page,提示找不到对应的视图。

原因分析

  1. 模板引擎的依赖没有正确引入,或者版本不兼容。
  2. 模板引擎的配置不正确,如视图前缀、后缀设置错误,导致无法找到对应的模板文件。
  3. 模板文件的存放位置不符合配置要求,或者文件名拼写错误。

解决方案

以 Thymeleaf 为例:

1. 确保在 pom.xml 文件中正确引入 Thymeleaf 依赖:

xml 复制代码
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>

2. 在 application.properties 文件中配置 Thymeleaf 的视图前缀和后缀:

properties 复制代码
spring.thymeleaf.prefix=classpath:/templates/
spring.thymeleaf.suffix=.html
spring.thymeleaf.mode=HTML
spring.thymeleaf.encoding=UTF-8
spring.thymeleaf.content-type=text/html

上述配置表示 Thymeleaf 会在 classpath:/templates/ 目录下寻找模板文件,并且模板文件的后缀为 .html

3. 确保模板文件存放在正确的目录下,并且文件名与控制器返回的视图名称一致。例如,控制器方法:

java 复制代码
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;

@Controller
public class IndexController {
    @GetMapping("/")
    public String index() {
        return "index"; // 返回视图名称为 index,对应 templates 目录下的 index.html 文件
    }
}

同时,检查模板文件中是否存在语法错误,如标签闭合不正确、表达式错误等,这些也可能导致视图解析失败。

十、跨域请求被拒?CORS 配置是否完整?

在前后端分离项目中,经常会遇到跨域请求被拒绝的问题,影响前后端数据交互。

问题场景

前端发起请求到后端接口,浏览器控制台提示 Access to XMLHttpRequest at 'xxx' from origin 'xxx' has been blocked by CORS policy 错误,请求无法成功发送。

原因分析

  1. 后端没有配置 CORS(Cross-Origin Resource Sharing,跨域资源共享)相关规则,浏览器出于安全策略限制了跨域请求。
  2. CORS 配置不完整,如只允许了部分请求方法、没有设置允许携带凭证等,导致请求不符合跨域规则。

解决方案

方式一:使用 @CrossOrigin 注解

在控制器类或方法上添加 @CrossOrigin 注解,简单快速地解决跨域问题。例如:

java 复制代码
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@CrossOrigin(origins = "http://localhost:3000", allowedHeaders = "*", methods = {java.net.HttpURLConnection.HTTP_GET, java.net.HttpURLConnection.HTTP_POST})
public class ApiController {
    @GetMapping("/data")
    public String getData() {
        return "Hello, Cross-Origin Data";
    }
}

上述代码中,@CrossOrigin 注解允许来自 http://localhost:3000 的请求,允许所有请求头,支持 GET 和 POST 请求方法。

方式二:全局 CORS 配置

通过配置类实现 WebMvcConfigurer 接口,重写 addCorsMappings 方法进行全局 CORS 配置:

java 复制代码
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class CorsConfig implements WebMvcConfigurer {
    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/**")
               .allowedOrigins("http://localhost:3000")
               .allowedMethods("GET", "POST", "PUT", "DELETE")
               .allowedHeaders("*")
               .allowCredentials(true);
    }
}

这里配置了对所有请求路径(/**)的跨域支持,允许来自 http://localhost:3000 的请求,支持 GET、POST、PUT、DELETE 等请求方法,允许所有请求头,并且允许携带凭证(如 Cookie)。

通过以上对 SpringMVC 开发中十大常见问题的详细解析和解决方案介绍,希望能帮助你在实际开发中顺利避开这些"坑"。

相关推荐
Java微观世界3 分钟前
Java逻辑运算符完全指南:短路与、非短路或、异或的妙用,一篇搞定!
后端
星星电灯猴5 分钟前
数据差异的iOS性能调试:设备日志导出和iOS文件管理
后端
Ghostbaby10 分钟前
stack_traces 创建失败
后端
瀚海澜生11 分钟前
快速掌握使用redis分布式锁
后端
haokan_Jia14 分钟前
【java中使用stream处理list数据提取其中的某个字段,并由List<String>转为List<Long>】
java·windows·list
码破苍穹ovo21 分钟前
回溯----5.括号生成
java·数据结构·力扣·递归
软件20522 分钟前
【Java树形菜单系统设计与实现】
java
yz_518 Nemo23 分钟前
Django项目实战
后端·python·django
麓殇⊙23 分钟前
操作系统期末复习--操作系统初识以及进程与线程
java·大数据·数据库
胖头鱼不吃鱼23 分钟前
Apipost 与 Apifox:API 协议功能扩展对比,满足多元开发需求
后端