Java外功精要(2)——Spring IoCDI
1.IoC(控制反转)
1.1 Spring Ioc VS Servlet
在上文:Java外功基础(1)——Spring Web MVC中,很形象地模拟出使用Spring"建造房子"的大概流程。使用Spring建造房子不需要像Servlet那样烧制每一块砖,只需要从Spring中取出一个个提前预制好的组件然后组装即可。换言之:Spring是包含了大量工具的IoC容器
1.2 IoC解析
1.2.1 IoC概述
概念:
IoC(Inversion of Control,控制反转),是一种设计原则,用于减少代码间的直接依赖关系。传统编程中,调用者通常主动创建和管理被调用者的生命周期,而 IoC 将这种控制权交给外部容器或框架,由容器负责对象的创建、依赖注入和管理
示例一:
传统编程模式
class Car{protected Framework framework;public Car(){framework = new Framework();System.out.println("car init");}public void run() {System.out.println("car run");}
}class Framework{protected Bottom bottom;public Framework(){bottom = new Bottom();System.out.println("framework init");}
}class Bottom{protected Tire tire;public Bottom(){tire = new Tire();System.out.println("bottom init");}
}class Tire{public Tire(){System.out.println("tire init");}
}public class Main {public static void main(String[] args) {Car car = new Car();car.run();}
}
缺点:
- 所有依赖都在类内部硬编码,缺乏灵活性
- 难以修改组件的具体实现(例如更换Tire的类型或参数)
- 组件之间耦合度高
示例二:
IoC
class Car{protected Framework framework;public Car(Framework framework){this.framework = framework;System.out.println("car init");}public void run() {System.out.println("car run");}
}class Framework{protected Bottom bottom;public Framework(Bottom bottom){this.bottom = bottom;System.out.println("framework init");}
}class Bottom{protected Tire tire;public Bottom(Tire tire){this.tire = tire;System.out.println("bottom init");}
}class Tire{protected int size;protected String color;public Tire(int size, String color){this.size = size;this.color = color;System.out.println("tire init");}
}
public class Main {//main方法中实例化并保存了多个对象,理论上可以视为一种简单的容器实现public static void main(String[] args) {Tire tire = new Tire(10,"blue");Bottom bottom = new Bottom(tire);Framework framework = new Framework(bottom);Car car = new Car(framework);car.run();}
}
优点:
- 依赖通过构造函数从外部注入(依赖注入),组件之间解耦
- 可以灵活定制组件的具体实现(例如Tire的size和color可动态配置)
1.2.2 IoC容器管理
Spring IoC容器保存了以下几类核心内容:
Bean定义(Bean Definitions):
这是容器保存的最核心的元数据。它就像是对象的“蓝图”或“配方”,告诉容器如何创建一个Bean。容器在启动时(比如ApplicationContext被刷新时)会加载并解析这些定义Bean实例(Bean Instances):
Bean定义创建出来的实际对象Bean之间的依赖关系(Dependencies):
IoC的核心是“控制反转”,即由容器来注入依赖。因此,容器内部维护着一个依赖关系图Bean生命周期(Lifecycle State):
管理Bean的整个生命周期
- 已创建/未创建
- 依赖注入是否完成
- 初始化方法是否已执行
- 是否已被销毁
1.3 ApplicationContext
定义:
ApplicationContext是Spring IoC容器的核心接口,并有多种实现类。它实现了BeanFactory接口,因此可以通过getBean()方法获取Bean实例,但在实际开发中应优先使用依赖注入
//标识该类为Spring项目的启动类
@SpringBootApplication
public class SpringIocApplication {public static void main(String[] args) {ApplicationContext context = SpringApplication.run(SpringIocApplication.class, args);}
}
1.4 类注解
在Spring中有五大类注解,主要用于类级别的声明。它们都源自 @Component,它们在功能上几乎一致,主要区别在于语义和用途,用于标识不同架构层次的组件,让代码结构更清晰
@Component - 组件:
最基础的通用注解,用于标记任何一个需要被Spring管理的组件,当你不确定一个类属于@Controller、@Service、@Repository中的哪一类时,或者它就是一个通用的工具类、管理器等,就可以使用它@Controller - 控制器:
专门用于标记 MVC 模式中的控制器,用于接收 Web 请求、处理业务逻辑并返回视图或数据
- 派生注解:@RestController(@Controller + @ResponseBody)
@Service - 业务逻辑层:
专门用于标记业务逻辑层的组件。它本身不提供额外功能,但通过这个名称,开发者可以清晰地知道这个类包含了核心业务规则@Repository - 数据访问层:
专门用于标记数据访问层的组件,用于封装对数据库等数据源进行增删改查的类
- 特殊功能:自动将平台特定的异常(如SQLException)转换为Spring统一的DataAccessException层次结构中的非受查异常
- 统一异常体系
- 提供更有意义的异常信息,如BadSqlGrammarException(SQL语法错误)
- 简化异常处理,由于转换后的异常都是非受查异常,不再需要写大量的try-catch块,可以选择在适当的层次统一处理
@Configuration - 配置类:
用于标记配置类,其内部可以定义Bean(通常使用@Bean注解)
- 特殊功能:它确保了其中使用@Bean注解的方法的单例性。Spring 会通过CGLIB代理来拦截对@Bean方法的调用,确保每次返回的是同一个实例(单例作用域下)
1.5 获取Bean
1.5.1 通过Bean的类型获取
//将HelloController类交给Spring管理
@Controller
public class HelloController {public void demo() {System.out.println("Hello HelloController");}
}
@SpringBootApplication
public class SpringIocApplication {public static void main(String[] args) {ApplicationContext context = SpringApplication.run(SpringIocApplication.class, args);//通过Bean的类型获取HelloController helloController = context.getBean(HelloController.class);helloController.demo();}
}
如果HelloController类没有交给Spring管理,会抛出:
如果某接口(类)有多个实现类,此时根据类型来获取Bean会抛出:
public interface Func {
}@Service("dataBaseFunc")//设置Bean的名称为dataBaseFunc
class DataBaseFunc implements Func {}@Service("fileFunc")//设置Bean的名称为fileFunc
class FileFunc implements Func {}
1.5.2 通过Bean的名称(ID)获取
在Java中,Bean的命名规范如下:
Bean命名约定
约定是在命名bean时使用标准的Java约定作为实例字段名。也就是说,bean名称以小写字母开头,然后使用驼峰式大小写。此类名称的示例包括accountManager,accountService,userDao,loginController
以JDK17为例,在java.beans.Introspector
类中提供了如下方法用于Bean的命名
public static String decapitalize(String name) {if (name == null || name.length() == 0) {return name;}//如果类名的前两个字母都是大写,那么Bean的名字就是类名if (name.length() > 1 && Character.isUpperCase(name.charAt(1)) &&Character.isUpperCase(name.charAt(0))){return name;}char[] chars = name.toCharArray();//将类名的第一个字母转为小写作为Bean的名称chars[0] = Character.toLowerCase(chars[0]);return new String(chars);}
示例1:
public interface Func {void init();
}@Service("dataBaseFunc")//设置Bean的名称为dataBaseFunc
class DataBaseFunc implements Func {@Overridepublic void init(){System.out.println("database init");}
}@Service("fileFunc")//设置Bean的名称为fileFunc
class FileFunc implements Func {@Overridepublic void init(){System.out.println("file init");}
}
@SpringBootApplication
public class SpringIocApplication {public static void main(String[] args) {ApplicationContext context = SpringApplication.run(SpringIocApplication.class, args);Func dataBaseFunc = (Func)context.getBean("dataBaseFunc");Func fileFunc = (Func)context.getBean("fileFunc");dataBaseFunc.init();fileFunc.init();}
}
运行结果:
database init
file init
示例2:
以上述HelloController为例
@SpringBootApplication
public class SpringIocApplication {public static void main(String[] args) {ApplicationContext context = SpringApplication.run(SpringIocApplication.class, args);HelloController helloController = (HelloController)context.getBean("helloController");}
}
运行结果:
Hello HelloController
1.6 方法注解 - @Bean
1.6.1 特性解析
作用:
在五大类注解标记的类的方法上使用,告诉Spring该方法返回的对象应该被注册为Bean
使用场景:
配置类中定义第三方库的Bean或需要复杂初始化逻辑的Bean
package org.example.springioc.model;
@Data
public class UserObject {private String name;private Integer age;private Integer id;public UserObject(){}public UserObject(String name) {this.name = name;}public UserObject(String name, Integer age) {this.name = name;this.age = age;}public UserObject(String name, Integer age, Integer id) {this.name = name;this.age = age;this.id = id;}
}
package org.example.springioc.configuration;
@Configuration
public class UserConfiguration {@Beanpublic UserObject getUserObject1() {//你完全控制对象的创建过程return new UserObject();//无参构造}@Beanpublic UserObject getUserObject2() {//你可以传递任意参数return new UserObject("李四");//带参构造}
}
优点:完全控制对象创建,可以传递参数、调用方法、进行复杂初始化
@Component//或其他四大注解
public class UserObject {private String name;private Integer age;//Spring只会调用无参构造方法public UserObject() {//Spring通过反射创建对象时,只会调用这个构造方法//你无法传递"张三",18这样的参数}public UserObject(String name, int age) {this.name = name;this.age = age;}
}
问题:Spring通过反射创建Bean时,默认调用无参构造方法,你无法传递特定参数
1.6.2 通过Bean的类型获取
与五大类注解一致
1.6.3 通过Bean的名称(ID)获取
与五大注解不同,使用@Bean标注方法时方法名就是该Bean的名称
@SpringBootApplication
public class SpringIocApplication {public static void main(String[] args) {ApplicationContext context = SpringApplication.run(SpringIocApplication.class, args);UserObject getUserObject1 = (UserObject)context.getBean("getUserObject1");System.out.println(getUserObject1);UserObject getUserObject2 = (UserObject)context.getBean("getUserObject2");System.out.println(getUserObject2);}
}
运行结果:
UserObject(name=null, age=null, id=null)
UserObject(name=李四, age=null, id=null)
1.7 @ComponentScan - 扫描路径
概念:
@ComponentScan是Spring框架中一个非常核心的注解,它负责告诉Spring容器去哪里扫描那些被@Component、@Service、@Repository、@Controller等注解标记的类,并将它们自动注册为Bean
- 在启动类中,@SpringBootApplication注解包含了@ComponentScan注解,默认扫描路径是启动类所在的包及其子包(可以显式配置来指定扫描路径,如@ComponentScan(basePackages = {“org.example.springioc”}))
- 如果一个类没有添加Spring的五大注解,那么即使它位于扫描路径下,它也不会被注册为Spring容器中的Bean
2.DI(依赖注入)
2.1 概述
依赖注入
是一种设计模式,它将对象的依赖关系(即它所需要的其他对象)从外部注入,而不是由对象自己在内部创建
2.2 构造方法注入(Constructor Injection)
@Controller
public class HelloController {private HelloService helloService;private HelloComponent helloComponent;//如果只有一个构造方法,只能使用该构造方法//当存在多个构造方法时,如果有无参构造方法,默认调用无参构造方法//当存在多个构造方法时,如果没有无参构造方法,Spring就不知道该用哪个构造方法,需要使用@Autowired指定一个默认的构造方法public HelloController(){}public HelloController(HelloService helloService) {this.helloService = helloService;}@Autowiredpublic HelloController(HelloService helloService, HelloComponent helloComponent) {this.helloService = helloService;this.helloComponent = helloComponent;}public void demo() {helloService.demo();helloComponent.demo();System.out.println("Hello HelloController");}
}
构造方法使用规范:
- 当添加有参构造方法时,显式添加无参构造方法
- 如果存在多个构造方法,使用@Autowired指定一个默认的构造方法
2.3 Setter方法注入(Setter Injection)
@Controller
public class HelloController {private HelloService helloService;private HelloComponent helloComponent;//搭配@Autowired使用@Autowiredpublic void setHelloService(HelloService helloService) {this.helloService = helloService;}@Autowiredpublic void setHelloComponent(HelloComponent helloComponent) {this.helloComponent = helloComponent;}public void demo() {helloService.demo();helloComponent.demo();System.out.println("Hello HelloController");}
}
2.4 字段/属性注入(Field Injection)
@Controller
public class HelloController {@Autowiredprivate HelloService helloService;@Autowiredprivate HelloComponent helloComponent;public void demo() {helloService.demo();helloComponent.demo();System.out.println("Hello HelloController");}
}
默认行为:
按类型匹配(byType)
- 查找候选Bean:Spring会首先在IoC容器中查找类型匹配的Bean
- 判断并注入:
- 找到0个:如果required属性为true(默认),则会抛出NoSuchBeanDefinitionException异常
- 找到1个:直接注入,这是最理想的情况
- 找到多个:此时按类型匹配失败,Spring 会尝试后备策略,即按名称匹配 (byName)。它会将变量名(或属性名)作为Bean的ID去容器中查找
2.5 优缺点对比
构造方法注入:
- 优点:
- 可以注入final修饰的属性
- 注入的依赖在调用前一定会完全初始化,因为构造方法在类加载阶段执行
- 在Spring以外的其他框架中同样支持,构造方法由JDK提供
- 缺点:
- 注入多个依赖时代码量庞大
Setter方法注入:
- 优点:
- Setter方法可以被多次调用,这意味着该依赖可以多次注入
- 缺点:
- 不能注入final修饰的属性
- 多次注入可能导致依赖被修改
字段/属性注入:
- 优点:
- 代码简洁,使用方便
- 缺点:
- 不能注入final修饰的属性
- 在非IoC容器中不可用
2.6 候选Bean问题
在使用@Autowired注入时,优先按照类型匹配,如果匹配到多个Bean,会将变量名(或属性名)作为Bean的ID去容器中查找。当变量名(或属性名)匹配失败时,程序无法运行成功
在上述代码中,UserController类需要注入一个单例Bean,却发现多个候选Bean,这叫做候选Bean问题
2.6.1 @Primary注解
作用:
指定当存在多个相同类型的 Bean 时,优先使用被标记的Bean
运行结果:UserObject(name=李四, age=null, id=null)
2.6.2 @Qualifier注解
作用:
是Spring框架中用于精确指定要注入哪个Bean的注解,它解决了当存在多个相同类型Bean时的依赖注入歧义问题
运行结果:UserObject(name=王五, age=18, id=102)
2.6.3 @Resource注解
作用:
@Resource注解是Java标准注解,Spring框架提供了对它的支持。它是@Autowired的一个替代方案,但在匹配机制上有重要区别
- 优先按照Bean的名称匹配
- 也可以明确指定名称,按名称精确匹配
2.6.4 优先级
当@Primary、@Qualifier、@Resource、@Autowired同时存在时,优先级如下: Resource > Qualifier > Primary > Autowired