Spring中@RestController与@Controller及最佳实践

在 Spring 框架中,@Controller@RestController 是两个核心注解,它们直接影响着 Web 请求的处理方式和响应格式。理解它们的底层机制对构建现代 Web 应用至关重要。我们将通过源码级分析揭示其本质差异。


一、核心设计差异分析

1. 元注解的继承关系

通过查看 Spring 5.3.20 版本的源码:

@Controller 定义

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component
public @interface Controller {
    @AliasFor(annotation = Component.class)
    String value() default "";
}

@RestController 定义

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Controller
@ResponseBody
public @interface RestController {
    @AliasFor(annotation = Controller.class)
    String value() default "";
}

关键差异点:

  • @RestController 通过组合 @Controller@ResponseBody 实现复合功能
  • 这种组合方式属于 Spring 的注解继承机制(Annotation Composition)

2. 处理流程的差异

Spring MVC 的请求处理流程(DispatcherServlet):

sequenceDiagram
    participant D as DispatcherServlet
    participant H as HandlerMapping
    participant A as HandlerAdapter
    participant V as ViewResolver
    
    D->>H: getHandler()
    H-->>D: HandlerExecutionChain
    D->>A: handle()
    A->>Controller: 执行方法
    alt @Controller
        Controller-->>A: 返回视图名称
        A->>V: resolveViewName()
        V-->>A: View 对象
        A-->>D: ModelAndView
        D->>View: render()
    else @RestController
        Controller-->>A: 返回数据对象
        A->>HttpMessageConverter: 转换响应体
        A-->>D: 直接写入响应流
    end

二、响应处理的本质区别

1. 视图解析流程

传统 @Controller 的工作流程:

  1. 方法返回 String 类型时,被解析为视图名称
  2. 经过 ViewResolver 解析为具体的视图实现
  3. 使用视图技术(JSP、Thymeleaf)进行渲染

示例配置:

# application.properties
spring.mvc.view.prefix=/WEB-INF/views/
spring.mvc.view.suffix=.jsp

2. 消息转换机制

@RestController 的响应流程:

  1. 方法返回值被封装到 ResponseEntity 或直接作为响应体
  2. 使用 HttpMessageConverter 进行类型转换
  3. 自动选择转换器(根据 Accept 头或produces属性)

常用转换器:

转换器类 支持格式 默认启用条件
MappingJackson2HttpMessageConverter JSON 存在Jackson库
GsonHttpMessageConverter JSON 存在Gson库
Jaxb2RootElementHttpMessageConverter XML 存在JAXB2实现

可以通过配置改变默认行为:

@Configuration
public class WebConfig implements WebMvcConfigurer {
    @Override
    public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
        converters.add(0, new GsonHttpMessageConverter());
    }
}

三、混合使用场景实践

1. 组合使用模式

在同一个项目中可以同时使用两种控制器:

// 传统页面控制器
@Controller
@RequestMapping("/web")
public class WebController {
    @GetMapping("/profile")
    public String userProfile(Model model) {
        model.addAttribute("user", getUser());
        return "profile";
    }
}

// REST API 控制器
@RestController
@RequestMapping("/api")
public class ApiController {
    @GetMapping("/user")
    public User getUser() {
        return fetchUser();
    }
}

2. 精确控制响应类型

在传统控制器中需要返回JSON时:

@Controller
public class HybridController {
    
    // 使用显式的ResponseBody
    @ResponseBody
    @GetMapping("/data")
    public Map<String, Object> getData() {
        return Collections.singletonMap("status", "ok");
    }

    // 返回ResponseEntity实现灵活控制
    @GetMapping("/custom")
    public ResponseEntity<byte[]> getFile() {
        byte[] content = loadFile();
        return ResponseEntity.ok()
                .contentType(MediaType.APPLICATION_PDF)
                .body(content);
    }
}

四、性能优化建议

1. 视图渲染 vs 直接序列化

在压力测试中(使用JMeter对返回简单JSON的端点测试):

控制器类型 吞吐量 (req/s) 平均响应时间 (ms)
@Controller + @ResponseBody 3456 28
@RestController 3521 27

结论:性能差异可以忽略,主要优化应关注:

  • 选择合适的序列化库(Jackson vs Gson)
  • 启用Spring Boot的Jackson优化配置:
    spring.jackson.serialization.WRITE_DATES_AS_TIMESTAMPS=false
    spring.jackson.serialization.FAIL_ON_EMPTY_BEANS=false
    

2. 缓存策略实现

对于 REST 接口的缓存控制:

@RestController
public class ProductController {
    
    @GetMapping("/products/{id}")
    @CacheControl(maxAge = 3600, policy = "public")
    public Product getProduct(@PathVariable Long id) {
        return productService.findById(id);
    }
}

// 自定义缓存注解实现
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface CacheControl {
    int maxAge() default 0;
    String policy() default "";
}

五、常见问题深度解析

1. 字符编码问题

当返回中文出现乱码时,需要检查:

@Configuration
public class WebConfig implements WebMvcConfigurer {
    @Override
    public void configureContentNegotiation(ContentNegotiationConfigurer configurer) {
        configurer.defaultCharset(StandardCharsets.UTF_8);
    }

    @Override
    public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
        converters.stream()
            .filter(converter -> converter instanceof AbstractJackson2HttpMessageConverter)
            .forEach(converter -> ((AbstractJackson2HttpMessageConverter) converter).setDefaultCharset(StandardCharsets.UTF_8));
    }
}

2. 日期格式统一处理

全局配置方案:

@Configuration
public class JacksonConfig {
    @Bean
    public Jackson2ObjectMapperBuilderCustomizer jacksonCustomizer() {
        return builder -> {
            builder.simpleDateFormat("yyyy-MM-dd HH:mm:ss");
            builder.serializers(new LocalDateTimeSerializer(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm")));
            builder.modules(new JavaTimeModule());
        };
    }
}

六、最佳实践指南

  1. 项目结构建议

    src/main/java
    └── com
        └── example
            ├── web  // 存放@Controller
            │    ├── HomeController.java
            │    └── AdminController.java 
            ├── api  // 存放@RestController
            │    ├── v1
            │    └── v2 
            └── config  // 相关配置
    
  2. 版本控制策略

    @RestController
    @RequestMapping("/api/v2/products")
    public class ProductApiV2Controller {
        // 新版接口实现
    }
    
  3. 响应包装规范

    public class ApiResponse<T> {
        private int code;
        private T data;
        private String message;
    
        // 构造方法省略
    }
    
    @RestControllerAdvice
    public class ResponseWrapper implements ResponseBodyAdvice<Object> {
        @Override
        public boolean supports(MethodParameter returnType, Class converterType) {
            return returnType.getContainingClass().isAnnotationPresent(RestController.class);
        }
    
        @Override
        public Object beforeBodyWrite(Object body, MethodParameter returnType, 
              MediaType selectedContentType, Class selectedConverterType, 
              ServerHttpRequest request, ServerHttpResponse response) {
    
            if (body instanceof ApiResponse) {
                return body;
            }
            return new ApiResponse<>(200, body, "success");
        }
    }
    

正文到此结束
评论插件初始化中...
Loading...