springmvc项目应用层级

SpringMVC 实战指南与面试要点

前言

之前有文章来介绍过这个springmvc是什么,这里在简单概述一下吧。

springmvc要配合着tomcat去使用,tomcat作为web服务器,用来接收http请求,和相应http请求。也就是spring在tomcat拿到了http请求,解析java数据,在这中间,springmvc提供了一系列的机制,供我们来使用。所以,一切和前端发送http请求,相应http请求有关的技术,就是springmvc相关的。他大体上可以分为拦截器,解析响应http请求的机制,result类,统一格式返回数据,如果系统出现异常,弄的全局捕获异常。前端上传文件,以及跨域处理。这一切,都是和前端有关。

此文章是偏向实战和代码实现的,这里,我就集中讲,大体上,基本面要了解和认识的信息,以及面试题。(不怎么包含底层实现)

我们直接面向实战


1. 在SpringBoot里如何使用SpringMVC

问题1.1:SpringBoot中SpringMVC是如何自动配置的?

回答:

SpringBoot通过自动配置机制简化了SpringMVC的使用。当你在项目中引入spring-boot-starter-web依赖后,SpringBoot会自动完成以下配置:

  1. 内嵌Tomcat容器:无需单独部署war包,直接运行jar包即可
  2. DispatcherServlet自动注册:核心前端控制器自动配置
  3. 默认的视图解析器、消息转换器:如Jackson用于JSON转换
  4. 静态资源映射 :自动配置/static/public等目录
java 复制代码
// 只需在pom.xml中添加依赖
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

// 启动类
@SpringBootApplication
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

问题1.2:如何创建一个基本的Controller?

回答:

在SpringBoot中创建Controller非常简单,使用@RestController@Controller注解即可:

java 复制代码
@RestController
@RequestMapping("/api/user")
public class UserController {
    
    // GET请求示例
    @GetMapping("/{id}")
    public User getUser(@PathVariable Long id) {
        return userService.findById(id);
    }
    
    // POST请求示例
    @PostMapping
    public Result createUser(@RequestBody User user) {
        userService.save(user);
        return Result.success();
    }
}

关键注解说明:

  • @RestController = @Controller + @ResponseBody,自动将返回值转为JSON
  • @RequestMapping:定义请求路径
  • @GetMapping/@PostMapping/@PutMapping/@DeleteMapping:指定HTTP方法

2. SpringMVC提供了哪些机制,供我们在项目中使用

2.1 拦截器以及跨域处理

问题2.1.1:什么是拦截器?它和过滤器有什么区别?

回答:

拦截器(Interceptor)是SpringMVC提供的组件,用于在请求处理前后执行特定逻辑。

拦截器 vs 过滤器:

特性 拦截器(Interceptor) 过滤器(Filter)
归属 SpringMVC框架 Servlet规范
拦截范围 只拦截Controller请求 拦截所有请求(包括静态资源)
依赖注入 支持Spring依赖注入 不支持(需要特殊处理)
执行顺序 Controller前后 Servlet前后

拦截器实现示例:

java 复制代码
// 1. 创建拦截器
@Component
public class AuthInterceptor implements HandlerInterceptor {
    
    @Autowired
    private TokenService tokenService;
    
    @Override
    public boolean preHandle(HttpServletRequest request, 
                            HttpServletResponse response, 
                            Object handler) throws Exception {
        String token = request.getHeader("Authorization");
        
        if (token == null || !tokenService.validate(token)) {
            response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
            response.getWriter().write("Unauthorized");
            return false; // 拦截请求
        }
        
        return true; // 放行
    }
    
    @Override
    public void postHandle(HttpServletRequest request, 
                          HttpServletResponse response, 
                          Object handler, 
                          ModelAndView modelAndView) {
        // Controller执行后,视图渲染前执行
    }
    
    @Override
    public void afterCompletion(HttpServletRequest request, 
                               HttpServletResponse response, 
                               Object handler, 
                               Exception ex) {
        // 视图渲染后执行,常用于资源清理
    }
}

// 2. 注册拦截器
@Configuration
public class WebConfig implements WebMvcConfigurer {
    
    @Autowired
    private AuthInterceptor authInterceptor;
    
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(authInterceptor)
                .addPathPatterns("/api/**")  // 拦截路径
                .excludePathPatterns("/api/login", "/api/register"); // 排除路径
    }
}
问题2.1.2:如何处理跨域问题?

回答:

跨域(CORS)问题是浏览器的同源策略导致的。SpringMVC提供了多种解决方案:

方案一:使用@CrossOrigin注解(局部配置)

java 复制代码
@RestController
@RequestMapping("/api/user")
@CrossOrigin(origins = "http://localhost:3000", maxAge = 3600)
public class UserController {
    // 允许localhost:3000访问此Controller
}

方案二:全局配置(推荐)

java 复制代码
@Configuration
public class CorsConfig implements WebMvcConfigurer {
    
    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/api/**")  // 允许跨域的路径
                .allowedOrigins("http://localhost:3000", "https://example.com")
                .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
                .allowedHeaders("*")
                .allowCredentials(true)  // 允许携带cookie
                .maxAge(3600);  // 预检请求缓存时间
    }
}

方案三:使用过滤器(更底层)

java 复制代码
@Configuration
public class CorsFilterConfig {
    
    @Bean
    public CorsFilter corsFilter() {
        CorsConfiguration config = new CorsConfiguration();
        config.addAllowedOrigin("http://localhost:3000");
        config.addAllowedMethod("*");
        config.addAllowedHeader("*");
        config.setAllowCredentials(true);
        
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", config);
        
        return new CorsFilter(source);
    }
}
问题2.1.3:拦截器的执行顺序是怎样的?

回答:

当有多个拦截器时,执行顺序如下:

复制代码
请求 → 拦截器1.preHandle → 拦截器2.preHandle → Controller
     → 拦截器2.postHandle → 拦截器1.postHandle → 视图渲染
     → 拦截器2.afterCompletion → 拦截器1.afterCompletion → 响应

注册顺序决定执行顺序:

java 复制代码
@Override
public void addInterceptors(InterceptorRegistry registry) {
    registry.addInterceptor(new LogInterceptor()).order(1);
    registry.addInterceptor(new AuthInterceptor()).order(2);
    // order值越小越先执行
}

2.2 解析响应HTTP请求数据(包括文件格式)

问题2.2.1:如何接收不同类型的请求参数?

回答:

SpringMVC提供了多种参数绑定方式:

1. 接收URL路径参数(@PathVariable)

java 复制代码
@GetMapping("/user/{id}")
public User getUser(@PathVariable Long id) {
    return userService.findById(id);
}
// 请求:GET /user/123

2. 接收查询参数(@RequestParam)

java 复制代码
@GetMapping("/users")
public List<User> listUsers(
    @RequestParam(required = false, defaultValue = "1") Integer page,
    @RequestParam(required = false, defaultValue = "10") Integer size,
    @RequestParam(required = false) String keyword) {
    return userService.list(page, size, keyword);
}
// 请求:GET /users?page=1&size=20&keyword=张三

3. 接收请求体JSON数据(@RequestBody)

java 复制代码
@PostMapping("/user")
public Result createUser(@RequestBody User user) {
    userService.save(user);
    return Result.success();
}
// 请求:POST /user
// Body: {"name":"张三","age":25}

4. 接收表单数据(无注解或@ModelAttribute)

java 复制代码
@PostMapping("/login")
public Result login(String username, String password) {
    // 自动绑定表单字段
    return authService.login(username, password);
}

// 或使用对象接收
@PostMapping("/register")
public Result register(@ModelAttribute UserForm form) {
    return userService.register(form);
}

5. 接收请求头(@RequestHeader)

java 复制代码
@GetMapping("/info")
public Result getInfo(@RequestHeader("Authorization") String token) {
    return userService.getUserByToken(token);
}

6. 接收Cookie(@CookieValue)

java 复制代码
@GetMapping("/session")
public Result checkSession(@CookieValue("JSESSIONID") String sessionId) {
    return Result.success(sessionId);
}
问题2.2.2:如何处理文件上传?

回答:

SpringMVC使用MultipartFile处理文件上传:

单文件上传:

java 复制代码
@PostMapping("/upload")
public Result uploadFile(@RequestParam("file") MultipartFile file) {
    if (file.isEmpty()) {
        return Result.error("文件不能为空");
    }
    
    try {
        // 获取文件信息
        String originalFilename = file.getOriginalFilename();
        String contentType = file.getContentType();
        long size = file.getSize();
        
        // 保存文件
        String savePath = "/uploads/" + UUID.randomUUID() + "_" + originalFilename;
        File dest = new File(savePath);
        file.transferTo(dest);
        
        return Result.success(savePath);
        
    } catch (IOException e) {
        return Result.error("文件上传失败");
    }
}

多文件上传:

java 复制代码
@PostMapping("/batch-upload")
public Result uploadFiles(@RequestParam("files") MultipartFile[] files) {
    List<String> urls = new ArrayList<>();
    
    for (MultipartFile file : files) {
        if (!file.isEmpty()) {
            String url = saveFile(file);
            urls.add(url);
        }
    }
    
    return Result.success(urls);
}

文件上传配置:

yaml 复制代码
# application.yml
spring:
  servlet:
    multipart:
      enabled: true
      max-file-size: 10MB  # 单个文件大小限制
      max-request-size: 50MB  # 总请求大小限制
问题2.2.3:如何统一返回数据格式?

回答:

使用统一的Result类封装响应数据:

java 复制代码
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Result<T> {
    private Integer code;
    private String message;
    private T data;
    
    public static <T> Result<T> success() {
        return new Result<>(200, "success", null);
    }
    
    public static <T> Result<T> success(T data) {
        return new Result<>(200, "success", data);
    }
    
    public static <T> Result<T> error(String message) {
        return new Result<>(500, message, null);
    }
    
    public static <T> Result<T> error(Integer code, String message) {
        return new Result<>(code, message, null);
    }
}

使用示例:

java 复制代码
@GetMapping("/user/{id}")
public Result<User> getUser(@PathVariable Long id) {
    User user = userService.findById(id);
    if (user == null) {
        return Result.error(404, "用户不存在");
    }
    return Result.success(user);
}

响应JSON格式:

json 复制代码
{
    "code": 200,
    "message": "success",
    "data": {
        "id": 1,
        "name": "张三",
        "age": 25
    }
}
问题2.2.4:如何进行参数校验?

回答:

使用JSR-303(Bean Validation)进行参数校验:

1. 添加依赖:

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

2. 在实体类上添加校验注解:

java 复制代码
@Data
public class UserDTO {
    
    @NotNull(message = "用户名不能为空")
    @Size(min = 3, max = 20, message = "用户名长度必须在3-20之间")
    private String username;
    
    @NotNull(message = "密码不能为空")
    @Pattern(regexp = "^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d).{8,}$", 
             message = "密码必须包含大小写字母和数字,长度至少8位")
    private String password;
    
    @NotNull(message = "邮箱不能为空")
    @Email(message = "邮箱格式不正确")
    private String email;
    
    @Min(value = 1, message = "年龄必须大于0")
    @Max(value = 150, message = "年龄必须小于150")
    private Integer age;
}

3. 在Controller中使用@Valid或@Validated:

java 复制代码
@PostMapping("/register")
public Result register(@Valid @RequestBody UserDTO userDTO, 
                       BindingResult bindingResult) {
    // 手动处理校验结果
    if (bindingResult.hasErrors()) {
        String errorMsg = bindingResult.getAllErrors()
                .stream()
                .map(ObjectError::getDefaultMessage)
                .collect(Collectors.joining(", "));
        return Result.error(errorMsg);
    }
    
    userService.register(userDTO);
    return Result.success();
}

// 或者配合全局异常处理(推荐)
@PostMapping("/register")
public Result register(@Valid @RequestBody UserDTO userDTO) {
    // 校验失败会抛出MethodArgumentNotValidException
    // 由全局异常处理器统一处理
    userService.register(userDTO);
    return Result.success();
}

常用校验注解:

  • @NotNull:不能为null
  • @NotEmpty:不能为null且长度大于0(用于字符串、集合)
  • @NotBlank:不能为null且去除空格后长度大于0(用于字符串)
  • @Size:长度限制
  • @Min/@Max:数值范围
  • @Email:邮箱格式
  • @Pattern:正则表达式
  • @Past/@Future:日期必须是过去/将来

2.3 捕获处理全局异常

问题2.3.1:Java异常体系有哪些?

回答:

Java异常体系分为两大类:

复制代码
Throwable
├── Error(错误,程序无法处理)
│   ├── OutOfMemoryError
│   ├── StackOverflowError
│   └── VirtualMachineError
│
└── Exception(异常,程序可以处理)
    ├── RuntimeException(运行时异常,非受检异常)
    │   ├── NullPointerException
    │   ├── ArrayIndexOutOfBoundsException
    │   ├── IllegalArgumentException
    │   └── NumberFormatException
    │
    └── 受检异常(编译时异常,必须处理)
        ├── IOException
        ├── SQLException
        └── ClassNotFoundException

关键区别:

  1. Error:严重错误,程序无法处理(如内存溢出)
  2. RuntimeException:运行时异常,可以不显式捕获(如空指针)
  3. 受检异常:编译器强制要求处理(try-catch或throws)
问题2.3.2:全局异常处理的最佳实践是什么?

回答:

使用@ControllerAdvice + @ExceptionHandler实现全局异常处理:

1. 自定义业务异常:

java 复制代码
@Data
public class BusinessException extends RuntimeException {
    private Integer code;
    
    public BusinessException(String message) {
        super(message);
        this.code = 500;
    }
    
    public BusinessException(Integer code, String message) {
        super(message);
        this.code = code;
    }
}

2. 全局异常处理器:

java 复制代码
@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {
    
    /**
     * 处理业务异常
     */
    @ExceptionHandler(BusinessException.class)
    public Result handleBusinessException(BusinessException e) {
        log.error("业务异常:{}", e.getMessage());
        return Result.error(e.getCode(), e.getMessage());
    }
    
    /**
     * 处理参数校验异常
     */
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public Result handleValidationException(MethodArgumentNotValidException e) {
        String errorMsg = e.getBindingResult().getAllErrors()
                .stream()
                .map(ObjectError::getDefaultMessage)
                .collect(Collectors.joining(", "));
        log.error("参数校验失败:{}", errorMsg);
        return Result.error(400, errorMsg);
    }
    
    /**
     * 处理参数绑定异常
     */
    @ExceptionHandler(BindException.class)
    public Result handleBindException(BindException e) {
        String errorMsg = e.getBindingResult().getAllErrors()
                .stream()
                .map(ObjectError::getDefaultMessage)
                .collect(Collectors.joining(", "));
        return Result.error(400, errorMsg);
    }
    
    /**
     * 处理空指针异常
     */
    @ExceptionHandler(NullPointerException.class)
    public Result handleNullPointerException(NullPointerException e) {
        log.error("空指针异常", e);
        return Result.error("系统异常,请联系管理员");
    }
    
    /**
     * 处理SQL异常
     */
    @ExceptionHandler(SQLException.class)
    public Result handleSQLException(SQLException e) {
        log.error("数据库异常", e);
        return Result.error("数据库操作失败");
    }
    
    /**
     * 处理其他未捕获的异常
     */
    @ExceptionHandler(Exception.class)
    public Result handleException(Exception e) {
        log.error("未知异常", e);
        return Result.error("系统异常,请联系管理员");
    }
}

3. 业务代码中使用:

java 复制代码
@Service
public class UserService {
    
    public User findById(Long id) {
        User user = userMapper.selectById(id);
        if (user == null) {
            throw new BusinessException(404, "用户不存在");
        }
        return user;
    }
    
    public void updateUser(User user) {
        if (user.getAge() < 0) {
            throw new BusinessException("年龄不能为负数");
        }
        userMapper.updateById(user);
    }
}
问题2.3.3:如何区分不同环境下的异常信息?

回答:

在生产环境应该隐藏敏感的异常信息,开发环境可以显示详细堆栈:

java 复制代码
@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {
    
    @Value("${spring.profiles.active}")
    private String activeProfile;
    
    @ExceptionHandler(Exception.class)
    public Result handleException(Exception e) {
        log.error("系统异常", e);
        
        // 开发环境返回详细错误信息
        if ("dev".equals(activeProfile)) {
            return Result.error(500, e.getMessage());
        }
        
        // 生产环境返回通用错误信息
        return Result.error("系统异常,请联系管理员");
    }
}
问题2.3.4:异常处理的最佳实践总结

回答:

  1. 分层处理异常

    • Controller层:处理参数校验异常
    • Service层:处理业务逻辑异常
    • DAO层:处理数据访问异常
  2. 自定义异常

    • 定义统一的业务异常类
    • 使用异常码区分不同的异常类型
  3. 全局异常处理

    • 使用@ControllerAdvice统一处理
    • 区分不同类型的异常,返回不同的错误信息
    • 记录详细日志,方便排查问题
  4. 用户友好的错误信息

    • 生产环境不暴露敏感信息
    • 返回用户可理解的错误提示
  5. 日志记录

    • 所有异常都应记录日志
    • 区分日志级别(error、warn、info)

3. 补充知识点

3.1 SpringMVC的核心组件

问题:SpringMVC的执行流程是怎样的?

回答:

复制代码
1. 客户端发送请求 → DispatcherServlet(前端控制器)
2. DispatcherServlet → HandlerMapping(处理器映射器)查找Handler
3. HandlerMapping → DispatcherServlet 返回HandlerExecutionChain
4. DispatcherServlet → HandlerAdapter(处理器适配器)执行Handler
5. HandlerAdapter → Controller 执行业务逻辑
6. Controller → HandlerAdapter 返回ModelAndView
7. HandlerAdapter → DispatcherServlet 返回ModelAndView
8. DispatcherServlet → ViewResolver(视图解析器)解析视图
9. ViewResolver → DispatcherServlet 返回View
10. DispatcherServlet → View 渲染视图
11. View → 客户端 返回响应

核心组件:

  • DispatcherServlet:前端控制器,整个流程的中心
  • HandlerMapping:根据URL找到对应的Handler(Controller方法)
  • HandlerAdapter:执行Handler
  • ViewResolver:视图解析器(前后端分离项目中较少使用)
  • Interceptor:拦截器

3.2 RESTful API设计规范

问题:如何设计符合RESTful规范的API?

回答:

RESTful API设计的核心原则:

  1. 使用HTTP方法表示操作

    • GET:查询资源
    • POST:创建资源
    • PUT:更新资源(全量更新)
    • PATCH:更新资源(部分更新)
    • DELETE:删除资源
  2. URL命名规范

    复制代码
    ✅ 好的设计:
    GET    /api/users          查询用户列表
    GET    /api/users/123      查询单个用户
    POST   /api/users          创建用户
    PUT    /api/users/123      更新用户
    DELETE /api/users/123      删除用户
    
    ❌ 不好的设计:
    GET    /api/getUserList
    POST   /api/createUser
    GET    /api/deleteUser?id=123
  3. 使用HTTP状态码

    • 200:成功
    • 201:创建成功
    • 400:请求参数错误
    • 401:未认证
    • 403:无权限
    • 404:资源不存在
    • 500:服务器错误

示例代码:

java 复制代码
@RestController
@RequestMapping("/api/users")
public class UserController {
    
    @GetMapping
    public Result<List<User>> list(@RequestParam Integer page, 
                                    @RequestParam Integer size) {
        return Result.success(userService.list(page, size));
    }
    
    @GetMapping("/{id}")
    public Result<User> getById(@PathVariable Long id) {
        return Result.success(userService.findById(id));
    }
    
    @PostMapping
    public Result create(@Valid @RequestBody User user) {
        userService.save(user);
        return Result.success();
    }
    
    @PutMapping("/{id}")
    public Result update(@PathVariable Long id, 
                        @Valid @RequestBody User user) {
        user.setId(id);
        userService.update(user);
        return Result.success();
    }
    
    @DeleteMapping("/{id}")
    public Result delete(@PathVariable Long id) {
        userService.delete(id);
        return Result.success();
    }
}

3.3 常见面试题

问题:@Controller和@RestController的区别?

回答:

  • @Controller:返回视图(HTML页面)
  • @RestController = @Controller + @ResponseBody:返回JSON数据

问题:Get和Post请求的区别?

回答:

特性 GET POST
参数位置 URL中(查询字符串) 请求体中
安全性 参数暴露在URL中 相对安全
数据大小限制 受URL长度限制(2KB) 无限制
幂等性 幂等(多次请求结果相同) 非幂等
缓存 可以被缓存 不能被缓存
语义 查询数据 提交数据

问题:如何实现接口幂等性?

回答:

幂等性是指多次调用产生的结果与单次调用相同。实现方案:

  1. Token机制:前端先获取token,提交时携带,后端验证并删除
  2. 分布式锁:使用Redis等实现
  3. 数据库唯一索引:防止重复插入
  4. 状态机:通过状态流转控制
java 复制代码
// Token机制示例
@PostMapping("/order")
public Result createOrder(@RequestHeader("Idempotent-Token") String token,
                         @RequestBody Order order) {
    // 验证并删除token
    if (!redisTemplate.delete("token:" + token)) {
        throw new BusinessException("请勿重复提交");
    }
    
    orderService.create(order);
    return Result.success();
}

总结

SpringMVC作为Spring生态的重要组成部分,提供了完整的Web开发解决方案。掌握其核心机制------拦截器、参数绑定、异常处理、跨域处理等,是每个Java后端开发者的必备技能。

本文从实战角度出发,覆盖了SpringMVC在实际项目中的常见使用场景和最佳实践,希望能帮助你快速掌握SpringMVC的核心知识,在面试和实际开发中游刃有余。

相关推荐
geekmice4 小时前
实现一个功能:springboot项目启动将controller地址拼接打印到txt文件
java·spring boot·后端
老华带你飞4 小时前
旅游|基于Java旅游信息系统(源码+数据库+文档)
java·开发语言·数据库·vue.js·spring boot·旅游
qq_589568105 小时前
mybatis-plus和springboot项目错误记录
spring boot·后端·mybatis
一 乐6 小时前
高校评教|基于SpringBoot+vue高校学生评教系统 (源码+数据库+文档)
java·数据库·vue.js·spring boot·后端·学习
哈哈老师啊8 小时前
Springboot学生接送服务平台8rzvo(程序+源码+数据库+调试部署+开发环境)带论文文档1万字以上,文末可获取,系统界面在最后面。
数据库·spring boot·后端
VX:Fegn08958 小时前
计算机毕业设计|基于springboot + vue图书商城系统(源码+数据库+文档)
数据库·vue.js·spring boot·后端·课程设计
小安同学iter9 小时前
天机学堂day05
java·开发语言·spring boot·分布式·后端·spring cloud·微服务
一 乐9 小时前
物业管理|基于SprinBoot+vue的智慧物业管理系统(源码+数据库+文档)
前端·javascript·数据库·vue.js·spring boot
白露与泡影10 小时前
springboot中File默认路径
java·spring boot·后端