SpringBoot 分层解耦
从没有分层思想到传统 Web 分层,再到 Spring Boot 分层架构
1. 没有分层思想
在最初的项目开发中,很多开发者并没有明确的分层思想,所有逻辑都堆砌在一个类或一个方法中。这样的开发方式通常会导致以下问题:
-
代码混乱:没有清晰的层次,业务逻辑、数据访问、UI 逻辑等混在一起,导致代码难以维护。
-
重复代码多:由于没有模块化的结构,很多功能会被重复实现。
-
难以扩展和测试:随着项目变大,增加新功能或修改功能会变得困难,因为不同的功能可能会交叉影响。
举例:
假设我们有一个简单的用户查询功能:public class UserService {public User getUserById(Long id) {// 数据库查询逻辑String query = "SELECT * FROM users WHERE id = " + id;// 模拟查询User user = database.query(query); // 数据库直接操作// 业务逻辑if (user == null) {throw new RuntimeException("User not found");}return user;} }
在这个例子中,
UserService
类中直接包含了数据查询的逻辑,没有分层的思想,导致数据访问和业务逻辑耦合在一起。
2. 传统 Web 分层架构
为了应对这些问题,传统的 Web 开发逐步引入了分层架构(Layered Architecture)。经典的 Web 分层架构一般包括以下几层:
- 表现层(Controller 层):负责接收请求、验证数据、调用业务逻辑层,最终将结果返回给用户。
- 业务逻辑层(Service 层):处理核心的业务逻辑,接收来自控制层的请求并做出响应,调用数据层来处理数据。
- 数据访问层(Repository/DAO 层):与数据库或其他存储系统交互,执行具体的增、删、改、查操作。
- 实体层(Entity 层):用于定义实体类,映射数据库中的表。
这个分层模式解决了很多问题,比如代码重复、可维护性差、功能耦合等。但它还是会面临一些性能上的问题,因为每一层之间都需要调用,导致了性能开销,尤其是在大型项目中。
下面是传统 Web 分层架构的一个简单实现:
- 表现层(Controller 层):
@RestController
@RequestMapping("/users")
public class UserController {private final UserService userService;@Autowiredpublic UserController(UserService userService) {this.userService = userService;}@GetMapping("/{id}")public ResponseEntity<User> getUser(@PathVariable Long id) {User user = userService.getUserById(id);return ResponseEntity.ok(user);}
}
- 业务逻辑层(Service 层):
@Service
public class UserService {private final UserRepository userRepository;@Autowiredpublic UserService(UserRepository userRepository) {this.userRepository = userRepository;}public User getUserById(Long id) {return userRepository.findById(id).orElseThrow(() -> new RuntimeException("User not found"));}
}
- 数据访问层(Repository 层):
@Repository
public interface UserRepository extends JpaRepository<User, Long> {
}
好处:
- 各层职责明确,业务逻辑和数据访问相互独立,易于维护。
- 可以通过依赖注入(DI)实现松耦合,使得模块间的依赖关系减少,增强可扩展性和可测试性。
3. Spring Boot 分层架构
Spring Boot 提供了一种更简洁、更灵活的分层架构方案,并且通过框架自动化配置来减少开发者的负担。Spring Boot 在传统分层架构的基础上做了很多优化,包括:
- 自动配置:通过
@SpringBootApplication
注解,Spring Boot 会自动扫描并配置相关的 Bean,减少了大量的手动配置。 - 模块化:Spring Boot 支持将应用划分为多个模块,每个模块处理不同的功能,增加了系统的可扩展性和可维护性。
- 简化配置:Spring Boot 提供了大量的默认配置,减少了手动配置的复杂性,使得开发者可以专注于业务逻辑的实现。
举例:
Spring Boot 的分层架构和传统的 Web 分层架构基本相似,主要区别在于 Spring Boot 的自动化配置和简化的开发流程。通过 Spring Boot,开发者无需手动配置很多内容,系统的构建过程更加简单和高效。
假设我们有一个 Spring Boot 项目,使用 Spring Data JPA 实现数据访问:
- Controller 层(表现层):
@RestController
@RequestMapping("/users")
public class UserController {private final UserService userService;@Autowiredpublic UserController(UserService userService) {this.userService = userService;}@GetMapping("/{id}")public ResponseEntity<User> getUser(@PathVariable Long id) {User user = userService.getUserById(id);return ResponseEntity.ok(user);}
}
- Service 层(业务逻辑层):
@Service
public class UserService {private final UserRepository userRepository;@Autowiredpublic UserService(UserRepository userRepository) {this.userRepository = userRepository;}public User getUserById(Long id) {return userRepository.findById(id).orElseThrow(() -> new RuntimeException("User not found"));}
}
- Repository 层(数据访问层):
@Repository
public interface UserRepository extends JpaRepository<User, Long> {
}
优化与简化:
- 自动配置:通过
@SpringBootApplication
,开发者无需手动配置 Spring 的应用上下文,Spring Boot 自动处理大部分配置。 - 约定优于配置:Spring Boot 提供了大量的默认配置(例如数据库连接、Web 配置),使得开发者可以专注于业务逻辑的实现。
分层架构的好处
- 高内聚,低耦合:每个层次有明确的责任,减少了模块间的依赖,使得代码更加清晰。
- 易于维护:每层职责单一,修改某一层的实现不会影响其他层,降低了修改时的风险。
- 易于扩展:可以独立对某一层进行修改或替换,比如使用不同的数据库或者更换业务逻辑的实现方式。
- 便于测试:分层架构使得单元测试变得容易,可以对每一层进行独立的测试。
实现案例:Spring Boot 的分层架构
1. Controller 层(表现层)
@RestController
@RequestMapping("/users")
public class UserController {private final UserService userService;@Autowiredpublic UserController(UserService userService) {this.userService = userService;}@GetMapping("/{id}")public ResponseEntity<User> getUser(@PathVariable Long id) {User user = userService.getUserById(id);return ResponseEntity.ok(user);}
}
2. Service 层(业务层)
@Service
public class UserService {private final UserRepository userRepository;@Autowiredpublic UserService(UserRepository userRepository) {this.userRepository = userRepository;}public User getUserById(Long id) {return userRepository.findById(id).orElseThrow(() -> new RuntimeException("User not found"));}
}
3. Repository 层(数据访问层)
@Repository
public interface UserRepository extends JpaRepository<User, Long> {
}
依赖注入(DI)与控制反转(IoC)
依赖注入(DI)和控制反转(IoC)是 Spring 框架的核心概念,它们有助于降低系统的耦合度,提高模块间的解耦性,使得系统更具扩展性和可维护性。在深入了解 DI 的概念时,我们要首先理解其背后的控制反转(IoC)思想,以及如何通过 DI 来实现这一思想。
1. 依赖注入(DI)的定义
依赖注入(Dependency Injection,简称 DI) 是一种设计模式,它的核心思想是将对象的依赖关系交给外部容器管理,从而实现松耦合。Spring 通过 DI 模式,在应用启动时自动将所需的依赖注入到类中,而不需要开发者手动去管理这些依赖。这些依赖关系通常通过构造器、setter 方法或字段注入的方式实现。
通过 DI,开发者无需显式创建对象或管理对象之间的依赖关系,而是将所有的对象创建交由 Spring 容器负责。这种方式使得组件之间的依赖关系得以清晰地定义,并且在后期维护和测试时,能够轻松地替换或模拟这些依赖。
2. 依赖注入的原理
依赖注入的原理建立在控制反转(Inversion of Control,简称 IoC)基础上。控制反转(IoC) 是一种设计原则,它的核心思想是将对象的创建、管理和依赖关系控制权从程序员手中反转给外部容器(例如 Spring 容器)。因此,Spring 容器通过 IoC 控制了对象的生命周期、依赖关系、配置以及其他相关信息,而开发者只需要关注核心业务逻辑。
在 Spring 中,IoC 容器负责以下几个方面:
- 对象的创建:通过配置文件或注解配置对象的创建方式,Spring 容器负责实例化和配置所有对象。
- 依赖的注入:Spring 容器通过构造器、Setter 方法或字段注入来自动提供类所需要的依赖对象。
- 对象生命周期管理:Spring 容器负责管理对象的生命周期,例如单例模式、原型模式等。
3. 依赖注入的方式
Spring 提供了多种方式来实现依赖注入,每种方式适用于不同的场景。下面是常见的三种注入方式:
3.1. 构造器注入
构造器注入是最推荐的方式,尤其在依赖关系是不可变的、依赖对象是必须的时非常合适。通过构造器注入,所有的依赖对象都会在对象实例化时被注入,这样可以确保依赖项在对象创建时就被完整地注入,避免了空依赖问题。
优势:
- 保证了对象在创建时即被完全注入,确保了依赖关系的完整性。
- 不容易出现空依赖,因为如果某个依赖没有注入,Spring 会抛出异常,提示开发者缺少必要的依赖。
示例:
@Service
public class UserService {private final UserRepository userRepository;@Autowired // 注解表明构造器注入public UserService(UserRepository userRepository) {this.userRepository = userRepository;}
}
在这个例子中,UserService
的 userRepository
被通过构造器注入。Spring 会自动为构造器注入 UserRepository
的实例。
3.2. Setter 注入
Setter 注入通过 setter 方法注入依赖对象。此方式适用于可选依赖的场景,也就是说,某些依赖对象不是必需的,系统可以正常工作即使这些依赖没有被注入。
优势:
- 更灵活,允许依赖项在对象实例化后被动态修改。
- 可以通过 setter 方法来修改对象的依赖关系,便于配置和修改。
示例:
@Service
public class UserService {private UserRepository userRepository;@Autowired // 注解表明 Setter 注入public void setUserRepository(UserRepository userRepository) {this.userRepository = userRepository;}
}
在这个例子中,UserRepository
的实例通过 setUserRepository
方法注入。Spring 会通过反射调用 setter 方法进行注入。
3.3. 字段注入
字段注入是通过直接在字段上使用 @Autowired
注解来实现依赖注入。虽然这种方式非常简洁,但通常不推荐使用,因为它无法通过构造函数来保证依赖的完整性,并且也不容易进行单元测试。
优势:
- 简洁,代码量少,开发效率高。
缺点:
- 由于没有通过构造器或 setter 方法,依赖关系不够明确,可能导致代码难以理解和维护。
- 不容易进行单元测试,特别是在无法控制依赖注入的情况下。
示例:
@Service
public class UserService {@Autowired // 直接注入字段private UserRepository userRepository;
}
这种方式下,userRepository
通过 Spring 自动注入,无需手动调用构造器或 setter 方法。
4. 注意事项
虽然依赖注入为应用带来了很多优势,但在使用过程中,我们也需要注意以下几个问题:
4.1. 循环依赖
循环依赖 是指两个或多个对象相互依赖,导致 Spring 容器无法正常实例化对象。例如,A
类依赖 B
类,B
类又依赖 A
类,这样会导致一个死循环。
Spring 可以通过三级缓存机制解决循环依赖问题:
- 一级缓存:保存已创建的 Bean 实例。
- 二级缓存:保存 Bean 的代理对象。
- 三级缓存:保存 Bean 的引用,以便能够完成后续的注入。
尽管 Spring 会自动处理循环依赖,但应尽量避免出现复杂的循环依赖,特别是跨层级的依赖循环。
4.2. 空依赖
确保依赖项不为空,特别是当某些依赖是必需的时。如果某个依赖对象为空,将会导致应用运行时出现异常。构造器注入是最安全的方式,因为它强制确保所有必需的依赖项在对象创建时就被注入。
4.3. 选择注入方式
- 构造器注入:推荐在大多数情况下使用,尤其当依赖关系是不可变的且必须提供时。构造器注入有助于在对象创建时就完成依赖注入,避免了空依赖问题。
- Setter 注入:适合可选依赖或需要在对象创建后动态设置的依赖。需要注意的是,如果某些依赖是必需的,使用 setter 注入时应该做好适当的校验。
- 字段注入:虽然这种方式简单,但通常不推荐使用,因为它隐藏了依赖关系,可能导致代码难以理解、测试和维护。
依赖注入(DI)是 Spring 的核心特性,它通过控制反转(IoC)实现对象的自动管理与注入,从而降低了模块之间的耦合度。使用依赖注入能够提高代码的灵活性、可维护性,并简化测试工作。开发者应根据不同的场景和需求选择合适的注入方式,并注意避免循环依赖和空依赖等问题
控制反转(IoC)的原理与实现
控制反转(Inversion of Control,简称 IoC)是 Spring 框架的核心思想之一。IoC 的核心概念是将对象的创建和管理权从程序员手中反转到 Spring 容器。这种设计使得对象之间的依赖关系不再由程序员手动管理,而是由框架统一处理,从而实现松耦合和模块化。
1. 控制反转的定义
控制反转(Inversion of Control, IoC) 是一种设计原则,它的核心思想是将对象的创建、配置、依赖管理等控制权交给外部容器,而不是让程序员直接控制。这种方式使得系统的各个模块更加独立,且系统间的依赖关系更加清晰。在 Spring 框架中,IoC 容器负责管理对象的生命周期、依赖关系和配置,从而实现了控制反转。
在传统的编程中,开发者通常自己实例化和管理对象,而在 IoC 模式下,Spring 容器负责根据配置文件或注解自动实例化、配置和管理对象。程序员只需专注于业务逻辑的实现,Spring 容器则负责管理对象的创建和生命周期。
2. 控制反转的实现
Spring 提供了多种方式来实现 IoC,其中最常见的两种方法是通过注解和 XML 配置来定义和管理 Bean(对象):
2.1. 通过注解实现 IoC
Spring 通过一系列的注解使得对象的声明、创建和管理变得非常简洁和自动化。开发者只需要使用特定的注解标记类,Spring 容器就会自动将这些类作为 Bean 加入到容器中进行管理。常见的注解有:
@Component
:用于将一个普通的 Java 类标记为 Spring 管理的 Bean。@Service
:通常用于标记服务层的 Bean,继承自@Component
。@Repository
:用于标记数据访问层的 Bean,继承自@Component
。@Controller
:用于标记控制器类,继承自@Component
,通常用于 Spring MVC 中。
例如,使用 @Service
注解来标记一个服务类:
@Service
public class UserService {public void addUser() {// 添加用户的业务逻辑}
}
在这个例子中,UserService
类被 @Service
注解标记为一个 Spring Bean。Spring 会自动检测这个注解并将该类实例化并加入到 IoC 容器中,开发者无需手动管理对象的创建和生命周期。
2.2. 通过 XML 配置实现 IoC
在 Spring 框架的早期版本中,IoC 容器的配置主要是通过 XML 文件来实现的。开发者可以在 XML 配置文件中显式声明 Bean,以及它们的依赖关系。这种方式较为冗长,但在大型系统中提供了更好的控制和灵活性。
例如,在 XML 配置文件中声明一个 UserService
Bean:
<bean id="userService" class="com.example.UserService"/>
这种方式明确指出了 Spring 容器中需要管理的 UserService
类,并且通过 <bean>
标签配置了类的全限定名。容器将会根据这个配置自动创建 UserService
的实例,并管理其生命周期。
2.3. 自动装配(Autowired)
无论是使用注解方式还是 XML 配置方式,Spring 都提供了 自动装配(Autowiring)的机制来自动注入依赖。自动装配允许 Spring 容器根据配置自动为 Bean 注入其依赖的其他 Bean,减少了显式定义依赖的复杂性。
自动装配常通过 @Autowired
注解来实现。这个注解可以用在构造器、Setter 方法或字段上,Spring 会自动查找和注入符合要求的 Bean 实例。
构造器注入:
@Service
public class UserService {private final UserRepository userRepository;@Autowiredpublic UserService(UserRepository userRepository) {this.userRepository = userRepository;}
}
在这个例子中,UserService
依赖于 UserRepository
,Spring 会自动注入 UserRepository
实例。
字段注入:
@Service
public class UserService {@Autowiredprivate UserRepository userRepository;
}
在字段注入的方式下,userRepository
字段会自动注入一个合适的 UserRepository
实例。
2.4 常见的问题
在 Spring 的自动装配机制中,虽然可以大大简化依赖注入的过程,但也可能遇到一些常见的问题,如依赖冲突和多个 Bean 注入相同的依赖。为了处理这些问题,Spring 提供了几种方式来解决这些冲突和确保正确的依赖注入。以下是一些常见问题及其解决方案:
1. 依赖冲突:多个 Bean 注入相同的依赖
如果容器中存在多个同类型的 Bean,而 Spring 无法确定注入哪个 Bean,就会出现依赖冲突。这时,可以通过以下几种方式来解决:
a) 使用 @Qualifier
注解指定 Bean 名称
@Qualifier
注解可以与 @Autowired
配合使用,指定要注入的 Bean 名称。当容器中有多个符合条件的 Bean 时,Spring 会根据 @Qualifier
中的值来选择注入特定的 Bean。
@Service
public class UserService {private final UserRepository userRepository;@Autowiredpublic UserService(@Qualifier("userRepositoryImpl") UserRepository userRepository) {this.userRepository = userRepository;}
}
在这个例子中,如果容器中存在多个 UserRepository
的实现,@Qualifier
注解确保了 userRepositoryImpl
被正确注入。
b) 使用 Bean 名称作为注解参数
如果通过 XML 配置的方式,可以为 Bean 配置一个唯一的名称,使用该名称来指定注入的 Bean。
<bean id="userRepositoryImpl" class="com.example.UserRepositoryImpl" />
<bean id="userService" class="com.example.UserService"><constructor-arg ref="userRepositoryImpl" />
</bean>
c) 使用 @Primary
注解
@Primary
注解可用来标记一个 Bean 作为首选 Bean。当多个符合条件的 Bean 存在时,Spring 会选择标记为 @Primary
的 Bean 进行注入。
@Service
@Primary
public class UserRepositoryImpl implements UserRepository {// 实现代码
}
如果没有 @Qualifier
或其他明确指定的 Bean,Spring 将自动选择带有 @Primary
注解的 Bean 进行注入。
2.5 . 构造器注入 vs. 字段注入
虽然构造器注入和字段注入都能实现自动装配,但它们各有优缺点。通常推荐使用构造器注入,因为它有以下优点:
- 不可变性:构造器注入使得依赖变成不可变的,一旦 Bean 被创建,依赖就被固定了,这有助于减少潜在的错误。
- 可测试性:构造器注入使得依赖更容易进行单元测试。由于所有依赖都显式地通过构造器传入,它们很容易被模拟或替换。
- 更清晰的依赖关系:构造器注入使依赖关系显式化,而字段注入可能让依赖关系不清晰。
字段注入的缺点是容易忽略依赖关系,并且无法保证 Bean 在创建时已完全初始化。
2.6. 注入空值 (Null) 或 Bean 未初始化
如果你尝试自动装配的 Bean 为 null
或没有被正确初始化,可能会导致 NullPointerException
或其他错误。常见的解决方案包括:
a) 确保 Bean 已经声明并被容器管理
通过 @Component
, @Service
, @Repository
等注解确保类被 Spring 容器管理,或者在 XML 配置文件中正确声明 Bean。
b) 使用 @Autowired(required = false)
如果某个 Bean 可能不存在,可以使用 @Autowired
的 required
属性来设置是否必须注入该 Bean。如果为 false
,则该 Bean 可以为空。
@Autowired(required = false)
private UserRepository userRepository;
这意味着如果 Spring 容器中没有找到符合条件的 UserRepository
,userRepository
就会保持为 null
,不会抛出异常。
2.7. 多个依赖存在时选择自动装配的策略
如果存在多个 Bean 可以满足自动装配条件(例如,有多个实现了同一接口的 Bean),而没有使用 @Qualifier
或 @Primary
来区分,Spring 容器会抛出异常。为了避免这种情况,确保有明确的选择方式(如使用 @Qualifier
或 @Primary
)。
2.8. 循环依赖问题
Spring 自动装配可能会引发循环依赖问题。例如,A
依赖于 B
,而 B
又依赖于 A
。Spring 通过构造器注入时会检测到这种循环,导致应用无法启动。为了解决这个问题:
a) 使用 Setter 注入解决循环依赖
Setter 注入在 Spring 中比构造器注入支持循环依赖,因为在 Bean 创建时,Spring 会首先实例化 Bean,再通过 Setter 方法注入依赖。
@Service
public class UserService {private UserRepository userRepository;@Autowiredpublic void setUserRepository(UserRepository userRepository) {this.userRepository = userRepository;}
}
Spring 的自动装配机制是一个强大的特性,能够自动将 Bean 注入到需要的地方,减少了手动配置的复杂性。然而,在使用过程中,可能会遇到多个 Bean 的冲突、循环依赖等问题。通过合理使用
@Autowired
,@Qualifier
,@Primary
等注解,结合构造器注入或 Setter 注入的选择,可以有效地解决这些问题,提高代码的可维护性和可测试性。
3. 控制反转的优点(详解及扩展)
控制反转(IoC)作为 Spring 框架的核心思想,极大地优化了传统 Java 开发中的代码结构和模块设计。以下对 IoC 的主要优点进行逐一阐述,并通过详实的例子和场景解读其具体意义和实际效果。
3.1. 松耦合
概念解析
在软件设计中,松耦合是一种降低模块之间依赖性的重要设计原则。传统编程方式中,模块通过直接实例化依赖对象的方式完成功能。这种方式的问题在于,当需要替换依赖对象或增加新功能时,通常需要修改调用方代码,从而导致耦合度过高,降低了系统的可维护性和扩展性。
IoC 的出现改变了这一状况。通过依赖注入(Dependency Injection, DI)的方式,Spring 容器负责动态地为模块注入所需的依赖,而调用方只需要面向接口编程,不关注具体实现类。
详尽示例
场景说明
我们构建一个支付系统,支持不同支付方式(例如支付宝、微信支付、银行卡支付),要求能够根据用户选择动态切换支付方式,并且未来可能需要增加新支付方式(例如 Apple Pay)。
传统实现方式(高耦合)
传统编程方式中,支付服务(OrderService
)需要明确地实例化具体支付类:
public class OrderService {private AlipayService alipayService = new AlipayService();public void processPayment() {alipayService.pay();}
}
- 问题:
- 当需要切换到微信支付时,必须修改
OrderService
类,代码侵入性强。 - 新增支付方式(例如 Apple Pay)时,
OrderService
类需要增加逻辑,违反了开闭原则。
- 当需要切换到微信支付时,必须修改
使用 IoC 实现松耦合
通过接口编程和 IoC 容器管理实现松耦合:
-
定义支付接口:
public interface PaymentService {void pay(); }
-
实现具体支付方式:
public class AlipayService implements PaymentService {@Overridepublic void pay() {System.out.println("通过支付宝支付");} }public class WeChatPayService implements PaymentService {@Overridepublic void pay() {System.out.println("通过微信支付");} }
-
注入依赖:
public class OrderService {private PaymentService paymentService;public OrderService(PaymentService paymentService) {this.paymentService = paymentService;}public void processPayment() {paymentService.pay();} }
-
Spring 配置:
<bean id="paymentService" class="com.example.AlipayService"/> <bean id="orderService" class="com.example.OrderService"><constructor-arg ref="paymentService"/> </bean>
-
替换支付方式:
修改配置即可切换到微信支付:
<bean id="paymentService" class="com.example.WeChatPayService"/>
优势分析
- 解耦:
OrderService
仅依赖于接口,支付方式的具体实现由 IoC 容器管理。 - 易扩展:新增支付方式时,只需实现
PaymentService
接口,代码完全无需修改。
3.2. 集中管理
概念解析
Spring 容器通过 IoC,将对象的创建、配置、生命周期管理统一起来,开发者不再需要手动编写冗长的实例化代码,也不必在多个类中分散管理依赖关系。所有的依赖和配置都可以通过注解或配置文件进行集中管理,极大地简化了项目的维护和升级。
详尽示例
场景说明
一个用户服务(UserService
)需要依赖用户数据访问层(UserRepository
)。传统方式中,这些依赖的初始化通常散落在多个代码文件中。
传统实现方式(分散管理)
public class App {public static void main(String[] args) {UserRepository repository = new UserRepository();UserService userService = new UserService(repository);userService.processUser();}
}
- 问题:
- 每次使用
UserService
都需要手动实例化UserRepository
。 - 如果
UserRepository
的依赖链条复杂,会导致初始化代码难以维护。
- 每次使用
使用 Spring 集中管理
-
定义依赖:
@Component public class UserRepository {// 数据操作逻辑 }@Component public class UserService {private final UserRepository repository;@Autowiredpublic UserService(UserRepository repository) {this.repository = repository;} }
-
启用自动扫描和集中管理:
<context:component-scan base-package="com.example"/>
-
使用
ApplicationContext
获取UserService:
ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml"); UserService userService = context.getBean(UserService.class); userService.processUser();
优势分析
- 所有依赖关系集中在 Spring 配置或注解中,避免手动创建对象。
- 修改依赖关系时,只需修改配置,无需修改业务逻辑。
3.3. 提高可扩展性
概念解析
使用 IoC 后,系统的依赖关系通过接口定义,具体实现由容器管理。因此,开发者可以轻松替换实现类或新增模块,而无需修改调用代码。这种设计极大提高了系统的灵活性和扩展性。
详尽示例
场景说明
假设一个日志系统,初始版本只支持控制台日志,后续需要增加文件日志或远程日志。
实现步骤
-
定义日志接口:
public interface Logger {void log(String message); }
-
实现多种日志方式:
public class ConsoleLogger implements Logger {@Overridepublic void log(String message) {System.out.println("Console log: " + message);} }public class FileLogger implements Logger {@Overridepublic void log(String message) {// 写入文件逻辑System.out.println("File log: " + message);} }
-
使用 Spring 配置切换日志实现:
<bean id="logger" class="com.example.ConsoleLogger"/>
替换为文件日志:
<bean id="logger" class="com.example.FileLogger"/>
优势分析
- 新增日志方式时,只需实现接口,无需修改原有代码。
- 容器通过配置注入不同实现,动态满足不同需求。
3.4. 便于单元测试
概念解析
IoC 允许开发者在测试时替换真实依赖为 Mock 对象,避免外部依赖(如数据库、API 等)对测试的影响,简化单元测试的编写。
详尽示例
场景说明
一个 UserService
类依赖 UserRepository
,需要通过 ID 查询用户信息。
测试代码
-
定义UserService:
public class UserService {private UserRepository repository;public UserService(UserRepository repository) {this.repository = repository;}public String getUserName(int id) {return repository.findById(id).getName();} }
-
使用 Mock 对象进行测试:
@Test public void testGetUserName() {// 创建 Mock 对象UserRepository mockRepository = mock(UserRepository.class);when(mockRepository.findById(1)).thenReturn(new User(1, "MockUser"));// 注入 Mock 对象UserService userService = new UserService(mockRepository);// 验证assertEquals("MockUser", userService.getUserName(1)); }
优势分析
- 测试独立性强:无需数据库或外部依赖。
- 快速定位问题,测试运行速度快。
总结
控制反转(IoC)是 Spring 框架的核心概念之一,它通过将对象的创建和管理交给容器来实现系统组件之间的松耦合。通过 IoC,开发者可以专注于业务逻辑的实现,而将对象的管理和依赖关系交给 Spring 容器。Spring 提供了注解和 XML 配置两种方式来实现 IoC,帮助开发者更加灵活和高效地构建应用。控制反转不仅提升了系统的可维护性、可扩展性,还方便了单元测试和模块的独立开发。
如果这篇文章对您有帮助,就点个关注呗 😄👍
相关文章:
SpringBoot 分层解耦
从没有分层思想到传统 Web 分层,再到 Spring Boot 分层架构 1. 没有分层思想 在最初的项目开发中,很多开发者并没有明确的分层思想,所有逻辑都堆砌在一个类或一个方法中。这样的开发方式通常会导致以下问题: 代码混乱࿱…...
不一样的CSS(4)--icon图标系列之svg
序言 上一节内容我们讲解了如何利用css去画一个五角星,其中包括了使用svg的方法,有些小伙伴们对svg的使用不是很了解,那么本节内容我们主要来讲一下,关于svg标签的的使用。 目录 序言一、svg的介绍二、安装SVG扩展插件三、SVG基…...
Go-知识依赖管理2
Go-知识依赖管理2 1. go.sum1.1 go.sum 文件记录1.2 生成1.3 校验1.4 校验和数据库2. 模块代理2.1 GOPROXY 介绍2.2 代理协议2.2.1 获取模块列表2.2.2 获取模块元素数据2.2.3 获取 go.mod 文件2.2.4 获取代码压缩包2.2.5 获取模块的最新可用版本2.2.6 下载过程2.3 观察下载步骤…...
el-select的搜索功能
el-select的相关信息: 最基本信息 v-model的值为当前被选中的el-option的 value 属性值 :label是选择器可以看到的内容 过滤搜索 普通过滤搜索 <el-selectv-model"selectedCountry"placeholder"请选择国家"filterable:loading"lo…...
批量将不同的工作簿合并到同一个Excel文件
批量将不同的工作簿合并到同一个Excel文件 下面是一个示例,展示如何批量将不同的工作簿合并到同一个Excel文件,并生成模拟数据。我们将使用 Python 的 pandas 库来完成这个任务。具体步骤如下: 步骤 1: 安装必要的库 首先确保你已安装 pan…...
git遇见冲突怎么解决?
问: 回答:...
Spring和SpringBoot的关系和区别?
大家好,我是锋哥。今天分享关于【Spring和SpringBoot的关系和区别?】面试题。希望对大家有帮助; Spring和SpringBoot的关系和区别? 1000道 互联网大厂Java工程师 精选面试题-Java资源分享网 Spring和Spring Boot是两种相关但有所…...
python学习——字符串的编码和解码
在Python中,字符串的编码和解码是处理文本数据时非常重要的概念。以下是对字符串编码和解码的详细解释: 字符串编码 字符串编码是将字符串转换成字节序列的过程。Python中的字符串是Unicode编码的,所以在将字符串转换成字节序列时ÿ…...
Web游戏开发指南:在 Phaser.js 中读取和管理游戏手柄输入
前言 Phaser.js 是一个广受欢迎的 HTML5 游戏框架,为开发者提供了创建跨平台 2D 游戏的强大工具。在现代游戏开发中,支持游戏手柄已成为提升玩家体验的重要方面。本文将详细介绍如何在 Phaser.js 中监听和处理游戏手柄的输入,帮助开发者为他…...
HTML5系列(13)-- 微数据与结构化数据指南
前端技术探索系列:HTML5 微数据与结构化数据指南 📊 致读者:探索数据语义化的世界 👋 前端开发者们, 今天我们将深入探讨 HTML5 微数据与结构化数据,学习如何让网页内容更易被搜索引擎理解和解析。 微数…...
MAA ADB问题
模拟器官方MUMU12 连接设置 | MaaAssistantArknights 参考文档,找谷歌platform ADB,放入MAA文件夹 选择谷歌ADB,选择MUMU12的ADB代码 MuMu 模拟器 12 127.0.0.1:16384 重新连接 ok...
基于VTX356语音识别合成芯片的智能语音交互闹钟方案
一、方案概述 本方案旨在利用VTX356语音识别合成芯片强大的语音处理能力,结合蓝牙功能、APP或小程序,打造一款功能全面且智能化程度高的闹钟产品。除了基本的时钟显示和闹钟提醒功能外,还拥有正计时、倒计时、日程安排、重要日提醒以及番茄钟…...
大语言模型应用开发框架LangChain
大语言模型应用开发框架LangChain 一、LangChain项目介绍1、简介2、LangChain的价值3、实战演练 二、LangChain提示词大语言模型应用1、简介1.1、提示词模板化的优点1.2、提示词模板LLM 的应用1.3、Prompt 2、应用实战2.1、PromptTemplate LLM2.2、PromptTemplate LLM Outpu…...
php7.4安装pg扩展-contos7
今天接到一个需求,就是需要用thinkphp6链接pg(postgresql)数据库。废话不多说,直接上操作步骤 一、安装依赖 yum install -y sqlite-devel libxml2 libxml2-devel openssl openssl-devel bzip2 bzip2-devel libcurl libcurl-devel libjpeg libjpeg-dev…...
【开源】A064—基于JAVA的民族婚纱预定系统的设计与实现
🙊作者简介:在校研究生,拥有计算机专业的研究生开发团队,分享技术代码帮助学生学习,独立完成自己的网站项目。 代码可以查看项目链接获取⬇️,记得注明来意哦~🌹 赠送计算机毕业设计600个选题ex…...
网络安全防护指南:筑牢网络安全防线(5/10)
一、网络安全的基本概念 (一)网络的定义 网络是指由计算机或者其他信息终端及相关设备组成的按照一定的规则和程序对信息收集、存储、传输、交换、处理的系统。在当今数字化时代,网络已经成为人们生活和工作中不可或缺的一部分。它连接了世…...
集合框架(2)List
Collection的子接口:List、Set 1、List接口 鉴于Java中数组用来存储数据的局限性,我们通常使用java.util.List替代数组List集合类中元素有序、且可重复,集合中的每个元素都有其对应的顺序索引。JDK API中List接口的实现类常用的有ÿ…...
12.5作业
1.完成指针的练习 1.已知数组a[10]和b[10]中元素的值递增有序,用指针实现将两个数组中的元素按递增的顺序输出。 ex: int arr[5]{1,3,5,7,9}; int arr1[5]{2,4,6,8,10}; 程序结束后输出1,2,3,4,5,6&am…...
61 基于单片机的小车雷达避障及阈值可调
所有仿真详情导航: PROTEUS专栏说明-CSDN博客 目录 一、主要功能 二、硬件资源 三、主程序编程 四、资源下载 一、主要功能 基于51单片机,采用超声波传感器检测距离,通过LCD1602显示屏显示,三个按键,第一个按键是…...
116. UE5 GAS RPG 实现击杀掉落战利品功能
这一篇,我们实现敌人被击败后,掉落战利品的功能。首先,我们将创建一个新的结构体,用于定义掉落体的内容,方便我们设置掉落物。然后,我们实现敌人死亡时的掉落函数,并在蓝图里实现对应的逻辑&…...
原子类相关
原子引用 JUC 并发包提供了: AtomicReferenceAtomicMarkableReferenceAtomicStampedReference AtomicReference 使用举例 public interface DecimalAccount {// 获取余额BigDecimal getBalance();// 取款void withdraw(BigDecimal amount);/*** 方法内会启动 10…...
DeCoOp: Robust Prompt Tuning with Out-of-Distribution Detection
文章汇总 me:看得很迷糊 新型检测器 M D \mathcal M_D MD的训练是为了对一个子基类去划分子基类中的base和new。 在获得每个子基类之后,为每个检测器训练子分类器 M C \mathcal M_C MC 在推理时,如果最高得分的检测器 M D i ( x ) \ma…...
Tinker热修复框架详解:Android应用补丁生成,提升应用稳定性
Tinker 是腾讯开源的Android热修复框架,通过动态更新和修复应用中的代码、资源和本地库文件,无需用户重新安装 APK,便可以及时修复应用中的 bug,优化用户体验。 下面是Tinker在Android项目中的详细用法,结合Kotlin 代…...
手写—— netty 实现 rabbitMq客户端
要使用 Netty 实现一个 RabbitMQ 客户端,你可以将 RabbitMQ 协议封装在 Netty 中,通过自定义编码和解码来实现与 RabbitMQ 的通信。RabbitMQ 使用 AMQP (Advanced Message Queuing Protocol) 协议,因此我们需要创建合适的协议封装和处理逻辑。…...
调用高德地图天气查询api
之前使用的api一直用不了,才发现web端类型的没有天气查询功能 web服务才有 然后在linux的环境变量中配置一下 发现linux中配的环境变量不行,于是给输入amap_weather给的字典明文token。 # 选用RolePlay 配置agent from modelscope_agent.agents.role_p…...
【Vulkan入门】03-创建Device
目录 先叨叨git信息关键代码VulkanEnv::CreateDevice() 编译并运行程序题外话 先叨叨 在上篇已经选择了一个合适的PhysicalDevice。 本篇要为这个PhysicalDevice创将一个Device。Device可以理解为APP与PhysicalDevice之间的代理。 所有APP与PhysicalDevice之间交互的资源都通过…...
【Axios】如何在Vue中使用Axios请求拦截器
✨✨ 欢迎大家来到景天科技苑✨✨ 🎈🎈 养成好习惯,先赞后看哦~🎈🎈 🏆 作者简介:景天科技苑 🏆《头衔》:大厂架构师,华为云开发者社区专家博主,…...
query did not return a unique result: 2;
文章目录 错误原因分析关键位置可能原因解决方法1. 检查数据库数据2. 修改查询方法3. 限定查询返回唯一结果4. 检查业务逻辑 总结 1、LoginLogRepository2、LoginLogService3、LoginLogApiService4、MyAuthenticationSuccessHandler 微信小程序开发者工具控制台报错 {"tim…...
PHP升级
PHP升级CentOs8 wget http://rpms.famillecollet.com/enterprise/remi-release-8.rpm rpm -ivh remi-release-8.rpm --nodeps --force rpm -qa | grep remi dnf module list php dnf module enable php:remi-7.4首先,重置当前的 PHP 模块,以便清理所有已…...
C++设计模式(原型、代理、适配器、组合)
一、原型模式 1.定义 用原型实例指定创建对象的种类,并且通过拷贝这些原型创建新的对象。 原型模式允许通过复制现有的对象来创建新对象,而不是通过实例化类来创建。这种方式可以避免创建重复的对象,从而提高性能和降低内存消耗。 2.组成 …...
超详细搭建PhpStorm+PhpStudy开发环境
刚开始接触PHP开发,搭建开发环境是第一步,网上下载PhpStorm和PhpStudy软件,怎样安装和激活就不详细说了,我们重点来看一看怎样搭配这两个开发环境。 前提:现在假设你已经安装完PhpStorm和PhpStudy软件。 我的PhpStor…...
Axure RP在智慧农场可视化大屏系统设计中的应用
随着科技的飞速发展,智慧农业已成为现代农业的重要发展方向。智慧农场可视化大屏系统作为智慧农业的重要组成部分,正逐步成为农场管理、决策和展示的核心工具。Axure RP,作为一款强大的原型设计工具,其在智慧农场可视化大屏系统的…...
《嵌入式硬件设计》
一、引言 嵌入式系统在现代科技中占据着至关重要的地位,广泛应用于消费电子、工业控制、汽车电子、医疗设备等众多领域。嵌入式硬件设计作为嵌入式系统开发的基础,直接决定了系统的性能、可靠性和成本。本文将深入探讨嵌入式硬件设计的各个方面ÿ…...
【C语言篇】C 语言总复习(上):点亮编程思维,穿越代码的浩瀚星河
我的个人主页 我的专栏:C语言,希望能帮助到大家!!!点赞❤ 收藏❤ 在计算机科学的广袤宇宙中,C语言犹如一颗璀璨的恒星,散发着持久而耀眼的光芒。它作为一种基础且强大的编程语言,承载…...
多线程——04
本节目标 1. wait 和 notify 方法 2. 代码案例 1. wait 和 notify 方法 1. 方法使用 多个线程的执行顺序本身是随机的(抢占式执行) wait —— 让指定线程进入阻塞状态 notify —— 唤醒对应的阻塞状态的线程 注意: wait, notify, notifyAl…...
使用ECS和OSS搭建个人网盘
在linux服务器 一、下载cloudreve安装包。 执行如下命令,下载cloudreve安装包。 wget https://labfileapp.oss-cn-hangzhou.aliyuncs.com/cloudreve_3.3.1_linux_amd64.tar.gz 下载完毕后,执行如下命令,解压cloudreve安装包。 tar -zxvf c…...
Android 单元测试断言校验方法 org.junit.Assert
判断布尔值 assertTrue assertFalse 判断对象非空 assertNull(object); 案例: PersistableBundle result Util.getCarrierConfig(mockContext, subId);assertNull(result); 判断是否相等 assertEquals("mocked_string", result.toString()); package or…...
SpringSecurity学习
介绍 SpringSecurity是一个作用于身份认证和权限控制的框架,其针对的主要就是网站的安全问题 页面代码 要使用SpringSecurity的前提是有一个可以正常访问业务逻辑的代码,再使用SpringSecurity实现权限控制和身份验证。 后端代码 package com.learn.…...
Eureka和Zookeeper、Nacos的区别
目录 一、Eureka与Zookeeper的区别 适用场景: 架构设计: 功能特性: 社区生态: 二、Eureka与Nacos的区别 接口方式: 实例类型: 健康检测: 服务发现: 一致性与可用性&#…...
基于gitlab API刷新MR的commit的指定status
场景介绍 自己部署的gitlab Jenkins,并已经设置好联动(如何设置可以在网上很容易搜到)每个MergeRequest都可以触发多个Jenkins pipeline,pipeline结束后会将状态更新到gitlab这个MR上希望可以跳过pipeline运行,直接将指定的MR的指定pipeline状态刷新为…...
SpringBoot | 拦截器 | 统一数据返回格式 | 统一异常处理 | 适配器模式
拦截器 拦截器是Spring框架提供的核心功能之一, 主要用来拦截用户的请求, 在指定方法前后, 根据业务需要执行预先设定的代码. 也就是说, 允许开发人员提前预定义一些逻辑, 在用户的请求响应前后执行. 也可以在用户请求前阻止其执行. 在拦截器当中,开发人员可以在…...
Oracle清除水位
– 清除水位 ALTER TABLE 数据库名.表名 ENABLE ROW MOVEMENT; ALTER TABLE 数据库名.表名 SHRINK SPACE CASCADE; ALTER TABLE 数据库名.表名 DISABLE ROW MOVEMENT; – 回收统计信息 BEGIN DBMS_STATS.GATHER_TABLE_STATS(OWNNAME > ‘数据库名’, TABNAME > ‘表名’…...
软件工程——期末复习(2)
Part1:软件工程基本概念 软件程序文档数据 在软件工程中,软件通常被定为程序、文档和数据的集合。程序是按事先设计的功能和性能要求编写的指令序列;程序是完成指定功能的一段特定语言代码。文档是描述程序操作和使用的文档,是与…...
RAID1技术是什么?它的发展和工作原理如何?
RIAD1是一种先进的数据存储与冗余技术,设计用于解决现代分布式系统中常见的数据安全、数据一致性和高可用性等问题。随着云计算和大规模分布式存储系统的兴起,如何保障数据在高效传输与存储过程中仍然能具备足够的安全性和可靠性,成为了各大企…...
【Apache Paimon】-- 8 -- flink 创建 paimon connecor 的两种方式
目录 1、使用 catalog 创建非临时表 2、使用 with 创建 temporary 表 3、对比 4、参考 1、使用 catalog 创建非临时表 CREATE CATALOG my_catalog WITH (type = paimon,warehouse = hdfs:///path/to/warehouse );USE CATALOG my_catalog; CREATE TABLE `<your-paimon-…...
js进阶-关于运算符++
一、运算符与表达式 运算符按参与的运算单元数量分为:一元运算符、二元运算符和三元运算符;表达式是运算单元和运算符一起构成的;每个表达式都有一个运算后的返回值。 二、关于运算符 1.概述 运算符分为两部分,第一部分是返回运…...
三维地图,智慧城市,商业智能BI,数据可视化大屏(Cesiumjs/UE)
绘图工具 三维地图:Cesiumjs 建模方式:激光点云建模、航拍倾斜摄影建模、GIS建模、BIM建模、手工建模 建模工具:C4D Blender GeoBuilding ArcGIS Cesiumjs <!DOCTYPE html> <html lang"en"> <head><meta …...
通过EPEL 仓库,在 CentOS 7 上安装 OpenResty
通过EPEL 仓库,在 CentOS 7 上安装 OpenResty 通过EPEL 仓库,在 CentOS 7 上安装 OpenResty步骤 1: 安装 EPEL 仓库步骤 2: 安装 OpenResty步骤 3: 启动 OpenResty步骤 4: 设置开机自启步骤 5: 验证安装说明 通过EPEL 仓库,在 CentOS 7 上安装…...
每日一题 LCR 054. 把二叉搜索树转换为累加树
LCR 054. 把二叉搜索树转换为累加树 使用后序遍历即可 class Solution { public:TreeNode* convertBST(TreeNode* root) {int temp 0;dfs(root,temp);return root;}void dfs(TreeNode* root,int &temp){if(!root){return ;}dfs(root->right,temp);temp root->val;…...
【贪心算法】贪心算法五
贪心算法五 1.跳跃游戏 II2.跳跃游戏3.加油站3.单调递增的数字 点赞👍👍收藏🌟🌟关注💖💖 你的支持是对我最大的鼓励,我们一起努力吧!😃😃 1.跳跃游戏 II 题目链接&…...