3.2.2.SpringMVC简介
3.2.2.SpringMVC
Spring MVC(Model-View-Controller)是一个用于构建Web应用程序的框架,属于Spring Framework的一部分。它采用了MVC设计模式,并通过Spring提供的各种功能来管理Web应用程序的请求、响应、控制和展示逻辑。Spring MVC的设计理念是解耦(Decoupling),即将业务逻辑、显示层、数据存储层等模块分开,使得各个模块之间的依赖最小化。
3.2.2.1.Spring MVC架构设计
Spring MVC采用经典的MVC(Model-View-Controller)设计模式:
Model(模型):负责处理业务逻辑和数据。通常,Model是一个POJO(Plain Old Java Object),代表应用程序的业务对象。在Spring MVC中,Model是传递给视图的数据部分。
View(视图):负责将Model中的数据呈现给用户,通常是JSP、Thymeleaf、FreeMarker等技术。View呈现最终的用户界面。
Controller(控制器):充当中介者,接收用户的请求,执行业务逻辑,并返回视图。它从Model获取数据,并将该数据传递给View。
3.2.2.2.Spring MVC整体流程
Spring MVC使用一个请求-响应的流程进行工作,基本流程如下:
客户端请求:浏览器发出HTTP请求,通常是通过URL(如 /home)发出。
DispatcherServlet接收请求:所有的请求都通过DispatcherServlet,它是Spring MVC的前端控制器。
请求映射到控制器:DispatcherServlet根据URL和映射配置,通过HandlerMapping来找到对应的控制器方法。
控制器处理请求:控制器处理请求,执行业务逻辑,并将数据放入Model中。
视图解析:返回的ModelAndView对象中包含了数据和视图名称,DispatcherServlet将根据视图名称通过ViewResolver解析得到最终视图。
渲染视图并返回响应:最后,DispatcherServlet将Model渲染到视图中,并通过HTTP响应返回给客户端。
3.2.2.3.Spring MVC核心组件
Spring MVC的核心组件是:DispatcherServlet、HandlerMapping、Controller、ModelAndView、ViewResolver等。每个组件在框架中都扮演着重要角色,下面将详细介绍它们。
1)DispatcherServlet(前端控制器)
DispatcherServlet是Spring MVC的核心,是所有请求的入口。它作为前端控制器(Front Controller)接收客户端请求,并将请求分发到相应的处理器(Controller)。
功能:
请求接收:接收来自客户端的HTTP请求。
请求分发:根据请求的URL和配置,选择合适的控制器来处理请求。
视图解析:请求处理后,返回视图(如JSP、HTML、JSON等),将数据填充到视图中。
2)HandlerMapping(请求映射)
HandlerMapping负责将用户的请求URL映射到具体的控制器方法。Spring MVC提供了多个HandlerMapping的实现来支持不同类型的请求映射:
AnnotationMethodHandlerMapping:通过@RequestMapping等注解映射请求。
SimpleUrlHandlerMapping:根据URL将请求映射到处理器Bean。
BeanNameUrlHandlerMapping:根据Bean的名称进行映射。
常见的映射方法:
@RequestMapping:最常用的映射注解,用于映射请求的URL。
@GetMapping / @PostMapping / @PutMapping / @DeleteMapping:分别映射GET、POST、PUT、DELETE请求。
示例:
@Controller public class MyController { @RequestMapping("/hello") public String sayHello() { return "hello"; } } |
3)Controller(控制器)
控制器是Spring MVC的核心组件,负责处理用户的请求,执行相应的业务逻辑,并将数据传递给视图。控制器通常是一个Java类,带有@Controller注解,并包含多个请求处理方法。
常用注解:
@Controller:标注为控制器类,负责处理用户请求。
@RequestMapping:标注方法来处理指定URL的请求。
@RestController:是@Controller和@ResponseBody的组合,主要用于RESTful服务。
@RequestParam:用于接收请求参数。
@PathVariable:从路径中提取参数。
@ModelAttribute:将表单数据绑定到Java对象。
示例:
@Controller public class UserController { @RequestMapping("/user/{id}") public String getUser(@PathVariable("id") int userId, Model model) { User user = userService.getUserById(userId); model.addAttribute("user", user); return "userDetail"; } } |
4)ModelAndView(模型与视图)
ModelAndView是Spring MVC中的一个重要对象,用来封装模型数据和视图信息。它包含两部分内容:
Model:封装业务数据(如通过model.addAttribute()传递的对象)。
View:指定渲染的视图(如JSP、HTML等)。
@RequestMapping("/hello") public ModelAndView sayHello() { ModelAndView modelAndView = new ModelAndView(); modelAndView.setViewName("hello"); modelAndView.addObject("message", "Hello, Spring MVC!"); return modelAndView; } |
5)ViewResolver(视图解析器)
ViewResolver用于根据控制器返回的视图名称解析出最终的视图资源。Spring MVC提供了几种常见的视图解析器:
InternalResourceViewResolver:基于JSP的视图解析器。
ThymeleafViewResolver:支持Thymeleaf模板的视图解析器。
FreeMarkerViewResolver:支持FreeMarker的视图解析器。
@Bean public InternalResourceViewResolver viewResolver() { InternalResourceViewResolver resolver = new InternalResourceViewResolver(); resolver.setPrefix("/WEB-INF/views/"); resolver.setSuffix(".jsp"); return resolver; } |
6)HandlerInterceptor(处理器拦截器)
HandlerInterceptor用于在请求处理过程的不同阶段执行逻辑,例如在请求到达控制器之前、控制器处理之后、视图渲染之前等。
常用方法:
preHandle():请求处理之前执行。
postHandle():请求处理之后执行,但在视图渲染之前。
afterCompletion():视图渲染之后执行,清理资源。
public class MyInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { System.out.println("Request pre-handling"); return true; // return true to continue the request, false to stop } } |
3.2.2.4.Spring MVC的优缺点
优点:
松耦合设计:Spring MVC通过分离控制器、模型和视图,使得各个模块的耦合度较低,便于维护和扩展。
灵活性:Spring MVC支持多种视图技术(JSP、Thymeleaf、FreeMarker等),并且具有强大的扩展性,可以与其他Spring模块无缝集成。
支持RESTful架构:Spring MVC提供了非常好的支持RESTful Web服务的能力,通过@RestController等注解来简化RESTful API开发。
强大的注解支持:通过注解可以轻松进行请求映射、参数绑定、异常处理等操作,使得代码更加简洁和清晰。
缺点:
配置繁琐:Spring MVC的配置文件较多,尤其是早期的XML配置方式可能使得项目配置较为繁杂。
学习曲线较高:Spring MVC的概念较为抽象,尤其是对于初学者,可能需要一定的时间来理解其工作流程。
性能开销:由于其基于Servlet的模型,性能可能不如某些轻量级框架(如Spring Boot嵌入式服务器)或者原生Servlet实现。
3.2.2.5.管理SpringBean
在 Spring 中,Spring Bean 的管理 是核心的一部分。它涉及如何创建、配置、注入和销毁应用程序中的各种组件(Bean)。Spring 提供了强大且灵活的机制来管理 Bean 生命周期、作用域和依赖注入。做好 Spring Bean 的管理不仅有助于优化代码结构,也能提升系统的可维护性和扩展性。
1)合理使用 Spring Bean 的作用域(Scope)
Spring 提供了不同的作用域来管理Bean实例的生命周期。根据需求,选择合适的作用域非常重要。
singleton(单例):默认作用域。Spring 容器启动时会创建一个单例对象,并在整个应用程序的生命周期中共享这个实例。适用于无状态的 Bean。
prototype(原型):每次请求都会创建一个新的 Bean 实例。适用于有状态的 Bean,或者需要动态创建的 Bean。
request:在 Web 应用程序中,每一个 HTTP 请求都会创建一个新的 Bean 实例。适用于请求级别的 Bean。
session:每一个 HTTP 会话都会创建一个新的 Bean 实例。适用于会话级别的 Bean。
application:类似于 singleton,但是针对每个 Spring 容器实例,适用于上下文级别的管理。
使用示例:
@Component @Scope("singleton") // 默认作用域 public class MySingletonBean { // ... 业务逻辑 } @Component @Scope("prototype") // 每次注入时都创建新的实例 public class MyPrototypeBean { // ... 业务逻辑 } |
2)合理使用依赖注入(DI)
Spring 提供了两种常用的依赖注入方式:构造器注入和Setter 注入。构造器注入被认为是更推荐的方式,尤其是在涉及到不可变依赖时。
构造器注入:使用构造函数注入依赖。这种方式的好处是可以让 Bean 在创建时就注入依赖,确保依赖是不可变的。
@Component public class MyService { private final MyRepository repository; @Autowired public MyService(MyRepository repository) { this.repository = repository; } // 使用 repository } |
Setter 注入:通过 setter 方法来注入依赖。适合于可选的依赖,或当依赖关系较复杂时。
@Component public class MyService { private MyRepository repository; @Autowired public void setRepository(MyRepository repository) { this.repository = repository; } // 使用 repository } |
字段注入:通过直接在字段上注入依赖。这种方式很简洁,但不推荐用于必须初始化的依赖。
@Component public class MyService { @Autowired private MyRepository repository; } |
推荐做法:
优先使用 构造器注入,可以确保依赖是不可变的,有助于避免空指针异常。
Setter 注入 适合于可选依赖。
避免过多使用 字段注入,因为它隐藏了依赖关系,降低了代码可读性和可测试性。
3)使用 @Qualifier 解决 Bean 注入冲突
如果存在多个类型相同的 Bean 时,Spring 会不知道注入哪个 Bean,这时可以使用 @Qualifier 来指定要注入的具体 Bean。
@Component public class MyService { private final MyRepository repository; @Autowired public MyService(@Qualifier("myRepositoryImpl") MyRepository repository) { this.repository = repository; } } |
@Qualifier 注解与 @Autowired 配合使用,可以指定需要注入的具体 Bean 名称。
4)合理使用 @Primary
如果有多个候选 Bean 可以注入,并且你希望 Spring 自动注入其中一个作为默认选择,可以使用 @Primary 注解。
@Component @Primary public class PrimaryRepository implements MyRepository { // 实现方法 } @Component public class SecondaryRepository implements MyRepository { // 实现方法 } |
如果没有使用 @Qualifier 注解,Spring 会默认选择 @Primary 注解的 Bean。
5)初始化和销毁方法
在 Bean 的生命周期中,有时需要进行初始化和销毁操作。Spring 提供了两种方式来处理这些操作:
使用 @PostConstruct 和 @PreDestroy 注解。
@Component public class MyBean { @PostConstruct public void init() { // 初始化逻辑 } @PreDestroy public void cleanup() { // 销毁逻辑 } } |
使用 InitializingBean 和 DisposableBean 接口。
@Component public class MyBean implements InitializingBean, DisposableBean { @Override public void afterPropertiesSet() throws Exception { // 初始化逻辑 } @Override public void destroy() throws Exception { // 销毁逻辑 } } |
使用 @Bean 注解的 initMethod 和 destroyMethod 属性(适用于 Java 配置方式)。
@Configuration public class AppConfig { @Bean(initMethod = "init", destroyMethod = "cleanup") public MyBean myBean() { return new MyBean(); } } |
这些方法可以帮助你在 Bean 的生命周期中执行一些初始化和销毁操作,确保资源的正确释放。
6)使用 @Profile 管理环境配置
当你在不同的环境中使用 Spring 应用(例如开发、测试、生产环境)时,可以使用 @Profile 注解来管理 Bean 的激活状态。
@Component @Profile("dev") public class DevService implements MyService { // 开发环境的实现 } @Component @Profile("prod") public class ProdService implements MyService { // 生产环境的实现 } |
然后在配置文件中设置激活的 profile:
application.properties
spring.profiles.active=dev |
使用 @Profile 可以让你的 Spring 应用根据不同的环境注入不同的 Bean,提高灵活性。
7)避免过度注入和过度依赖
避免过度注入:当一个 Bean 的依赖项过多时,可能是设计上的问题。尽量遵循 单一职责原则,拆分过大的类和过多依赖。
避免过度依赖:避免在类中注入大量的服务。可以通过 Service Layer 或 Facade 模式来简化依赖关系。
8)AOP 管理 Bean 的行为
使用 AOP(面向切面编程) 可以帮助你对 Bean 的方法进行拦截和处理,增加日志、事务等功能,而不必修改原始代码。
@Aspect @Component public class LoggingAspect { @Before("execution(* com.example.service.*.*(..))") public void logBefore(JoinPoint joinPoint) { System.out.println("Before method: " + joinPoint.getSignature().getName()); } } |
通过 AOP 管理 Bean 的横切关注点,可以提高代码的复用性和解耦性。
3.2.2.6.避免重复Bean
在 Spring 中,避免重复的 Bean 是一个重要的设计考量,尤其是在复杂的项目中。重复的 Bean 会导致依赖注入错误、程序逻辑不清晰,甚至可能引发 NoUniqueBeanDefinitionException 错误(当存在多个相同类型的 Bean 时)。为了有效避免重复 Bean 的问题,可以采取以下几种策略:
1)使用 @Qualifier 注解指定 Bean 名称
当有多个类型相同的 Bean 时,Spring 会默认选择第一个匹配的 Bean。为了明确指定使用哪个 Bean,可以使用 @Qualifier 注解指定具体的 Bean 名称。
@Component("beanA") public class MyBeanA implements MyBean { // Bean A 实现 } @Component("beanB") public class MyBeanB implements MyBean { // Bean B 实现 } @Component public class MyService { @Autowired @Qualifier("beanA") private MyBean myBean; // 明确指定注入 beanA } |
优点:
使用 @Qualifier 注解,可以显式地告诉 Spring 使用哪个 Bean,避免了多个同类型 Bean 的冲突。
注意:
@Qualifier 必须和 @Autowired 一起使用,否则 Spring 无法知道注入哪个 Bean。
2)使用 @Primary 注解指定默认 Bean
当你有多个同类型的 Bean 时,可以通过 @Primary 注解指定一个默认的 Bean。当没有明确指定的情况下,Spring 会选择 @Primary 注解的 Bean。
@Component @Primary public class MyBeanA implements MyBean { // 实现 A } @Component public class MyBeanB implements MyBean { // 实现 B } |
优点:
@Primary 注解让你在多个同类型的 Bean 中指定一个默认的 Bean。
可以减少使用 @Qualifier 的需求,简化配置。
注意:
仅适用于没有明确指定的场景,如果明确使用 @Qualifier,@Primary 不会生效。
3)避免 Bean 的重复定义(使用唯一 Bean 名称)
在 Spring 中,Bean 的名字默认是类名的首字母小写。确保在 @Component、@Service、@Repository 等注解中指定唯一的 Bean 名称,避免多个 Bean 使用相同名称。
@Component("myUniqueBeanName") public class MyService { // 业务逻辑 } |
优点:
保证了 Bean 的名称唯一性,避免了重复的 Bean 名称问题。
注意:
在复杂的项目中,建议使用规范的命名策略来管理 Bean 名称,避免出现冲突。
4)使用 @Profile 来分离不同环境的 Bean 定义
如果你在开发、测试和生产环境中需要使用不同的 Bean,可以通过 @Profile 注解来管理 Bean 的加载,避免不同环境中重复加载相同的 Bean。
@Component @Profile("dev") public class DevService implements MyService { // 开发环境的实现 } @Component @Profile("prod") public class ProdService implements MyService { // 生产环境的实现 } |
优点:
@Profile 可以帮助你在不同的环境中加载不同的 Bean,避免不同环境中的重复 Bean。
注意:
使用@Profile 时,确保配置文件中正确指定了激活的 Profile,例如:spring.profiles.active=dev。
5)避免多个配置文件定义相同 Bean
在使用 Java 配置方式时,可能会在不同的 @Configuration 文件中定义相同类型的 Bean。确保在配置类中没有重复定义相同的 Bean。
@Configuration public class ConfigA { @Bean public MyService myServiceA() { return new MyServiceImpl(); } } @Configuration public class ConfigB { @Bean public MyService myServiceB() { return new MyServiceImpl(); } } |
这种配置容易引发 Bean 的重复定义错误,尤其是当两个配置文件中都定义了相同类型的 Bean 时。为了解决这个问题,可以:
使用不同的名称来区分 Bean。
使用条件注解(如 @Profile)来确保只在特定环境中加载某个 Bean。
6)使用 @Import 来导入 Bean 定义
当你有多个配置类时,确保每个配置类的责任清晰且不会重复。如果需要将多个配置类合并,可以使用 @Import 注解导入其他配置类。
@Configuration @Import(OtherConfig.class) public class MainConfig { // 主配置 } |
优点:
@Import 可以帮助你清晰地管理多个配置类,避免重复 Bean 定义。
7)手动注册 Bean 时检查重复
如果你使用了 AnnotationConfigApplicationContext 或 GenericWebApplicationContext 等手动注册 Bean 的方式,在注册 Bean 时,要特别注意检查是否存在重复定义的情况。
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(); context.register(MyConfig.class); context.refresh(); // 检查是否有重复的 Bean if (context.getBeansOfType(MyService.class).size() > 1) { throw new IllegalStateException("存在多个 MyService Bean 定义!"); } |
优点:
这种方法可以帮助你在程序启动时主动检查是否有重复的 Bean 定义。
8)避免自动扫描重复包路径
当使用组件扫描时,确保没有扫描到重复的包路径。如果你的项目结构很复杂,可能会无意中扫描到重复的包或类,导致同样的 Bean 被创建多次。可以通过 @ComponentScan 来指定扫描的范围,避免重复扫描。
@ComponentScan(basePackages = "com.example.services") |
优点:
通过限制 @ComponentScan 的范围,避免多次扫描相同的 Bean 类。
9)检查第三方库中的 Bean 定义
如果你使用了第三方库(如 Spring Boot Starter),它们有时会自动注册一些 Bean。如果这些 Bean 与你的应用中的 Bean 重名或类型相同,可能会引发冲突。可以通过 @Primary 或 @Qualifier 解决,或者在配置文件中禁用某些自动装配。
10)避免不必要的注入
避免过度注入 Bean,特别是在某些类中注入大量不同类型的 Bean。如果一个类有过多的依赖项,它可能是一个 过度设计的类,应考虑重新设计或者拆分类。
11)必要时使用文档来记录、管理Bean
参考:Bean管理文档
