Spring
1. Spring框架的作用
Spring框架的主要作用是创建对象和管理对象。
创建对象:类似于User user = new User();
管理对象:随时可以通过Spring框架获取对象,甚至Spring框架还能够帮我们为对象的属性进行赋值等。
2. 通过Spring框架创建对象,并获取对象
在Eclipse中创建Maven Project,在创建过程中勾上Create a simple project,Group Id填为cn.tedu
,Artifact Id填为spring01
。
如果某个项目不会被其它项目所使用,只需要保证自身能独立运行,其实Group Id和Artifact Id的值是多少,并不重要。
创建成功后,先在pom.xml中添加以上配置:
<properties>
<java.version>1.8</java.version>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
</properties>
然后,对项目名称点击鼠标右键,选择Maven > Update Project以更新Maven,则当前环境会使用Java 1.8。
接下来,需要在pom.xml中添加Spring框架所需的spring-context
依赖的代码:
<dependencies>
<!-- https://mvnrepository.com/artifact/org.springframework/spring-context -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.2.6.RELEASE</version>
</dependency>
</dependencies>
如果需要Spring管理某个类的对象,可以通过自定义的类进行配置!
例如存在需求:使得Spring框架管理Date
类的对象!
先在项目的cn.tedu.spring
包中创建Beans
类,并且,在这个类中自定义方法,方法的返回值类型必须是Date
,然后,自行在方法体中创建出Date
对象并返回即可:
package cn.tedu.spring;
import java.util.Date;
import org.springframework.context.annotation.Bean;
public class Beans {
@Bean
public Date aaa() {
return new Date();
}
}
以上使用的包名是自定义,并不是强制要求。
以上使用的类名是自定义,并不是强制要求。
关于以上自定义的方法:
- 应该使用
public
权限; - 返回值类型是需要Spring管理的对象所归属的类型;
- 方法名称可以自定义;
- 参数列表暂时为空;
- 必须添加
@Bean
注解。
然后,就可以创建一个用于运行的类,加载以上类,并获取对象:
package cn.tedu.spring;
import java.util.Date;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
public class Demo {
public static void main(String[] args) {
// 1. 加载配置类,获取Spring容器
AnnotationConfigApplicationContext ac
= new AnnotationConfigApplicationContext(Beans.class);
// 2. 从Spring容器中获取对象
Date date = (Date) ac.getBean("aaa"); // getBean()方法的参数,就是配置对象的方法的名称
// 3. 测试
System.out.println(date);
// 4. 关闭
ac.close();
}
}
运行以上代码,就可以看到输出的对象!
在以上Beans
类中,配置对象的方法名是aaa
,在运行的Demo
类中,调用getBean()
方法的参数也必须是"aaa"
,名称必须保持一致!
通过,获取某个值的方法的名称都应该是getXxx
,所以,以上Beans
类中的方法应该改为:
@Bean
public Date getDate() {
return new Date();
}
但是,一旦将方法名改成了getDate
,运行时,调用的getBean()
方法的参数也应该改成getDate
,例如改成:
Date date = (Date) ac.getBean("getDate");
很显然,这个getDate
表示的是一个名称,而名称应该使用名词,而不应该使用动词作为前缀!
如果要解决这个问题,可以在@Bean
注解中配置参数,例如:
@Bean("date")
public Date getDate() {
return new Date();
}
后续,在运行时,调用getBean()
方法的参数就是@Bean
注解中配置的值,例如:
Date date = (Date) ac.getBean("date");
小结:在运行时,调用的getBean()
方法的参数值,默认情况下,是配置对象的方法的名称,如果配置对象的方法的@Bean
注解添加了参数,则getBean()
方法的参数就是@Bean
注解中的参数!
其实,Spring推荐直接使用名词作为以上配置对象的方法名称,即推荐使用date
作为方法名,而不是getDate
作为方法名!该名词将用于getBean()
方法的参数,毕竟这个方法定义好了以后,是由Spring框架去调用了,开发人员不需要自行调用该方法,为了简化代码的同时还保证getBean()
代码的语义,推荐使用名词作为方法名称!
3. 由Spring管理的对象的作用域
由Spring管理的对象,在默认情况下,都是单例的!如果在配置对象的方法之前,补充添加@Scope
注解,且注解参数配置为prototype
时,就不是单例的了!例如:
@Bean
@Scope("prototype")
public User user() {
return new User();
}
注意:Spring与单例模式是两个不同的概念!
当需要同时使用多个注解时,各注解不区分先后顺序。
由Spring管理的对象,在单例的情况下,默认是饿汉式的!如果希望调整为懒汉式的,则在配置对象方法的方法之前补充添加@Lazy
注解即可:
@Bean
@Lazy
public User user() {
return new User();
}
4. 由Spring管理的对象的生命周期
生命周期:某个对象从创建到最终销毁会经历的历程!
通常,需要讨论生命周期时,对应的数据类型的对象都不是由开发人员自行维护的!
被容器维护的对象,都是由容器创建对象,并在适当的时候调用其中的某些方法的!而开发人员需要做的就是“确定满足某条件的时候应该执行什么任务”!也就是说,“容器决定什么时候执行,开发人员决定执行时做什么”。
学习生命周期的意义就是“知道在什么时候需要做什么事情”!
以Servlet
为例,其生命周期中会有几个特殊的方法,在特定的情况下会被调用,这些就称之为生命周期方法:
init()
:当Servlet
被创建对象后,立即执行,且只执行1次,该方法适合编写一些初始化相关的代码;service()
:当接收到匹配的请求后会被调用,接收到多少次请求,就执行多少次该方法;destroy()
:当Servlet
对象即将被销毁之前执行,且只执行1次,该方法适合编写一些与结束相关的代码,例如关闭等等。
当使用Spring框架后,某些类的对象交给Spring框架来管理了,那么,这些对象在什么时候被创建、什么时候被销毁,及创建过程和销毁过程中需要执行某些代码,对于开发人员来说,一定程度上是不可控的!
Spring框架允许用户在类中自定义最多2个方法,分别表示“初始化方法”和“销毁方法”,并且,Spring框架会在创建对象之后自动调用初始化方法,会在销毁对象之前调用销毁方法!关于方法的定义:
- 访问权限:应该使用
public
权限; - 返回值类型:使用
void
; - 方法名称:自定义;
- 参数列表:空。
例如,在User
类中自定义2个生命周期方法:
public void init() {
System.out.println("User.init()");
}
public void destroy() {
System.out.println("User.destroy()");
}
然后,在配置对象的方法之前的@Bean
注解中配置这2个方法作为生命周期方法:
@Bean(initMethod="init", destroyMethod="destroy")
public User user() {
return new User();
}
5. 通过组件扫描使得Spring管理类的对象
假设存在User
类,需要被Spring框架创建并管理对象,则,必须先明确User
类所在的包,然后,在用于运行的类Demo
中,在AnnotationConfigApplicationContext
的构造方法中,将包名作为构造方法的参数,例如:
AnnotationConfigApplicationContext ac
= new AnnotationConfigApplicationContext("cn.tedu.spring");
以上代码则表示“组件扫描”,当执行时,Spring框架会扫描指定的包中所有的内容,并且,自动创建各组件的对象并进行管理!
当然,并不是所有的类都是“组件”,如果要标识某个类是“组件”,必须在类的声明之前添加@Component
注解!例如:
package cn.tedu.spring;
import org.springframework.stereotype.Component;
@Component
public class User {
}
后续,从Spring容器中获取对象时,默认情况下,将类名的首字母改为小写,作为Bean的名称,用于调用getBean()
方法的参数,例如:
User user = ac.getBean("user", User.class);
关于Bean的名称,如果类名首字母是大写,且第2个字母是小写,例如
User
或Student
,则Bean的名称就是将首字母改为小写即可,也就是user
或student
!如果不满足该条件,例如类名是teacher
或IUserDao
,则Bean的名称就是类的名称!
当然,Spring框架也允许自定义Bean的名称,只要将自定义的名称配置在@Component
注解参数中即可,例如:
package cn.tedu.spring;
import org.springframework.stereotype.Component;
@Component("stu")
public class Student {
}
则以上类的对象被Spring管理时,Bean的名称就是"stu"
,后续,调用getBean()
方法获取对象时,也必须使用这个名称来获取对象:
Student stu = ac.getBean("stu", Student.class);
【附】1. 单例模式
单例模式,是设计模式中的一种。
单例模式,其特点是:被设计为单例的类型,在同一时间内,该类型的对象只会存在1个!
假设存在King
类:
public class King {}
作为一个普通的类,是可以在类的外部随意创建对象的,例如:
King k1 = new King();
King k2 = new King();
King k3 = new King();
因为在King
类中,没有声明构造方法,则编译器会自动的添加默认构造方法,也就是公有的、无参数的构造方法,所以,在类的外部才可以随意创建对象!以上代码中就创建了3个King
类型的对象!
如果要实现单例,首先,就不能允许随意创建对象,可以显式的添加构造方法,并将其私有化,避免外部随意访问,例如:
public class King {
private King() {}
}
一旦显式的添加了构造方法,编译器就不会再自动添加构造方法了,则在类的外部,将不可以再执行King k1 = new King();
这类的代码了!
使用了私有的构造方法,并不影响在类的内部使用该构造方法,为了保证在类的外部依然可以获取类的对象,则可以:
public class King {
private King() {}
public King getInstance() {
return new King();
}
}
同时,为了保证“多次获取时,获取到的都是同一个对象”,则不能反复在方法中创建对象,而是改为:
public class King {
private King king = new King();
private King() {}
public King getInstance() {
return king;
}
}
改为这样以后,无论执行多少次getInstance()
方法,该方法返回的都是同一个king
变量,而该变量的值只是在类被加载时赋值了1次而已,就实现了单例的效果!
当然,以上代码是矛盾的!因为,如果要调用getInstance()
方法,必须先有King
类型的对象,而得到King
类型对象的唯一途径就是调用getInstance()
方法!也就是说:不调用getInstance()
方法就无法得到对象,但是,调用方法之前又必须有对象!
为了解决这个问题,可以为getInstance()
方法添加static
修饰符,则通过类名.方法名()
的格式就可以调用方法了,不需要事先获取对象!同时,基于“被static
修饰的成员不可以直接访问没被static
修饰的成员”的原则,所以,全局的private King king = new King();
也需要添加static
修饰符:
public class King {
private static King king = new King();
private King() {}
public static King getInstance() {
return king;
}
}
至此,简单的单例模式代码就完成了,后续,需要获取King
类型的对象时,只能通过King.getInstance()
得到对象,而该方法每次返回的都是同一个对象!
严格来说,以上单例模式的设计是“饿汉式”的,可以看到,在类被加载时,就直接创建了King
类的对象,此时,也许并不需要获取对象,但是,也创建好了 ,后续,当需要对象时,直接获取即可!另外,还有“懒汉式”的单例模式,其特点就是“不到逼不得已,不创建对象”!其基础代码例如:
public class King {
private static King king;
private King() {}
public static King getInstance() {
if (king == null) {
king = new King();
}
return king;
}
}
以上代码是多线程不安全的!为了解决这个问题,可以将以上代码块添加互斥锁,例如:
public class King {
private static King king;
private King() {}
public static King getInstance() {
synchronized("java") {
if (king == null) {
king = new King();
}
}
return king;
}
}
一旦添加了锁,就会导致每次调用getInstance()
方法时都会先锁定代码再执行,效率偏低,为了解决该问题,还可以:
public class King {
private static King king;
private King() {}
public static King getInstance() {
if (king == null) { // 判断是否有必须加锁,如果没有该判断,则效率可能偏低
synchronized("java") {
if (king == null) { // 判断是否有必须创建对象,如果没有该判断,则可能创建多个对象
king = new King();
}
}
}
return king;
}
}
以上,就是完整的懒汉式的单例模式!
小结:单例模式的特点就是“单一实例”,表现为“同一时间内,某个类的对象只有1个”!单例模式分为“饿汉式”和“懒汉式”这2种,前者是“早早的创建出对象,随时可以获取”,后者是“不到逼不得已不会创建对象”!
6. 组件扫描
首先,必须让Spring扫描组件所在的包,并且,组件类的声明之前必须添加@Component
注解!
其实,除了@Component
注解以外,还可以使用以下注解实现同样的效果:
@Controller
:推荐添加在控制器类之前;@Service
:推荐添加在业务类之前;@Repository
:推荐添加在处理持久层的类之前.
以上4个注解在Spring框架的作用领域中,效果是完全相同的,用法也完全相同,只是语义不同。
在使用组件扫描时,还可以自定义某个类,作为配置类,在这个类的声明之前使用@ComponentScan
注解来配置组件扫描的包:
package cn.tedu.spring;
import org.springframework.context.annotation.ComponentScan;
@ComponentScan("cn.tedu.spring")
public class SpringConfig {
}
后续,程序运行时,就需要加载这个配置类:
AnnotationConfigApplicationContext ac
= new AnnotationConfigApplicationContext(SpringConfig.class);
关于组件扫描的包,严格来说,是配置需要被扫描的“根包(base package)”,也就是说,在执行扫描时,会扫描所设置的包及其所有子孙包中的所有组件类!当设置为扫描cn.tedu
包时,会把cn.tedu.spring
甚至cn.tedu.spring.dao
这些包中的组件类都扫描到!
7. 关于注解的使用
以@Bean
注解为例,其声明是:
@Target({ElementType.METHOD, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Bean {
}
可以看到,注解都是通过@interface
声明的!
在注解的声明之前,还添加了一系列的注解,例如以上的@Target({ElementType.METHOD, ElementType.ANNOTATION_TYPE})
、@Retention(RetentionPolicy.RUNTIME)
、@Documented
,则表示当前@Bean
注解同时具有以上3个注解的特性。也就是说,@Bean
注解相当于以上3个注解的同时,还具有自身的特性!
在@Bean
注解内部,还有:
/**
* Alias for {@link #name}.
* <p>Intended to be used when no other attributes are needed, for example:
* {@code @Bean("customBeanName")}.
* @since 4.3.3
* @see #name
*/
@AliasFor("name")
String[] value() default {};
以上String[] value() default {};
有点像接口中的抽象方法,但是,在注解中,这是声明的注解属性!value
是属性名称,所以,在使用当前注解时,可以配置:
@Bean(value=???)
以上源代码中的String[]
看似是抽象方法的返回值,实则是value
属性的值的数值类型!所以,可以配置为:
@Bean(value={"a", "b", "c"})
以上源代码中的default {}
表示该属性的默认值,所以,以下2段配置是完全等效的:
@Bean
@Bean(value={})
在配置注解属性时,如果属性名称是value
,它是默认的属性,在配置时,可以不用显式的写出value=
部分,也就是说,以下2段配置是完全等效的:
@Bean(value={"a", "b", "c"})
@Bean({"a", "b", "c"})
在配置注解属性时,如果属性的值的类型是数组类型,但是,当前只需要配置1个值时,可以不用写成数组格式,只需要写成数组元素的格式即可!也就是说,以下2段配置是完全等效的:
@Bean({"a"})
@Bean("a")
所以,总的来说,关于@Bean
注解的value
属性,如果需要配置的值是"user"
,则以下4段代码都是完全等效的:
@Bean("user")
@Bean({"user"})
@Bean(value="user")
@Bean(value={"user"})
在以上源代码中,注释中还标明了@since 4.3.3
,表示该属性从Spring框架4.3.3版本开始才加入的,如果当前使用的环境改为4.3.3以下的版本,将导致该属性不可用,因为在更低的版本中,根本就没有这个属性,甚至可能连个注解本身都不存在!
在以上源代码中,在value
属性的声明之前还添加了@AliasFor("name")
注解,表示当前value
属性另有别名为name
,所以,在@Bean
注解的源代码中,还有:
/**
* The name of this bean, or if several names, a primary bean name plus aliases.
* <p>If left unspecified, the name of the bean is the name of the annotated method.
* If specified, the method name is ignored.
* <p>The bean name and aliases may also be configured via the {@link #value}
* attribute if no other attributes are declared.
* @see #value
*/
@AliasFor("value")
String[] name() default {};
则在@Bean
注解中,name
和value
这2个注解是完全等效的!
之所以存在2个完全等效的属性,是因为:
value
属性是默认的,在配置时可以不必显式的写出value=
部分,配置时更加简单;name
属性表现的语义更好,更易于根据源代码读懂程序的意思,在其它注解中,也可能存在与value
等效的属性。
需要注意的是:在配置注解中的属性时,如果需要配置的是value
属性的值,可以不用显式的写出value=
部分,前提是当前注解只配置value
这1个属性!如果需要配置多个属性,则必须写出每一个属性名,例如:
@Bean(value="user", initMethod="init")
而不能写成:
@Bean("user", initMethod="init") // 错误
8. 使用组件扫描后配置作用域与生命周期
在类的声明之前,添加@Scope("prototype")
即可将当前类配置为“非单例”的对象!例如:
package cn.tedu.spring;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Repository;
@Repository
@Scope("prototype")
public class User {
}
在单例的情况下,在类的声明之前添加@Lazy
注解,就可以将对象配置为“懒加载”的模式:
package cn.tedu.spring;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Repository;
@Repository
@Lazy
public class User {
}
如果需要配置当前类中的生命周期的处理,首先,还是需要在类中自定义2个方法,分别表示“初始化方法”和“销毁方法”,然后,在初始化方法之前添加@PostConstruct
注解,在销毁方法之前添加@PreDestroy
注解,例如:
package cn.tedu.spring;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Repository;
@Repository
@Lazy
public class User {
public User() {
System.out.println("User.User()");
}
@PostConstruct
public void init() {
System.out.println("User.init()");
}
@PreDestroy
public void destroy() {
System.out.println("User.destroy()");
}
}
注意:以上2个注解并不是Spring的注解,如果JRE环境版本太低,将无法识别以上2个注解,需要调整当前项目的JRE环境!
9. 关于Spring管理对象的小结
如果需要Spring管理某个类的对象,可以:
- 自定义方法,将方法的返回值类型设置为期望管理的类型,并在方法中返回匹配类型的对象,最后,在方法的声明之前添加
@Bean
注解; - 设置组件扫描的包,并在类的声明之前添加
@Component
/@Controller
/@Service
/@Repository
这4个注解中的某1个。
在实际使用时,大多采取第2种做法,但是,如果需要Spring管理的类并不是自定义的类,就只能采取第1种做法!
10. 关于Spring的解耦
在没有使用Spring框架的情况下,在项目中,各组件之间是存在依赖关系的,例如:
// 处理用户登录请求的Servlet组件类
public class UserLoginServlet {
private UserJdbcDao userDao = new UserJdbcDao();
public void doPost() {
userDao.login();
}
}
// 处理用户数据增删改查的组件
public class UserJdbcDao {
public void login() {
// 通过JDBC技术实现数据查询,判断用户名与密码是否正确
}
}
以上代码就体现了类与类之前的依赖关系,具体表现就是UserLoginServlet
是依赖于UserJdbcDao
的!
如果直接依赖于某个类,将会导致耦合度过高的问题!
假设在UserJdbcDao
中,是通过原生的JDBC技术实现数据访问的,后续,需要改为使用MyBatis框架技术来实现,则可能创建UserMybatisDao
类,用于取代UserJdbcDao
类!
如果需要替换,则项目中原有的以下代码:
private UserJdbcDao userDao = new UserJdbcDao();
全部需要替换为:
private UserMybatisDao userDao = new UserMybatisDao();
这种替换时需要调整大量原有代码的问题,就是高耦合的问题,我们希望的目标是低耦合,将原有高耦合的项目调整为低耦合的状态,就是解耦的做法!
可以将处理用户数据增删改查的相关操作声明在接口中,例如:
public interface UserDao {
void login();
}
然后,各个处理用户数据增删改查的类都去实现这个接口:
public class UserJdbcDao implements UserDao {
public void login() {
// 通过JDBC实现处理用户登录
}
}
public class UserMybatisDao implements UserDao {
public void login() {
// 通过MyBatis框架技术实现处理用户登录
}
}
后续,在各个Servlet组件中,就可以声明为接口类型:
private UserDao userDao = new UserMybatisDao();
通过以上代码调整,就可以使得Servlet组件依赖于接口,而不再是依赖于类,从而实现了解耦!
另外,还可以通过设计模式中的工厂模式来生产对象,例如:
public class UserDaoFactory {
public static UserDao newInstance() {
return new UserMybatisDao();
}
}
当有了以上工厂后,原本在Servlet组件中声明持久层对象的代码就可以再调整为:
private UserDao userDao = UserDaoFactory.newInstance();
至此,在项目中到底是使用UserJdbcDao
还是使用UserMybatisDao
,在以上代码都不会体现出来了,也就意味着当需要切换/替换时,以上代码是不需要修改的,而是修改UserDaoFactory
工厂类的方法的返回值这1处即可!
所以,通过定义接口和创建工厂类就可以实现解耦,但是,在实际项目开发时,不可能为每一个组件都创建专门的工厂类,而Spring框架就可以当作是一个庞大的工厂,开发人员可以通过Spring框架的使用约定,将某些类的对象交给Spring框架进行管理,后续,在具体使用过程中,就不必自行创建对象,而是获取对象即可!
11. 自动装配
在Spring框架的应用中,可以为需要被Spring自动赋值的属性添加@Autowired
,则Spring框架会从Spring容器中找出匹配的值,并自动完成赋值!这就是Spring框架的自动装配机制!
当Spring尝试为某个属性实现自动装配时,采取的模式主要有:
byName
:根据名称实现自动装配,在这种模式下,要求被装配的属性名称,与被Spring管理的对象的名称(调用getBean()
方法给出的参数名)必须相同;byType
:根据类型实现自动装配,在这种模式,要求被装配的属性的类型,在Spring容器中存在匹配类型的对象,当应用这种机制时,必须在Spring容器中保证匹配类型的对象只有1个,否则,将会出现NoUniqueBeanDefinitionException
异常;
当使用@Autowired
尝试自动装配时,Spring框架会先根据byType
模式找出所有匹配类型的对象,如果匹配类型的对象的数量为0,也就是没有匹配类型的对象,默认情况下会直接报错,提示信息例如:
Caused by: org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean of type 'cn.tedu.spring.UserDao' available: expected at least 1 bean which qualifies as autowire candidate. Dependency annotations: {@org.springframework.beans.factory.annotation.Autowired(required=true)}
如果使用
@Autowired
时明确的配置为@Autowired(required=false)
,当没有匹配类型的对象时,也不会因为装配失败而报错!
如果匹配类型的对象的数量为1,则直接装配;
如果匹配类型的对象的数量超过1个(有2个甚至更多个),会尝试byName
来装配,如果存在名称匹配的对象,则成功装配,如果名称均不匹配,则装配失败,会提示如下错误:
Caused by: org.springframework.beans.factory.NoUniqueBeanDefinitionException: No qualifying bean of type 'cn.tedu.spring.UserDao' available: expected single matching bean but found 2: userJdbcDao,userMybatisDao
当需要自动装配时,除了使用@Autowired
注解以外,还可以使用@Resource
注解!
当使用@Resource
注解尝试自动装配时,其工作原理是先尝试byName
装配,如果存在名称匹配的对象,则直接装配,如果没有名称匹配的对象,则尝试byType
装配。
另外,如果某个方法是被Spring调用的,还可以将需要装配的对象设置为方法的参数(不需要添加注解即可正常使用),Spring也可以实现方法参数的自动装配!例如:
public void test(UserDao userDao) {}
12. 通过Spring框架读取.properties文件
首先,在案例的src/main/resources下创建jdbc.properties文件,并且,在文件中,添加一些自定义的配置信息:
url=jdbc:mysql://localhost:3306/db_name
driver=com.mysql.jdbc.Driver
username=root
password=1234
本次案例的目标是读取以上文件的信息,并不用于真实的连接某个数据库,所以,各属性的值可以不是真正使用的值!
如果要读取以上信息,可以将这些信息都读取到某个类的各个属性中去,则先创建一个类,并在类中声明4个属性(与以上jdbc.properties文件中的配置信息的数量保持一致):
package cn.tedu.spring;
public class JdbcConfig {
private String url;
private String driver;
private String username;
private String password;
}
然后,在类的声明之前,通过@PropertySource
配置需要读取的配置文件:
@PropertySource("classpath:jdbc.properties")
然后,在各个属性的声明之前,通过@Value
注解读取配置信息中的值,并注入到属性中,其基本格式是@Value("${配置文件中的属性名称}")
,例如:
package cn.tedu.spring;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.PropertySource;
@PropertySource("classpath:jdbc.properties")
public class JdbcConfig {
@Value("${url}")
private String url;
@Value("${driver}")
private String driver;
@Value("${username}")
private String username;
@Value("${password}")
private String password;
@Override
public String toString() {
return "JdbcConfig [url=" + url + ", driver=" + driver + ", username=" + username + ", password=" + password
+ "]";
}
}
由于期望的是由Spring读取配置文件,并为以上类的各个属性赋值,所以,以上JdbcConfig
应该是被Spring管理的!所以,先使用一个类来配置组件扫描:
package cn.tedu.spring;
import org.springframework.context.annotation.ComponentScan;
@ComponentScan("cn.tedu.spring")
public class SpringConfig {
}
然后,在JdbcConfig
类的声明之前添加@Component
注解即可!
注意:在Windows操作系统中,如果配置文件中的属性名是username
,则最终注入属性的值将不是配置文件中的值,而是当前登录Windows操作系统的用户名,为了避免出现此类问题,建议在配置文件中,每个属性的名称之前都添加一些自定义的前缀。
13. 通过Environment读取.properties配置文件
假设在src/main/resources下存在jdbc.properties文件,并且,在该文件中存在若干条配置信息,如果需要读取该文件中的配置信息,可以先创建某个类,在类中声明Environment
接口类型的对象,通过自动装配的方式为该类型对象注入值:
package cn.tedu.spring;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.PropertySource;
import org.springframework.core.env.Environment;
import org.springframework.stereotype.Component;
@Component
@PropertySource("classpath:jdbc.properties")
public class JdbcConfig {
@Autowired
private Environment environment;
public Environment getEnvironment() {
return environment;
}
public void setEnvironment(Environment environment) {
this.environment = environment;
}
}
后续,需要读取配置文件中的值时,从以上类中获取Environment
类型的对象,然后,调用该对象的getProperty()
方法即可获取对应的值,例如:
package cn.tedu.spring;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.core.env.Environment;
public class Demo {
public static void main(String[] args) {
AnnotationConfigApplicationContext ac
= new AnnotationConfigApplicationContext(SpringConfig.class);
JdbcConfig jdbcConfig = ac.getBean("jdbcConfig", JdbcConfig.class);
Environment environment = jdbcConfig.getEnvironment();
System.out.println(environment.getProperty("db.url"));
System.out.println(environment.getProperty("db.driver"));
System.out.println(environment.getProperty("db.username"));
System.out.println(environment.getProperty("db.password"));
ac.close();
}
}
14.Spring阶段小结
-
【理解】Spring框架的主要作用:创建对象,管理对象;
-
【理解】Spring框架深层的作用:解耦;
-
【掌握】通过Spring框架创建对象:
- 在某个类中自定义方法,方法的返回值类型就是需要Spring框架创建对象的类型,在方法体中自行返回所需要创建的对象,并且为方法添加
@Bean
注解,后续,只要Spring框架加载这个类,就会自动调用被添加了@Bean
注解的方法,并管理方法所返回的对象。通常,当需要管理的对象所归属的类不是自定义的,必须使用这种做法; - 使得Spring框架执行组件扫描,保证相关的类在组件扫描的包或其子孙包中,并且,类还需要添加
@Component
/@Controller
/@Service
/@Repository
注解中的某1个。通常,当需要管理的对象所归属的类是自定义的,优先使用这种做法。
- 在某个类中自定义方法,方法的返回值类型就是需要Spring框架创建对象的类型,在方法体中自行返回所需要创建的对象,并且为方法添加
-
【掌握】关于组件扫描:可以将组件扫描的包的名称作为字符串参数直接应用于
AnnotationConfigApplicationContext
类的构造方法中,但是,并不推荐使用这种做法,在实际项目开发时,AnnotationConfigApplicationContext
类通常不是开发人员自行创建的,也就没有办法在构造方法中添加包的名称!推荐使用自定义的类作为配置类,并结合@ComponentScan
注解来配置组件扫描; -
【理解】被Spring管理的对象默认是单例的(注意:Spring框架不是设计模式中的单例模式,只是管理对象的方法是这样的),并且,不是懒加载的模式(相当于单例模式中的饿汉式单例的效果);
-
【了解】使用
@Scope
和@Lazy
调整被Spring管理的对象的作用域; -
【理解】被Spring管理的对象的生命周期;
-
【了解】配置对象的生命周期:
- 如果使用的是添加了
@Bean
注解的自定义方法返回对象的做法,在@Bean
注解中配置initMethod
和destroyMethod
属性,就可以将类中的方法分别指定为初始化方法和销毁方法; - 如果使用的是组件扫描和组件注解的做法,在类中的初始化方法之前添加
@PostConstruct
注解,在销毁方法之前添加@PreDestroy
方法。
- 如果使用的是添加了
-
【理解】关于Spring框架的DI与IoC:
- DI:Dependency Injection,依赖注入,具体的表现就是“为当前类对象所依赖的某个属性注入值”;
- IoC:Inversion of Control:控制反转,在传统模式下,是由开发人员自行创建对象(例如
User user = new User();
)且管理对象(例如user.setName("Jack");
),可以理解为开发人员具有对象的控制权,当使用了Spring框架后,创建对象和管理对象的权力就交给了框架。 - 在Spring框架中,DI是一种做法,IoC是最终实现的效果,也就是“Spring框架通过DI这种做法实现了IoC的效果”。
-
【理解】Spring框架自动装配机制的2种装配模式:
byName
:根据名称实现自动装配,在这种模式下,要求被装配的属性名称,与被Spring管理的对象的名称(调用getBean()
方法给出的参数名)必须相同;byType
:根据类型实现自动装配,在这种模式,要求被装配的属性的类型,在Spring容器中存在匹配类型的对象,当应用这种机制时,必须在Spring容器中保证匹配类型的对象只有1个,否则,将会出现NoUniqueBeanDefinitionException
异常;
-
【理解】使用
@Autowired
和@Resource
这2个注解实现自动装配时的区别:- 使用
@Autowired
尝试自动装配时,Spring框架会先根据byType
模式找出所有匹配类型的对象,如果匹配类型的对象的数量为0,也就是没有匹配类型的对象,默认情况下会直接报错(如果明确的配置为@Autowired(required=false)
时不会因为装配失败而出错);如果匹配类型的对象的数量为1,则直接装配;如果匹配类型的对象的数量超过1个(有2个甚至更多个),会尝试byName
来装配,如果存在名称匹配的对象,则成功装配,如果名称均不匹配,则装配失败。 - 使用
@Resource
注解尝试自动装配时,其工作原理是先尝试byName
装配,如果存在名称匹配的对象,则直接装配,如果没有名称匹配的对象,则尝试byType
装配。
- 使用
-
【掌握】通过Spring框架读取**.properties**配置文件中的信息:
- 在组件类的声明之前添加
@PropertySource
注解,以配置需要读取的**.properties**文件的位置,然后,通过@Value
注解将读取到的属性值一一注入到类的属性中,在声明类的属性时,只要该属性的类型是常规类型(例如基本数据类型、String
等)或Spring框架自定义的类型,都可以将类的属性直接声明为所期望的类型; - 在组件类的声明之前添加
@PropertySource
注解,以配置需要读取的**.properties文件的位置,然后,在类中自定义Environment
接口类型的对象,通过@Autowired
为该对象自动装配值,后续,调用该对象的getProperty()
方法即可获取在.properties**文件中配置的属性值。
- 在组件类的声明之前添加