开发基础知识 - Spring基本介绍,特性与原理
Background
单例模式
单例模式是一种创建对象的设计模式,它确保一个类在整个程序运行过程中,只会被创建一个实例,并提供一个全局访问点。比如一个Printer类,这个类的功能可以被多个服务使用,因为printer实例不保存状态,只提供打印服务,我们不想每次使用时就创建一个新的printer实例,因为这样浪费内存,也容易管理混乱。
示例单例模式的代码:
public class Printer {
private static Printer instance; // 1. 类的静态变量保存实例
private Printer() {
// 2. 私有构造函数,别人不能 new
}
public static Printer getInstance() {
if (instance == null) { // 3. 第一次调用时才创建
instance = new Printer();
}
return instance;
}
}
Printer p = Printer.getInstance(); // 得到单例
但是以上代码不符合单例模式的需求:
- 多线程环境下可能会出现两个线程同时判断
instance == null为 true, 创建两个实例,违反单例模式的设定。 instance = new Printer()可能会被编译器重排成如下顺序:- 分配内存空间
- 把内存地址赋给 instance(instance 不再为 null)
- 调用构造函数初始化对象
如果第二个线程看到instance != null,但其实这个实例还没有被初始化,那么第二个线程会直接使用没有初始化的对象,导致空指针等问题。因此要使用volatile防止指令重排。
正确的单例模式代码:
public class Printer {
// 确保线程可见性以及禁止指令重排
private static volatile Printer instance;
// private构造函数,防止外部实例化(防止外部使用new Printer())
private Printer() {}
public static Printer getInstance() {
if (instance == null) { // 第一次检查,如果 instance 已经有了,就直接返回,省去加锁的成本
synchronized (Printer.class) { // 确保同一时间只有一个线程能进入
if (instance == null) { // 第二次检查(可能两个线程同时通过了第一层的 if (instance == null))
instance = new Printer();
}
}
}
return instance;
}
}
Dependency Injection
依赖注入是由外部系统将对象所依赖的其他对象传入(注入)它的过程。
如果不使用依赖注入,比如我的代码中,Car 依赖于具体的 Engine 实现,两者耦合度高:
// Engine.java
public class Engine {
public void start() {
System.out.println("Engine started.");
}
}
// Car.java
public class Car {
private Engine engine = new Engine(); // 手动创建依赖
public void drive() {
engine.start();
System.out.println("Car is driving...");
}
}
// Main.java
public class Main {
public static void main(String[] args) {
Car car = new Car(); // 手动创建 Car 实例
car.drive();
}
}
在这个代码中,Car 类自己创建了 Engine 对象 new Engine()。
- 如果要换成 TurboEngine,需要修改 Car 的源码。
- 不方便测试(比如不能用一个假的 Engine)
使用依赖注入:
public interface Engine {
void start();
}
// 实现不同类型的Engine
public class GasEngine implements Engine {
public void start() {
System.out.println("Gas engine started.");
}
}
public class ElectricEngine implements Engine {
public void start() {
System.out.println("Electric engine started.");
}
}
// Car 接受一个 Engine 参数
public class Car {
private Engine engine;
// 构造函数注入(最常见方式)
public Car(Engine engine) {
this.engine = engine;
}
public void start() {
engine.start();
System.out.println("Car is driving...");
}
}
// 使用
public class Main {
public static void main(String[] args) {
// 注入不同的引擎类型
Engine gasEngine = new GasEngine();
Car car1 = new Car(gasEngine);
car1.start(); // 输出:Gas engine started.
Engine electricEngine = new ElectricEngine();
Car car2 = new Car(electricEngine);
car2.start(); // 输出:Electric engine started.
}
}
Spring Framework
Spring 框架的核心目标就是解耦和自动化。Spring负责类的创建、注入、销毁等。
Spring代码示例
代码结构
spring-demo/
├── src/
│ └── main/
│ ├── java/
│ │ └── com/example/demo/
│ │ ├── DemoApplication.java // 主程序入口
│ │ ├── controller/UserController.java// 控制器
│ │ ├── service/UserService.java // 业务逻辑
│ │ ├── model/User.java // 数据模型
│ │ └── repository/UserRepository.java// 数据仓库
│ └── resources/
│ └── application.properties // 配置文件
├── pom.xml // Maven 构建文件
- DemoApplication.java:应用入口
package com.example.demo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}
- User.java:实体类
package com.example.demo.model;
import jakarta.persistence.*;
@Entity
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private String email;
// Getter 和 Setter
}
- UserRepository.java:仓库接口
package com.example.demo.repository;
import com.example.demo.model.User;
import org.springframework.data.jpa.repository.JpaRepository;
public interface UserRepository extends JpaRepository<User, Long> {
}
- UserService.java:服务类(业务逻辑)
package com.example.demo.service;
import com.example.demo.model.User;
import com.example.demo.repository.UserRepository;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class UserService {
private final UserRepository repo;
@Autowired
public UserService(UserRepository repo) {
this.repo = repo;
}
public List<User> getAllUsers() {
return repo.findAll();
}
public User createUser(User user) {
return repo.save(user);
}
}
- UserController.java:控制器(REST 接口)
package com.example.demo.controller;
import com.example.demo.model.User;
import com.example.demo.service.UserService;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/users")
public class UserController {
private final UserService service;
@Autowired
public UserController(UserService service) {
this.service = service;
}
@GetMapping
public List<User> getUsers() {
return service.getAllUsers();
}
@PostMapping
public User addUser(@RequestBody User user) {
return service.createUser(user);
}
}
如果没有Spring
UserService service = new UserService(new UserRepository());
- 手动创建对象,项目初始化的时候很复杂
- 手动传依赖(UserService依赖UserRepository)
- 管理对象的销毁等
- 如果多个类依赖UserRepository,依赖关系复杂难以管理
会导致代码耦合度高,测试困难等。Spring可以帮你:
- 自动创建对象,你不用写 new。
- 自动注入依赖
- 控制对象的生命周期(什么时候创建、什么时候销毁)
- 帮你管理作用域:是单例?还是每次请求一个新的?都可以
IoC
控制反转(Inversion of Control),将对象的创建和依赖关系的维护从代码中移除,交给一个专门的容器(Spring 框架中的IoC 容器) 来管理。
在 Spring 应用 中,通常 Controller、Service 和 DAO(Repository) 都是由 Spring 容器管理的 Bean,这样可以充分利用 Spring 的 IoC(控制反转)和依赖注入(DI)机制,使得各层之间的依赖关系由 Spring 自动管理,减少手动创建对象的代码,提高可维护性和解耦性。
Bean
Bean是一个由Spring IoC容器管理的对象。这个对象可以是任何Java类的实例。Spring通过“控制反转(IoC)”的机制,把对象的创建权和管理权交给了容器。使用者不再手动 new 对象,而是让Spring把准备好的对象(Bean)交给使用者使用。这样可以使代码更清晰,专注于业务逻辑;更容易替换实现、做单元测试。
声明类为Beans的方法
- 使用注解 @Component
import org.springframework.stereotype.Component;
@Component
public class MyService {
public void doSomething() {
System.out.println("Doing something...");
}
}
- @Component:通用组件
- @Service:表示业务逻辑层
- @Repository:表示数据访问层
- @Controller:表示Web控制器
- 使用 @Bean 注解
当你希望手动控制某个 Bean 的创建过程时,用这个方式。
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class AppConfig {
@Bean // 在方法上使用 @Bean,Spring会调用这个方法并将返回值作为一个 Bean。
public MyService myService() {
return new MyService(); // 手动创建并返回对象
}
}
Spring 会执行这个方法,获取返回的对象,把它注册成一个 Bean。
自动依赖注入
Spring 会自动找到类型是 Engine 的 Bean,注入给 Car。
- 构造器注入
@Component
public class Car {
private final Engine engine;
@Autowired
public Car(Engine engine) {
this.engine = engine;
}
}
- Setter 注入
@Component
public class Car {
private Engine engine;
@Autowired
public void setEngine(Engine engine) {
this.engine = engine;
}
public void start() {
engine.run();
}
}
- 字段注入
@Component
public class Car {
@Autowired
private Engine engine;
public void start() {
engine.run();
}
}
AOP
AOP(Aspect-Oriented Programming)面向切面编程。在传统的面向对象编程中,比如我们在写一个网站的业务逻辑时,比如处理订单、用户登录等,我们经常还需要加上日志记录、权限校验、事务管理等功能。这些功能虽然必须有,但它们跟业务逻辑(比如下订单)并没有直接关系。
比如我有一个订单服务:
public void createOrder() {
System.out.println("日志:开始执行 createOrder");
// 下单逻辑
System.out.println("日志:createOrder 执行完毕");
}
Spring AOP 是 Spring 框架提供的一种实现 AOP 编程的方法,它通过代理对象把横切逻辑“织入”到业务逻辑中,不需要你改业务代码。Spring 根据配置创建一个代理对象,每当你调用业务方法(如 orderService.createOrder())时,代理对象会先执行切面逻辑(如日志),再执行真正的业务逻辑。
@Service
public class OrderService {
public void createOrder() {
System.out.println("执行创建订单逻辑...");
}
}
// 定义一个切面类(日志逻辑)
@Aspect
@Component
public class LogAspect {
// 切入点表达式:拦截所有 OrderService 的方法
@Pointcut("execution(* com.example.demo.OrderService.*(..))")
public void orderMethods() {}
// 前置通知:方法执行前记录日志
@Before("orderMethods()")
public void logBefore(JoinPoint joinPoint) {
System.out.println("日志:开始执行方法 -> " + joinPoint.getSignature().getName());
}
// 后置通知:方法执行后记录日志
@After("orderMethods()")
public void logAfter(JoinPoint joinPoint) {
System.out.println("日志:结束执行方法 -> " + joinPoint.getSignature().getName());
}
}
// 启动类开启 AOP 功能
@SpringBootApplication
@EnableAspectJAutoProxy
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}
这样,当我调用 orderService 的代码时,比如 orderService.createOrder(),控制台会打印:
日志:开始执行方法 -> createOrder
执行创建订单逻辑...
日志:结束执行方法 -> createOrder
Spring的原理
反射
反射是 Java 提供的一种机制,可以在程序运行时(不是写代码时),动态地获取类的信息(比如类的名字、方法、变量、构造函数等),甚至动态地调用对象的方法或访问它的属性。详细的反射介绍:反射。
IoC原理
回顾IoC主要是将对象的创建和依赖管理的责任从程序员手中“反转”给一个容器来管理,从而降低耦合度。在Spring启动时,会进行以下处理:
- 当 Spring 启动时,会扫描指定的包或目录(通常通过 @ComponentScan 注解指定),查找类上标注的注解(如 @Component、@Service、@Repository、@Controller 等)
- 所有标注了这些注解的类会被注册为 Spring 容器中的 Bean。
- 在创建 Bean 时,如果该 Bean 有依赖的其他 Bean(如通过 @Autowired 注解标注),Spring 会自动注入这些依赖对象。
AOP原理
动态代理
实例化一个接口时:
静态代理:你写了一个类去实现接口,写死了逻辑,编译器在编译期生成字节码。
动态代理:你在运行时才“生成一个类”(不是写出来的源码,而是直接生成字节码并加载),这个类会去实现接口,并在方法调用时,把实际处理交给 InvocationHandler。
如何让Java自动生成一个类来实现一个Interface?
// newProxyInstance 是 Java 提供的 JDK 动态代理 的入口方法。
// 它能在运行时创建一个代理对象,这个代理对象会实现你指定的接口,
// 但它的方法调用实际会被转发给一个你自己定义的处理器对象(InvocationHandler)
public static Object newProxyInstance(
ClassLoader loader, // 类加载器
Class<?>[] interfaces, // 接口列表
InvocationHandler h);
// 调用示例:实现对UserCervice interface的动态代理
public interface UserService {
void createUser(String username);
void deleteUser(String username);
}
public class Main {
public static void main(String[] args) {
// 创建一个 InvocationHandler,用来处理所有方法的调用
InvocationHandler handler = new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// 打印日志
System.out.println("[LOG] 调用方法:" + method.getName());
System.out.println("[LOG] 参数:" + args[0]);
// 模拟方法执行
if (method.getName().equals("createUser")) {
System.out.println("正在创建用户:" + args[0]);
} else if (method.getName().equals("deleteUser")) {
System.out.println("正在删除用户:" + args[0]);
}
return null; // 接口方法返回 void,这里返回 null
}
};
// 创建代理对象(实现了 UserService 接口)
UserService proxy = (UserService) Proxy.newProxyInstance(
UserService.class.getClassLoader(),
new Class[] { UserService.class },
handler
);
// 调用代理对象的方法,实际会被拦截并执行 handler.invoke(...)
proxy.createUser("alice");
proxy.deleteUser("bob");
}
}
以上例子,使用了动态代理的方法,创建了一个对象,实现了 UserService 接口。
实际输出:
[LOG] 调用方法:createUser
[LOG] 参数:alice
正在创建用户:alice
[LOG] 调用方法:deleteUser
[LOG] 参数:bob
正在删除用户:bob
AOP的实现使用了动态代理,其中代理对象在运行时动态地拦截方法调用并进行增强操作。
大体流程:
Spring启动时,会扫描标注了 @Aspect 的类,这些 @Aspect 类内部会定义切入点(@Pointcut)和通知(@Before、@After、@Around 等)。一旦 Spring 扫描到 @Aspect 注解的类,它就会在目标类上创建一个代理对象(根据注解动态生成代理对象)。Spring 会用代理对象替换原始目标对象,放入 Beans 来管理。代理对象的 invoke 方法会被 Spring 重写。在该方法中,Spring 会根据注解里的定义执行哪些横切逻辑。此后所有的方法调用都走代理逻辑,从而实现 AOP。
解决循环依赖
循环依赖是指 多个 Bean 之间的依赖形成了一个闭环,例如:A 依赖 B,B 依赖 A。如果 Spring 直接实例化 A,它发现 A 需要 B,于是去创建 B,但 B 又需要 A,形成了死锁,导致程序无法继续。为了解决循环依赖,Spring 在创建 Bean 时,使用了三级缓存机制:
- 一级缓存:用来存放已经创建的对象,通常是完全初始化的 Bean 实例。
- 二级缓存:用来存放已经实例化但是还没完全初始化的Bean(还未进行注入操作)。
- 三级缓存:这个缓存用来存放 Bean 工厂,它是用来获取 Bean 的实例。