Spring Framework 是一个功能强大的 Java 应用程序框架,旨在提供高效且可扩展的开发环境。它结合了轻量级的容器和依赖注入功能,提供了一种使用 POJO 进行容器配置和面向切面的编程的简单方法,以及一组用于 AOP 的模块。
Spring 两大核心机制:
- IoC(控制反转):工厂模式
- AOP(面向切面编程):代理模式
开发步骤
1、创建 Maven 工程,在 pom.xml
导入依赖:
1 2 3 4 5
| <dependency> <groupId>org.springframework</groupId> <artifactId>spring-framework-context</artifactId> <version>6.2.4</version> </dependency>
|
2、在 resources 路径下创建 spring.xml
:
1 2 3 4 5 6 7
| <?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<bean id="student" class="com.kecho.entity.Student"/> </beans>
|
3、获取 IoC 容器中已经创建的对象:
1 2 3 4 5 6 7
| ApplicationContext context = new ClassPathXmlApplicationContext("spring.xml");
Student student = (Student) context.getBean("student");
System.out.println(student.toString());
|
常用标签
bean
bean 标签用于创建一个对象,id 是唯一标识,class 是完全限定类名,name 可以同时取多个别名,别名之间可以使用空格或逗号或分号分隔。
1
| <bean id="student" class="com.kecho.entity.Student" name="student1 student2 student3"/>
|
alias
alias 可用于配置别名,alias 的 name 对应需要取别名的 bean 的 id。
1 2
| <bean id="student" class="com.kecho.entity.Student"/> <alias name="student" alias="stuNew" />
|
import
import 用于导入其它配置文件。
1 2
| <import resource="beans1.xml"/> <import resource="beans2.xml"/>
|
创建顺序
IoC 容器默认情况下是通过 spring.xml
中 bean 的配置顺序来决定创建顺序的,配置在前⾯的 bean 会先创建。在不更改配置顺序的前提下,通过 depends-on 来设置 bean 之间的依赖关系,从而调整 bean 的创建顺序。
1 2
| <bean id="student" class="com.kecho.entity.Student" depends-on="cat"/> <bean id="cat" class="com.kecho.entity.Cat"/>
|
上述代码的结果是先创建 Cat,再创建 Student 对象。
创建方式
IoC 容器通过读取 spring.xml
配置文件,按照顺序加载 bean 标签来创建对象,相当于替代了手动去 new 一个对象的过程,而创建对象的方式主要有两种:构造函数和 Setter 方法。
通过构造函数
IoC 容器通过对应构造函数去创建对象,这也要求实体类必须有对应的构造函数。
1
| <bean id="student" class="com.kecho.entity.Student"></bean>
|
- 有参构造函数(实体类必须有相同参数列表的有参构造函数)
1 2 3 4 5
| <bean id="student" class="com.kecho.entity.Student"> <constructor-arg name="id" value="1"/> <constructor-arg name="name" value="kecho"/> <constructor-arg name="age" value="18"/> </bean>
|
使用 name 时,参数顺序必须和构造函数中的参数顺序保持一致,其中 String 类型会自动转 int 类型(实体类 Student 中的 id 和 age 均为 int 类型)。
你也可以使用 index 来避免顺序不一致导致可能出现的问题:
1 2 3 4 5
| <bean id="student" class="com.kecho.entity.Student"> <constructor-arg index="1" value="kecho"/> <constructor-arg index="2" value="18"/> <constructor-arg index="0" value="1"/> </bean>
|
还可以使用 type 根据参数类型来匹配构造函数创建 bean(如果有多个有参构造函数,通过参数类型无法匹配到唯一的构造函数时会报错):
1 2 3 4 5
| <bean id="student" class="com.kecho.entity.Student"> <constructor-arg type="int" value="1"/> <constructor-arg type="java.lang.String" value="kecho"/> <constructor-arg type="int" value="18"/> </bean>
|
通过 Setter 方法
IoC 容器通过 Setter 方法去创建对象,这要求对应字段在实体类中必须有对应的 Setter 方法。
1 2 3 4 5
| <bean id="student" class="com.kecho.entity.Student"> <property name="id" value="1"/> <property name="name" value="kecho"/> <property name="age" value="18"/> </bean>
|
如果我想把 name 的值设置为 <小二>
,因为字符串包含了特殊字符,应该改用 CDATA 格式:
1 2 3 4 5 6 7
| <bean id="student" class="com.kecho.entity.Student"> <property name="id" value="1"/> <property name="name"> <value><![CDATA[<小二>]]]></value> </property> <property name="age" value="18"/> </bean>
|
两种创建方式的写法非常相似,主要是 constructor-arg
和 property
的不同,接下来的示例主要以 Setter 方法为主,构造器注入同理。
依赖注入
DI 是指 bean 之间的依赖注入,用于设置对象之间的级联关系。此时应该改用 ref 而不能改用 value,否则会抛出类型转换异常。除了 ref,还有数组、 List、Set、Map、Properties 类型,下面将使用 Setter 方式注入展示各种类型的写法:
实体类 Student:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| @Data @ToString @AllArgsConstructor @NoArgsConstructor public class Student { private String name; private Cat cat; private String[] books; private List<String> hobbys; private Set<String> games; private Map<String,String> card; private String spouse; private Properties info; }
|
spring.xml:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45
| <bean id="cat" class="com.kecho.entity.Cat"/> <bean id="student" class="com.kecho.entity.Student"> <property name="name" value="张三" /> <property name="cat" ref="cat"/> <property name="books"> <array> <value>红楼梦</value> <value>西游记</value> </array> </property> <property name="hobbys"> <list> <value>听歌</value> <value>敲代码</value> </list> </property> <property name="games"> <set> <value>LOL</value> <value>王者荣耀</value> </set> </property> <property name="card"> <map> <entry key="身份证" value="123456123412121234"/> </map> </property> <property name="spouse"> <null/> </property> <property name="info"> <props> <prop key="学号">111111</prop> <prop key="性别">男</prop> </props> </property> </bean>
|
上面如果改用构造器注入又改怎么写呢?只需要将 property
改成 constructor-arg
即可。
命名空间
c 命名空间
c 命名空间对应构造器注入。
使用 c 命名空间应在 xml 头部引入约束:xmlns:c="http://www.springframework.org/schema/c"
:
1 2 3 4 5 6 7 8 9 10
| <?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:c="http://www.springframework.org/schema/c" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<bean id="cat" class="com.kecho.entity.Cat" c:name="咪咪"/> <bean id="student" class="com.kecho.entity.Student" c:name="kecho" c:id="1" c:age="18" c:cat-ref="cat"/> </beans>
|
p 命名空间
p 命名空间对应 Setter 方法注入。
使用 p 命名空间应在 xml 头部引入约束:xmlns:p="http://www.springframework.org/schema/p"
:
1 2 3 4 5 6 7 8 9 10 11
| <?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:p="http://www.springframework.org/schema/p" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<bean id="cat" class="com.kecho.entity.Cat" p:name="咪咪"/> <bean id="student" class="com.kecho.entity.Student" p:name="kecho" p:id="1" p:age="18" p:cat-ref="cat"/> </beans>
|
作用域
bean 是根据 scope 来⽣成,表示 bean 的作⽤域,scope 有4种类型:
- singleton,单例,表示通过容器获取的对象是唯⼀的,默认值。
- prototype,原型,表示通过容器获取的对象是不同的。
- request,请求,表示在⼀次 HTTP 请求内有效。
- session,会话,表示在⼀个⽤户会话内有效。
requset,session 适⽤于 Web 项⽬。
singleton 模式下,只要加载 IoC 容器,⽆论是否从 IoC 中取出 bean,配置⽂件中的 bean 都会被创建。prototype 模式下,如果不从 IoC 中取 bean,则不创建对象,取⼀次 bean,就会创建⼀个对象。
1
| <bean id="student" class="com.kecho.entity.Student" scope="prototype"/>
|
也可以通过对类添加注解 @Scope("prototype")
来实现同样功能。
懒加载
默认情况下,ApplicationContext 的实现会急切地创建和配置所有的单例 bean,你可以通过将 bean 定义标记为懒加载来阻止单例 bean 的预实例化。懒加载的 bean 告诉 IoC 容器在第一次被请求时创建一个 bean 实例,而不是在启动时。
1
| <bean id="student" class="com.example.Student" lazy-init="true"/>
|
当懒加载 bean 是被未懒加载的单例 bean 所依赖时,ApplicationContext 会在启动时创建懒加载 bean。
你也可以通过使用 <beans>
元素上的 default-lazy-init
属性来控制容器级的懒加载:
1 2 3
| <beans default-lazy-init="true"> </beans>
|
读取外部资源
实际开发中,数据库的配置⼀般会单独保存到后缀为 properties 的⽂件中,⽅便维护和修改,如果使⽤ Spring 来加载数据源,就需要在 spring.xml
中读取 properties 中的数据,这就是读取外部资源。
jdbc.properties
1 2 3 4
| user = root password = root url = jdbc:mysql://localhost:3306/library driverName = com.mysql.cj.jdbc.Driver
|
spring.xml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| <?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd">
<context:property-placeholder location="classpath:jdbc.properties"/> <bean id="dataSource" class="com.southwind.entity.DataSource"> <property name="user" value="${user}"/> <property name="password" value="${password}"/> <property name="driverName" value="${driverName}"/> <property name="url" value="${url}"/> </bean> </beans>
|
工厂方法
IoC 通过⼯⼚模式创建 bean 有两种⽅式:静态⼯⼚⽅法和实例⼯⼚⽅法,区别在于静态⼯⼚类不需要实例化,实例⼯⼚类需要实例化。
静态⼯⼚⽅法
1、创建 Car 类
1 2 3 4 5 6
| @Data @AllArgsConstructor public class Car { private Integer num; private String brand; }
|
2、创建静态⼯⼚类、静态⼯⼚⽅法
1 2 3 4 5 6 7 8 9 10 11
| public class StaticCarFactory{ private static Map<Integer, Car> carMap; static { carMap = new HashMap<>(); carMap.put(1,new Car(1,"奥迪")); carMap.put(2,new Car(2,"奥拓")); } public static Car getCar(Integer num){ return carMap.get(num); } }
|
3、spring.xml
1 2 3
| <bean id="car1" class="com.example.factory.StaticCarFactory" factory-method="getCar"> <constructor-arg value="1"></constructor-arg> </bean>
|
factory-method
指向静态⽅法,constructor-arg
的 value 属性是调⽤静态⽅法传⼊的参数。
实例⼯⼚⽅法
1、创建实例⼯⼚类、⼯⼚⽅法
1 2 3 4 5 6 7 8 9 10 11
| public class InstanceCarFactory{ private Map<Integer, Car> carMap; public InstanceCarFactory() { carMap = new HashMap<>(); carMap.put(1,new Car(1,"奥迪")); carMap.put(2,new Car(2,"奥拓")); } public Car getCar(Integer num){ return carMap.get(num); } }
|
2、spring.xml
1 2 3 4
| <bean id="instanceCarFactory" class="com.example.factory.InstanceCarFactory"/> <bean id="car2" factory-bean="instanceCarFactory" factory-method="getCar"> <constructor-arg value="2"></constructor-arg> </bean>
|
静态⼯⼚⽅法创建 Car 对象,不需要实例化⼯⼚对象,因为静态⼯⼚的静态⽅法,不需要创建对象即可调⽤,spring.xml 中只需要配置⼀个 bean,即最终的结果 Car 即可。
实例⼯⼚⽅法创建 Car 对象,需要实例化⼯⼚对象,因为 getCar ⽅法是⾮静态的,就必须通过实例化对象才能调⽤,所以就必须要创建⼯⼚对象,spring.xml
中需要配置两个 bean,⼀个是⼯⼚ bean,⼀个是 Car bean。
自动装载
已知有两个实体类 Student 和 Cat。
Student:
1 2 3 4 5 6 7 8
| @Data @ToString public class Student { private int id; private String name; private int age; private Cat cat; }
|
Cat:
1 2 3 4
| @Data public class Cat { private String name; }
|
spring.xml:
1 2 3 4 5 6 7 8
| <bean id="cat" class="com.kecho.entity.Cat"> <property name="name" value="咪咪"/> </bean> <bean id="student" class="com.kecho.entity.Student" autowire="byName"> <property name="id" value="1"/> <property name="name" value="kecho"/> <property name="age" value="18"/> </bean>
|
最终得到 Student(id=1, name=kecho, age=18, cat=Cat(name=咪咪))
,通过 autowire
可以自动地将 cat 装载到 student 中,即使未显示使用 ref 声明。
autowire
有两个值:byName 和 byType。
byName 会去找和 set 方法后面的值相同的 bean id 的 bean 自动装载,没有相同的则会报错。
byType 会去找和成员变量的数据类型相同的 bean 自动装载,如果容器中有两个相同类型的 bean 则会报错。
基于注解开发
使用注解开发需要:
以下创建 bean 时没有使用到 Setter 方法。
手动创建
需要在 xml 中手动声明 bean:
1 2 3 4 5 6 7 8 9 10 11 12
| <?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd">
<context:annotation-config/>
<bean id="cat" class="com.kecho.entity.Cat"/> <bean id="student" class="com.kecho.entity.Student"/>
</beans>
|
此时可以使用 @Value
来设置属性值,使用 @Autowired
实现自动装载:
1 2 3 4 5 6 7 8 9 10 11
| public class Student { @Value("1") private int id; @Value("kecho") private String name; @Value("18") private int age; @Autowired private Cat cat; }
|
如果容器中没有这个 bean,@Autowired
会自动装配失败报错,可以使用 @Autowired(require = false)
来允许为空。
@Autowired
默认通过 byType 的方式来查找 bean,如果存在多个相同类型的 bean 也会无法自动装配,可以加上 @Qualifier(value = "beanid")
指定装配相应 id 的 bean,不带参数的 @Qualifier
默认指定 id 与属性名对应的 bean。
除了使用 @Autowired
,Java 也有自己的注解 @Resource
实现相同的功能,也可以通过 @Resource(name = "bean id")
指定 bean id 来自动装配对应的 bean。
不同于 @Autowired
通过 byType 的方式来查找 bean,@Resource
默认通过 byName 的方式实现,如果找不到名字则通过 byType 的方式。
自动扫描
为了避免每个 bean 都需要自己手动去创建,可以配置扫描包:
1 2 3 4 5 6 7 8 9 10
| <?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd">
<context:annotation-config/> <context:component-scan base-package="com.kecho.entity"/>
</beans>
|
此时使用 @Component
来自动加载:
1 2 3 4 5 6 7 8 9 10 11
| @Component public class Student { @Value("1") private int id; @Value("kecho") private String name; @Value("18") private int age; @Autowired(required = false) private Cat cat; }
|
除了 @Component
外,还有 @Repository
、@Service
、@Controller
,这些注解的功能是一样的。
@Configuration
除了在 xml 配置文件中进行创建 bean、配置 context 约束、配置扫描包路径、配置注解的支持等等,还可以在 Java 类中进行配置:
MyConfig 类:
1 2 3 4 5 6 7 8 9
| @Configuration
public class MyConfig { @Bean public Student myStudent() { return new Student(1, "kecho", 18); } }
|
获取 bean:
1 2 3
| ApplicationContext context = new AnnotationConfigApplicationContext(MyConfig.class); User user = context.getBean("myUser", User.class);
|
如果要自动扫描包来加载 bean,可以为 MyConfig 类设置 @ComponentScan("com.kecho.entity")
,同时搭配 @Component
来自动加载 bean。如下:
MyConfig 类:
1 2 3 4 5
| @Configuration @ComponentScan("com.kecho.entity") public class MyConfig {
}
|
Student 类:
1 2 3 4 5 6 7 8 9 10 11
| @Component public class Student { @Value("1") private int id; @Value("kecho") private String name; @Value("18") private int age; @Autowired(required = false) private Cat cat; }
|
IoC
控制反转(Inversion of Control,简称 IoC)是一种设计思想,它将对象的创建和依赖关系的管理交给容器,而不是在代码中直接控制对象的创建和依赖关系。IoC 的核心思想是通过容器来管理对象的生命周期和依赖关系,从而实现对象之间的松耦合。
思想
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
| public interface Fruit { public void get(); }
public class Apple implements Fruit { public void get(){ System.out.println("get a Apple"); } }
public class Banana implements Fruit { public void get(){ System.out.println("get a Banana"); } }
public interface UserService { public void getFruit(); }
public UserServiceImpl implements UserService { private Fruit fruit = new Apple(); public void getFruit() { fruit.get(); } }
public class MyTest { public static void main(String[] args) { UserService user = new UserServiceImpl(); user.getFruit(); } }
|
目前的控制权在业务层,每次用户需求改变时业务层也要跟着改变,即需要修改 UserServiceImpl 中的代码,如果工程量较大修改起来会很麻烦,而且修改代码容易对其它业务造成影响,可以考虑把控制权交给用户。
修改业务层的代码实现控制权的转换,程序由主动创建对象变为被动接收对象:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| public class UserServiceImpl implements UserService { private Fruit fruit; public void setFruit(Fruit fruit) { this.fruit = fruit; } public void getFruit() { this.fruit.get(); } }
public class MyTest { public static void main(String[] args) { UserService user = new UserServiceImpl(); user.setFruit(new Apple()); user.getFruit(); } }
|
在 spring.xml 中就是这么配置:
1 2 3 4 5
| <bean id="apple" class="com.example.dao.Apple"/> <bean id="banana" class="com.example.dao.Banana"/> <bean id="userServiceImpl" class="com.example.service.UserServiceImpl"> <property name="fruit" ref="apple" /> </bean>
|
实现原理
核⼼技术点:XML 解析 + 反射机制
具体的思路:
1、根据需求编写 XML ⽂件,配置需要创建的 bean。
2、编写程序读取 XML ⽂件,获取 bean 相关信息,类、属性、id。
3、根据第 2 步获取到的信息,结合反射机制动态创建对象,同时完成属性的赋值。
4、将创建好的 bean 存⼊ Map 集合,设置 key - value 映射,key 就是 bean 中 id 值,value 就是 bean 对象。
5、提供⽅法从 Map 中通过 id 获取到对应的 value。
下面将自定义一个 MyClassPathXmlApplicationContext 类来实现。
首先需要在 pom.xml
引入依赖解析 xml 文件:
1 2 3 4 5
| <dependency> <groupId>dom4j</groupId> <artifactId>dom4j</artifactId> <version>1.6.1</version> </dependency>
|
自定义 MyClassPathXmlApplicationContext 类(这里假设 bean 的属性仅为 int 或 String 类型):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62
| public class MyClassPathXmlApplicationContext implements ApplicationContext { private Map<String, Object> iocMap;
public MyClassPathXmlApplicationContext(String path) { iocMap = new HashMap<>(); parseXml(path); }
public void parseXml(String path){
SAXReader reader = new SAXReader(); try { Document document = reader.read("src/main/resources/" + path); Element rootElement = document.getRootElement(); Iterator<Element> elementIterator = rootElement.elementIterator(); while (elementIterator.hasNext()) { Element bean = elementIterator.next(); String beanID = bean.attributeValue("id"); String beanClassName = bean.attributeValue("class"); Class myClass = Class.forName(beanClassName); Constructor constructor = myClass.getConstructor(); Object object = constructor.newInstance(); Iterator<Element> propertyIterator = bean.elementIterator(); while (propertyIterator.hasNext()) { Element property = propertyIterator.next(); String propertyName = property.attributeValue("name"); String propertyValue = property.attributeValue("value"); String methodName = "set" + propertyName.substring(0, 1).toUpperCase() + propertyName.substring(1); Field field = myClass.getDeclaredField(propertyName); Method method = myClass.getMethod(methodName, field.getType()); Object value = propertyValue; switch (field.getType().getName()){ case "java.lang.String": break; case "int": case "java.lang.Integer": value = Integer.parseInt(propertyValue); break; } method.invoke(object, value); } iocMap.put(beanID, object); } } catch (Exception e){ e.printStackTrace(); } } @Override public Object getBean(String beanID) throws BeansException { return iocMap.get(beanID); } }
|
spring.xml
配置:
1 2 3 4 5 6 7 8 9 10 11 12
| <?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd ">
<bean id="student" class="com.kecho.entity.Student"> <property name="id" value="1"/> <property name="name" value="kecho"/> <property name="age" value="18"/> </bean>
</beans>
|
测试类:
1 2 3 4 5
| public static void main(String[] args ){ ApplicationContext context = new MyClassPathXmlApplicationContext("spring.xml"); Student student = (Student) context.getBean("student"); System.out.println(student); }
|
AOP