java

关注公众号 jb51net

关闭
首页 > 软件编程 > java > SpringMVC开发常见问题

SpringMVC开发中十大常见问题深度解析与解决方案

作者:程序员岳彬

在Java Web开发领域,SpringMVC作为一款主流的Web框架,凭借其强大的功能和便捷的开发体验深受开发者喜爱,然而,在实际使用过程中,开发者常常会遇到各种各样的“坑”,本文将针对SpringMVC开发中常见的十大问题,需要的朋友可以参考下

引言

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

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

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

问题场景

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

原因分析

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

解决方案

// 继承RuntimeException,定义库存不足异常
public class InsufficientStockException extends RuntimeException {
    public InsufficientStockException(String message) {
        super(message);
    }
}
@Service
public class OrderService {
    private int stock = 10; // 模拟库存数量

    public void placeOrder(int quantity) {
        if (quantity > stock) {
            // 库存不足时抛出自定义异常
            throw new InsufficientStockException("库存不足,无法下单");
        }
        // 正常下单逻辑
    }
}
@RestControllerAdvice
public class GlobalExceptionHandler {
    @ExceptionHandler(InsufficientStockException.class)
    public String handleInsufficientStockException(InsufficientStockException e) {
        return "错误信息:" + e.getMessage();
    }
}

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

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

问题场景

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

原因分析

解决方案

@SpringBootApplication
@ComponentScan(basePackages = {"com.example.demo"})
public class DemoApplication {
    public static void main(String[] args) {
        SpringApplication.run(DemoApplication.class, args);
    }
}
@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'错误。

原因分析

解决方案

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方法
}
spring.jackson.date-format=yyyy-MM-dd HH:mm:ss
spring.jackson.time-zone=GMT+8
@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);
        }
    }
}

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

@Configuration
public class WebConfig implements WebMvcConfigurer {
    @Autowired
    private CustomDateConverter customDateConverter;

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

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

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

问题场景

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

原因分析

解决方案

@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() {
    }
}
@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执行顺序
    }
}

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

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

问题场景

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

原因分析

解决方案

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

@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() {
    }
}
@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();
    }
}

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

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

问题场景

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

原因分析

解决方案

@GetMapping("/user")
public String getUser(@RequestParam(value = "age", required = false) Integer age) {
    if (age == null) {
        return "年龄参数未传递";
    }
    return "用户年龄为:" + age;
}
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;
    }
}

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

@Configuration
public class WebConfig implements WebMvcConfigurer {
    @Autowired
    private UserConverter userConverter;

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

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

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

问题场景

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

原因分析

解决方案

<form action="/submit" method="post" accept-charset="UTF-8">
    <input type="text" name="username" />
    <input type="submit" value="提交" />
</form>
spring.http.encoding.charset=UTF-8
spring.http.encoding.enabled=true
spring.http.encoding.force=true
@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() {
    }
}
<Connector port="8080" protocol="HTTP/1.1"
           connectionTimeout="20000"
           redirectPort="8443"
           URIEncoding="UTF-8"/>

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

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

问题场景

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

原因分析

解决方案

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

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. 在配置类中注册拦截器,并设置拦截和排除路径:

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 接口控制执行顺序:

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,提示找不到对应的视图。

原因分析

解决方案

以 Thymeleaf 为例:

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

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>

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

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

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 错误,请求无法成功发送。

原因分析

解决方案

方式一:使用 @CrossOrigin 注解

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

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 配置:

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 开发中十大常见问题的详细解析和解决方案介绍,希望能帮助你在实际开发中顺利避开这些“坑”。

以上就是SpringMVC开发中十大常见问题深度解析与解决方案的详细内容,更多关于SpringMVC开发常见问题的资料请关注脚本之家其它相关文章!

您可能感兴趣的文章:
阅读全文