官网:https://spring.io/
核心功能:当你的项目启动的时候,自动的将当前项目的各种 Bean 都自动的注册到 Spring 容器中,然后在项目的其他地方,如果需要用到这些 Bean,直接去 Spring 容器中查找需要的对象即可。
Spring 家族的产品:
Spring Framework:
创建一个 Maven 工程。
引入 Spring 依赖:
<dependencies>
<dependency>
<groupId>org.springframeworkgroupId>
<artifactId>spring-contextartifactId>
<version>5.3.20version>
dependency>
dependencies>
加入 Spring 的配置。
创建一个 Bean 并注册。
public class User {
public void sayHello() {
System.out.println("hello zhangsan");
}
}
<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 class="com.qfedu.demo.model.User" id="user"/>
beans>
启动 Spring 容器,并加载配置文件,当 Spring 容器启动之后,无论你是否跟 Spring 容器去要 User 对象,此时 User 对象都是已经创建好的状态,并保存在 Spring 容器中。
public class MainDemo01 {
public static void main(String[] args) {
//这里只需要写配置文件的文件名即可,系统会自动去 classpath 下查找配置文件
//这个就是加载 Spring 容器,只要 Spring 容器启动了,那么配置文件中的所有 Bean 就会完成初始化
ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml");
//根据名字去查找一个 user 对象,这个方法返回一个 Object,需要进行类型转换
User u1 = (User) ctx.getBean("user");
u1.sayHello();
//去查找 User 类型的对象
//这种方式有一个缺陷:Spring 容器中如果存在多个 User 对象,那么这个方法执行就会报错
User u2 = ctx.getBean(User.class);
u2.sayHello();
//告诉 Spring 容器,查找一个名为 user 的 bean,并且类型是 User
User u3 = ctx.getBean("user", User.class);
u3.sayHello();
}
}
NoSuchBeanDefinitionException
:
没有找到需要的 Bean:
NoUniqueBeanDefinitionException
:
这表示要查找的目标 Bean 有多个,查找异常。这种时候就不要按照类型去查找,而应该按照名字去查找。
构造器注入
默认情况下,如果我们向 Spring 容器注入一个 Bean 的时候,不指定构造方法,那么默认使用的构造方法就是无参构造方法,所以如果你的类里边没有无参构造方法,就会出错。
public class Book {
private Integer id;
private String name;
private String author;
public Book(Integer id, String name, String author) {
this.id = id;
this.name = name;
this.author = author;
}
}
由于这个 Java 类没有无参构造方法,所以在注入 Bean 的时候,如果按照下面的方式注入,就会出错:
<bean class="com.qfedu.demo.p2.model.Book" id="book"/>
在 bean 标签中,如果没有指定构造方法,默认就使用无参构造方法。
开发者也可以自己指定要使用哪一个构造方法:
<bean class="com.qfedu.demo.p2.model.Book" id="book"/>
<bean class="com.qfedu.demo.p2.model.Book" id="book2">
<constructor-arg name="id" value="1"/>
<constructor-arg name="name" value="三国演义"/>
<constructor-arg name="author" value="罗贯中"/>
bean>
set 方法注入
<bean class="com.qfedu.demo.p2.model.Book" id="book3">
<property name="id" value="2"/>
<property name="name" value="红楼梦"/>
<property name="author" value="曹雪芹"/>
bean>
p 名称空间注入
p 名称空间注入,本质上其实就是 set 方法注入。
<bean class="com.qfedu.demo.p2.model.Book" id="book4" p:id="3" p:name="水浒传" p:author="施耐庵">bean>
List、数组、对象、Map、Set、Properties 等。
首先定义 User 类:
public class User {
private Integer id;
private String name;
private String address;
private Cat cat;
private List<String> favorites;
private List<Cat> cats;
private Book[] books;
private Map<String, Object> info;
private Properties school;
}
属性注入:
<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 class="com.qfedu.demo.model.Cat" id="cat">
<property name="name" value="小白"/>
<property name="color" value="白色"/>
bean>
<bean class="com.qfedu.demo.model.User" id="user">
<property name="id" value="1"/>
<property name="name" value="zhangsan"/>
<property name="address" value="广州"/>
<property name="cat" ref="cat"/>
<property name="favorites">
<list>
<value>足球value>
<value>篮球value>
list>
property>
<property name="cats">
<list>
<ref bean="cat"/>
<bean class="com.qfedu.demo.model.Cat">
<property name="name" value="小黑"/>
<property name="color" value="黑色"/>
bean>
list>
property>
<property name="books">
<array>
<bean class="com.qfedu.demo.model.Book">
<property name="id" value="1"/>
<property name="name" value="三国演义"/>
<property name="author" value="罗贯中"/>
bean>
<bean class="com.qfedu.demo.model.Book">
<property name="id" value="2"/>
<property name="name" value="红楼梦"/>
<property name="author" value="曹雪芹"/>
bean>
array>
property>
<property name="info">
<map>
<entry key="gender" value="男"/>
<entry key="age" value="99"/>
map>
property>
<property name="school">
<props>
<prop key="name">广州千锋prop>
<prop key="address">广州市白云区prop>
props>
property>
bean>
beans>
如果是通过构造方法注入:
<bean class="com.qfedu.demo.model.User" id="user2">
<constructor-arg name="books">
constructor-arg>
bean>
内省。
对于框架而言,并不是看对象定义的属性叫什么名字,而是根据对象的 get/set 方法来推断属性名称,无论是 MyBatis、Spring、SpringMVC,所有框架,只要用到反射,都是这样的。所以,定义 get/set 方法的时候,不要写错,另一方面,变量的命名要符合规范。
用一个 Java 配置类,去代替 XML 配置即可:
/**
* 这个是 Java 配置类,它的作用类似于 applicationContext.xml
*
* @Configuration 表示这是一个配置类
*/
@Configuration
public class JavaConfig {
/**
* @Bean 就表示将当前方法的返回值注册到 Spring 容器中
*
* 默认情况下,方法名称就是 bean 的名字
*
* 如果需要自定义 bean 的名称,那么在注解中配置即可
*
* @return
*/
@Bean("u")
User user() {
User user = new User();
user.setAddress("广州");
user.setName("zhangsan");
return user;
}
}
启动 Spring 容器时,加载这个配置类即可:
public class Demo01 {
public static void main(String[] args) {
//启动 Spring 容器,加载一个 Java 配置类,构造方法中指定配置类即可
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(JavaConfig.class);
User user = ctx.getBean("u", User.class);
System.out.println("user = " + user);
}
}
@Configuration:这个注解表示当前类是一个配置类,那么当前类中,所有添加了 @Bean 注解的方法都会被注册到 Spring 容器中,如果有其他方法调用到一个添加了 @Bean 注解的方法,那么不会立马执行对应的方法,而是先去 Spring 容器中查看是否有对应的对象,如果有,则直接从容器中获取即可,如果容器中没有的话,才回去执行对应的方法。
@Component 虽然也可以加在配置类上,但是,如果有其他方法调用到一个添加了 @Bean 注解的方法,那么不会先去 Spring 容器中查看是否有对应的对象,而是直接执行对应的方法。所以一般在配置类中不使用 @Component 注解。如果一定要使用 @Component 注解,可以通过依赖注入来代替方法调用,类似下面这样:
/**
* 向 Spring 容器注册一个 Author 对象
*
* @return
*/
@Bean
Author author() {
Author author = new Author();
author.setName("鲁迅");
author.setAge(55);
return author;
}
/**
* 向 Spring 容器中注册一个 Book 对象
*
* book 中有一个 author 对象,book 中的 author 和 spring 容器中的 author 是否是同一个对象?
* @return
*/
@Bean
Book book2(Author author) {
Book book = new Book();
book.setName("故事新编");
book.setAuthor(author);
book.setPrice(18.0);
return book;
}
在这里,所有的方法都是 Spring 容器调用的,当 Spring 容器调用 book2 这个方法的时候,就会发现这个方法的执行需要一个 Author 类型的参数,那么此时 Spring 容器就会去查找是否有一个 Author,如果有,则直接作为参数传进来,如果 Spring 容器中没有这个对象,那么直接抛出异常。
条件注解是多环境配置的核心,思路就是提前准备好环境,所谓的环境,实际上就是 Condition 接口的实现类,然后在注册 Bean 的时候,通过 @Conditional 注解去指定环境,当满足某种条件的时候,bean 才会注入到 Spring 容器中。
两个非常经典的使用场景:
提前准备好生产环境、开发环境等的配置信息,将来通过 Profile 可以一键切换。
主要是使用 @Profile 注解,这个注解的本质就是 @Conditional 条件注解,用到的条件实际上就是 ProfileCondition。
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Conditional(ProfileCondition.class)
public @interface Profile {
/**
* The set of profiles for which the annotated component should be registered.
*/
String[] value();
}
条件注解:
class ProfileCondition implements Condition {
@Override
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
//获取 Profile 注解的所有属性,其实这个注解只有一个 value 属性,属性的值是一个数组
MultiValueMap<String, Object> attrs = metadata.getAllAnnotationAttributes(Profile.class.getName());
if (attrs != null) {
//将属性中的 value 读取出来,这个 value 的值实际上是一个 String 数组,遍历 String 数组
for (Object value : attrs.get("value")) {
//判断当前环境中,有没有 value 中的值
if (context.getEnvironment().acceptsProfiles(Profiles.of((String[]) value))) {
return true;
}
}
return false;
}
return true;
}
}
具体配置:
@Configuration
public class JavaConfig {
/**
* 开发环境的数据源
*
* 通过 @Profile("dev") 注解可以指定当前的环境是开发环境
* @return
*/
@Profile("dev")
@Bean("ds")
DataSource devDataSource() {
DataSource ds = new DataSource();
ds.setUsername("root");
ds.setPassword("123");
ds.setUrl("jdbc:mysql:///test01");
return ds;
}
/**
* 配置生产环境的数据源
* @return
*/
@Profile("prod")
@Bean("ds")
DataSource prodDataSource() {
DataSource ds = new DataSource();
ds.setUsername("zhangsan");
ds.setPassword("jdfkslajfl890324");
ds.setUrl("jdbc:mysql://114.132.43.22/prod01");
return ds;
}
}
注册 Bean 的时候,通过 @Profile(“prod”) 注解来指定当前 Bean 在哪个环境下生效。
当启动 Spring 容器的时候,要为 Spring 容器指定当前的环境信息:
public class Demo01 {
public static void main(String[] args) {
//注意先不要写配置类,要先设置环境信息
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();
//设置当前的环境信息
ctx.getEnvironment().addActiveProfile("prod");
ctx.register(JavaConfig.class);
ctx.refresh();
DataSource ds = ctx.getBean(DataSource.class);
System.out.println("ds = " + ds);
}
}
首先在 xml 文件中,通过 beans 标签来指定环境:
<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">
<beans profile="dev">
<bean class="com.qfedu.demo.p1.model.DataSource" id="dataSource">
<property name="username" value="root"/>
<property name="password" value="123"/>
<property name="url" value="jdbc:mysql:///test01"/>
bean>
beans>
<beans profile="prod">
<bean class="com.qfedu.demo.p1.model.DataSource" id="dataSource">
<property name="username" value="root"/>
<property name="password" value="jkld3u$%^"/>
<property name="url" value="jdbc:mysql://11.22.11.22/test01"/>
bean>
beans>
beans>
将来 Spring 容器启动的时候,会根据当前的环境信息去注册不同的 beans 标签中的 Bean。
启动容器的时候,设置一下当前环境即可:
public class Demo02 {
public static void main(String[] args) {
//先不要加载配置文件
ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext();
ctx.getEnvironment().addActiveProfile("dev");
//设置好当前环境之后,再去设置配置文件的位置
ctx.setConfigLocation("applicationContext.xml");
ctx.refresh();
DataSource ds = ctx.getBean(DataSource.class);
System.out.println("ds = " + ds);
}
}
主要是指 properties 配置文件的注入。
主要就是两个配置:
@Configuration
@PropertySource("classpath:db.properties")
public class DsConfig {
//跟 Spring 容器要一个字符串回来
@Value("${db.username}")
String username;
@Value("${db.password}")
String password;
@Value("${db.url}")
String url;
@Bean
DataSource dataSource() {
DataSource ds = new DataSource();
ds.setPassword(password);
ds.setUsername(username);
ds.setUrl(url);
return ds;
}
}
要能够从 Spring 容器中要到字符串,或者对象,有一个前提,当前 Bean 必须处于 Spring 容器中,不可以自己手动 new 一个 Bean。
<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:db.properties"/>
<bean class="com.qfedu.demo.p1.model.DataSource" id="dataSource">
<property name="url" value="${db.url}"/>
<property name="password" value="${db.password}"/>
<property name="username" value="${db.username}"/>
bean>
beans>
前面的 Bean 的注册方式,都是一个一个 Bean 去注册的。
整体上来说,三个方面:
将 Bean 注册到 Spring 容器中:@Repository、@Service、@Controller 以及 @Component。
/**
* 将 UserDao 注册到 Spring 容器中,将来谁需要使用 UserDao,就直接跟 Spring 容器去查找即可
*
*
* @Repository:一般是加在 Dao 层
* @Service:一般是加在 Service 层
* @Controller:一般是加在控制层,也就是 servlet
* @Component:身份不明的 Bean 注册到 Spring 容器中时,使用这个注解
*
* 技术上来说,这四个注解并没有差异,即实际上除了 @Controller 之外,其他几个注解是可以混用的。
*
* 实际开发中,不要混用
*
*/
@Repository
public class UserDao {
public User getUserByUsername(String username) {
User user = new User();
user.setUsername(username);
user.setAddress("广州");
return user;
}
}
从 Spring 容器中要一个 Bean 回来,官方推荐的方式,是通过构造器来注入。
/**
* UserService 想要使用 UserDao
* 1. UserService 自己需要在 Spring 容器
* 2. 跟 Spring 容器去要一个 UserDao
*/
@Service
public class UserService {
UserDao userDao;
/**
* 官方推荐使用构造器注入。因为 UserService 要注册到 Spring 容器,要生成一个对象,必然就要调用它的构造方法,现在它只有这一个构造法方法,那么 Spring 容器只能通过这个构造方法来获取一个 UserService 的实例,那么 Spring 容器会自动去查找容器中是否有一个 UserDao 的实例,如果有,就作为这个方法的参数传入进来
* @param userDao
*/
public UserService(UserDao userDao) {
this.userDao = userDao;
}
public User getUserById(String username) {
return userDao.getUserByUsername(username);
}
}
这里又涉及到一个问题:如果有多个构造器怎么办?@Autowired
注解可解决。
@Service
public class UserService {
UserDao userDao;
/**
* 官方推荐使用构造器注入。因为 UserService 要注册到 Spring 容器,要生成一个对象,必然就要调用它的构造方法,现在它只有这一个构造法方法,那么 Spring 容器只能通过这个构造方法来获取一个 UserService 的实例,那么 Spring 容器会自动去查找容器中是否有一个 UserDao 的实例,如果有,就作为这个方法的参数传入进来
*
* 如果有多个构造方法,那么可以通过 @Autowired 注解来告诉 Spring 容器,应该调用哪一个构造方法去初始化当前对象
* @param userDao
*/
@Autowired
public UserService(UserDao userDao) {
this.userDao = userDao;
}
public User getUserById(String username) {
return userDao.getUserByUsername(username);
}
}
配置包扫描。
@ComponentScan(basePackages = "com.qfedu.demo")
@Configuration
public class JavaConfig {
}
通过 @ComponentScan
注解指定组件的路径即可。
和前面的步骤相比,主要是第二步不一样,其他都是一样的。
@Controller
public class UserServlet {
/**
* @Autowired 表示根据类型去 Spring 容器中查找到相应的 Bean,并赋值给 userService 变量
*/
@Autowired
UserService userService;
public User getUserById(String username) {
return userService.getUserById(username);
}
}
直接在属性上添加 @Autowired
注解就可以完成属性的注入了。
这种写法是我们在实际开发中使用较多的一种方式。
构造方法注入:
注解注入:
XML 配置包扫描跟 Java 配置包扫描,区别在于第三步。即包扫描用 XML 文件来配置:
<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:component-scan base-package="com.qfedu.demo"/>
beans>
这个主要是解决一些第三方的 Bean,一些无法通过构造方法正常初始化、或者无法通过 set 方法正常为属性的赋值的 Bean,可以通过工厂 Bean 的方式将之注册到 Spring 容器中。
工厂方法是一个静态方法。
public class OkHttpClientFactory {
private static OkHttpClient okHttpClient;
public static OkHttpClient getInstance() {
if (okHttpClient == null) {
okHttpClient = new OkHttpClient.Builder()
//设置服务端的读取超时时间
.readTimeout(5000, TimeUnit.SECONDS)
//连接超时
.connectTimeout(5000, TimeUnit.SECONDS)
.build();
}
return okHttpClient;
}
}
在 XML 文件中,直接配置工厂 Bean 即可:
<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 class="com.qfedu.demo.OkHttpClientFactory" factory-method="getInstance" id="client01"/>
beans>
需要注意的是,这里将来注册到 Spring 容器中的 Bean,不是 class 对应的类的对象,而是 getInstance 方法返回的对象。
工厂方法变为实例方法,执行的时候,必须要现有工厂对象,然后才能调用对应的方法。
public class OkHttpClientFactory2 {
private OkHttpClient okHttpClient;
public OkHttpClient getInstance() {
if (okHttpClient == null) {
okHttpClient = new OkHttpClient.Builder()
//设置服务端的读取超时时间
.readTimeout(5000, TimeUnit.SECONDS)
//连接超时
.connectTimeout(5000, TimeUnit.SECONDS)
.build();
}
return okHttpClient;
}
}
Bean 的注册:
<bean class="com.qfedu.demo.OkHttpClientFactory2" id="clientFactory2"/>
<bean class="okhttp3.OkHttpClient" factory-bean="clientFactory2" factory-method="getInstance" id="client02"/>
这是 Spring 官方推荐的工厂 Bean 的实现方式。
public class OkHttpClientFactoryBean implements FactoryBean<OkHttpClient> {
/**
* 返回真正的对象
* @return
* @throws Exception
*/
@Override
public OkHttpClient getObject() throws Exception {
return new OkHttpClient.Builder()
//设置服务端的读取超时时间
.readTimeout(5000, TimeUnit.SECONDS)
//连接超时
.connectTimeout(5000, TimeUnit.SECONDS)
.build();
}
/**
* 返回对象的类型
* @return
*/
@Override
public Class<?> getObjectType() {
return OkHttpClient.class;
}
/**
* 这个对象是否是单例的
* @return
*/
@Override
public boolean isSingleton() {
return true;
}
}
然后在 XML 文件中注册即可:
<bean class="com.qfedu.demo.OkHttpClientFactoryBean" id="client03"/>
默认情况下,注册到 Spring 容器中的 Bean 是单例的:
<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 class="com.qfedu.demo.User" id="user"/>
beans>
如果不希望这个 Bean 是单例的,那么可以通过如下方式进行修改:
<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 class="com.qfedu.demo.User" id="user" scope="prototype"/>
beans>
scope 有五种取值:
也可以通过 Java 代码配置 scope,通过 @Scope 注解,可以指定一个 Bean 的作用域:
@Configuration
public class JavaConfig {
@Bean
@Scope("prototype")
User user() {
return new User();
}
}
主要是两个方法:
public class User {
private String username;
private String address;
public User() {
System.out.println("构造方法。。。");
}
/**
* 假设这个方法用来初始化当前 Bean
*/
public void init() {
System.out.println("初始化方法。。。");
}
/**
* 当当前 Bean 销毁的时候,可以在当前方法中做一些资源回收操作
*/
public void destroy() {
System.out.println("销毁方法。。。");
}
}
如果在 Spring 容器完成注册之后,还想要做一些配置,那么可以在 init 方法中完成,当 Spring 容器销毁的时候,可以在 destroy 方法中完成一些资源回收操作。
<bean class="com.qfedu.demo.User" init-method="init" destroy-method="destroy" id="user"/>
控制反转,指的是对象的控制权反转。本来,UserService 需要一个 UserDao,那么直接在 UserSerivce 中 new 一个 UserDao,那么此时 UserDao 的对象的控制权就在 UserService 中;有了 Spring 之后,系统启动的时候,UserDao 会将自己的各种信息告诉 Spring 容器,Spring 利用这些信息,就可以结合反射创建一个 UserDao 对象,此时 UserDao 对象处于 Spring 容器中,这个对象的控制权在 Spring 容器中,对象的控制权从 UserService 中转移到了 Spring 容器中,就是控制(权)反正。利用 IoC 可以实现对象之间的解耦。