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会自动完成以下配置:
- 内嵌Tomcat容器:无需单独部署war包,直接运行jar包即可
- DispatcherServlet自动注册:核心前端控制器自动配置
- 默认的视图解析器、消息转换器:如Jackson用于JSON转换
- 静态资源映射 :自动配置
/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
关键区别:
- Error:严重错误,程序无法处理(如内存溢出)
- RuntimeException:运行时异常,可以不显式捕获(如空指针)
- 受检异常:编译器强制要求处理(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:异常处理的最佳实践总结
回答:
-
分层处理异常:
- Controller层:处理参数校验异常
- Service层:处理业务逻辑异常
- DAO层:处理数据访问异常
-
自定义异常:
- 定义统一的业务异常类
- 使用异常码区分不同的异常类型
-
全局异常处理:
- 使用
@ControllerAdvice统一处理 - 区分不同类型的异常,返回不同的错误信息
- 记录详细日志,方便排查问题
- 使用
-
用户友好的错误信息:
- 生产环境不暴露敏感信息
- 返回用户可理解的错误提示
-
日志记录:
- 所有异常都应记录日志
- 区分日志级别(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设计的核心原则:
-
使用HTTP方法表示操作:
- GET:查询资源
- POST:创建资源
- PUT:更新资源(全量更新)
- PATCH:更新资源(部分更新)
- DELETE:删除资源
-
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 -
使用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) | 无限制 |
| 幂等性 | 幂等(多次请求结果相同) | 非幂等 |
| 缓存 | 可以被缓存 | 不能被缓存 |
| 语义 | 查询数据 | 提交数据 |
问题:如何实现接口幂等性?
回答:
幂等性是指多次调用产生的结果与单次调用相同。实现方案:
- Token机制:前端先获取token,提交时携带,后端验证并删除
- 分布式锁:使用Redis等实现
- 数据库唯一索引:防止重复插入
- 状态机:通过状态流转控制
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的核心知识,在面试和实际开发中游刃有余。