IOC的核心就是DI,Spring提供简单的机制来管理组件的依赖项,并在组件整个生命期都能进行管理,Spring主要提供了两种注入方式。
当在组件的构造函数中提供依赖项时,就发生了构造函数注入。这与调用具有特有参数的静态工厂方法创建Bean的实例一致(可参考上一篇文章内容)。代码如下所示:
IOC 容器可通过JavaBean的setter方法注入组件的依赖,下面演示使用setter方法注入依赖。
下面演示一个典型的构造器注入案例。
/**
* 消息服务接口
*/
public interface MessageService {
String getMessage();
}
/**
* 消息服务实现
*/
public class MessageServiceImpl implements MessageService {
private String name;
private int age;
public MessageServiceImpl(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public String getMessage() {
return "this is a message comes from a "+age+" year's boy whose name is "+name;
}
}
public class MessageSender {
private MessageService messageService;
/**
* 构造器注入
* @param messageService messageService
*/
public MessageSender(MessageService messageService) {
this.messageService = messageService;
}
public void printMessage() {
System.out.println(messageService.getMessage());
}
}
<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="messageService" class="com.codegeek.day2.MessageServiceImpl">
<constructor-arg name="name" value="张三"/>
<constructor-arg name="age" value="23"/>
bean>
<bean id="messageSender" class="com.codegeek.day2.MessageSender">
<constructor-arg name="messageService" ref="messageService"/>
bean>
beans>
public class SpringTest {
public static void main(String[] args) {
ClassPathXmlApplicationContext classPathXmlApplicationContext = new ClassPathXmlApplicationContext("day2/applicationContext.xml");
MessageSender messageSender = classPathXmlApplicationContext.getBean("messageSender", MessageSender.class);
messageSender.printMessage();
}
}
下面以开发中常常遇见的数据库数据源配置为例,如下图所示一个常见的数据源配置
其中property属性就是类的属性名称,而value就是为其进行赋值,可以看到PropertyPlaceholderConfigurer
其继承树图父类 PropertiesLoaderSupport
在这个类中有PropertiesLoaderSupport
中 setLocation方法。
如果Bean之间有协作方式则可以引用Bean,ref 元素可以是constructor-arg
标签也可以是Property。
Java集合类型中如List、Set、Map、Properties 可以使用提供的、
XML中元数据配置信息如下所示:
<bean id="messageSender1" class="com.codegeek.day2.MessageSender">
<property name="list">
<list>
<value>我是messageSender的list数据value>
list>
property>
bean>
测试类代码如下:
@Test
public void test() {
MessageSender messageSender = applicationContext.getBean("messageSender1", MessageSender.class);
System.out.println(messageSender.getList());
}
XML中元数据配置信息如下所示:
<bean id="messageSender2" class="com.codegeek.day2.MessageSender">
<property name="set">
<set>
<value>我是messageSender的set数据1value>
<value>我是messageSender的set数据2value>
set>
property>
bean>
测试类代码如下:
@Test
public void testSet() {
MessageSender messageSender = applicationContext.getBean("messageSender2", MessageSender.class);
System.out.println(messageSender.getSet());
}
XML中元数据配置信息如下所示:
<bean id="student" class="com.codegeek.day1.Student">
<property name="name" value="张三"/>
<property name="age" value="23"/>
<property name="gender" value="男"/>
<property name="address" value="中国"/>
bean>
<bean id="iphone11" class="com.codegeek.day1.Phone" factory-method="instance">
<constructor-arg name="brandName" value="iphone 11"/>
<constructor-arg name="price" value="5999"/>
<constructor-arg name="producePlace" value="china"/>
bean>
<bean id="messageSender3" class="com.codegeek.day2.MessageSender">
<property name="map">
<map>
<entry key="小学生" value="小明"/>
<entry key="china">
<map>
<entry key="hubei" value="wuhan~加油"/>
map>
entry>
<entry key="student" value-ref="iphone11"/>
map>
property>
bean>
测试类代码如下:
@Test
public void testMap() {
MessageSender messageSender = applicationContext.getBean("messageSender3", MessageSender.class);
System.out.println(messageSender.getMap());
}
XML中元数据配置信息如下所示:
<bean id="messageSender4" class="com.codegeek.day2.MessageSender">
<property name="properties">
<props>
<prop key="driver">com.mysql.jdbc.Driverprop>
props>
property>
bean>
测试类代码如下:
@Test
public void testProps() {
MessageSender messageSender = applicationContext.getBean("messageSender4", MessageSender.class);
System.out.println(messageSender.getProperties());
}
运行结果:
还有一种特色的集合类型设置即util名称创建空间集合,如使用util:list 如下所示:
<bean id="messageSender1" class="com.codegeek.day2.MessageSender">
<property name="list" ref="list1">
property>
bean>
<util:list id="list1">
<value>123value>
<value>456value>
util:list>
再次运行testList方法运行结果如下:
其他类型集合与上面类似,这里就不在一一演示,其常见简单配置如下所示:
开发中可以使用P命名空间简化类property
标签赋值,XML配置如下所示:
需要注意的是需要导入P命名空间,利用Idea智能提示可以自动添加P命名空间。
与P命名空间类似,开发可以使用C标签替换constructor-arg
,xml配置信息如下所示:
开发中常常会复用父类的属性或方法,此时如果希望子类继承父类的一些配置数据就可以在子Bean中定义parent属性,该属性指向父Bean,Xml配置如下所示:
声明一个抽象父类Person,然后User继承自Person
@Data
@AllArgsConstructor
@NoArgsConstructor
public abstract class Person {
private String userName;
private String email;
private Integer age;
}
@NoArgsConstructor
@Getter
@Setter
public class User extends Person {
public User(String userName, String email, Integer age) {
this.setUserName(userName);
this.setEmail(email);
this.setAge(age);
}
@Override
public String toString() {
return new StringJoiner(", ", User.class.getSimpleName() + "[", "]")
.add("userName='" + this.getUserName() + "'")
.add("email='" + this.getEmail() + "'")
.add("age=" + this.getAge())
.toString();
}
}
测试类如下:
运行结果可以看出,User2对象继承了来自Person的属性。
默认情况下位于IOC容器的Bean都是单例的,我们可以简单测试证明一下。
Bean的scope属性可用值如下所示:
范围 | 说明 |
---|---|
singleton | 默认值,IOC容器只会有一个实例 |
prototype | IOC容器会重复创建该类型Bean实例 |
request | web环境一次请求对应一个实例 |
session | web环境下一次会话对应一个实例 |
application | 定义IOC容器一个实例 |
websocket | web环境下一次webSocket对应一个实例 |
下面在演示一下prototype的实例,其他属性值设置不常见就不在演示了。
将user2的scope设置为prototype如下所示:
再次运行测试类代码发现结果为false。
一般情况下当IOC容器启动时就会自动实例化所有的单例对象,如果想取消IOC容器这个行为可以设置懒加载解决:即当要使用这个类在进行初始化。
如果上面的Bean设置了懒加载那么启动容器将不会创建该Bean,新增LazyBean对象:
@AllArgsConstructor
@NoArgsConstructor
@Getter
@ToString
public class LazyBean {
private Boolean flag;
public void setFlag(Boolean flag) {
System.out.println("设置了flag:"+flag);
this.flag = flag;
}
}
启动IOC容器如下所示:
需要注意的是当延迟加载Bean作为非延迟加载对象的引用时,延迟加载的对象会跟随IOC容器启动而创建,不再进行懒加载。
新增非延迟加载对象ReferenceBean
测试代码运行如下:
开发过程中可以使用三种方法来完成自定义Bean生命周期,可以使用 Spring提供的接口 InitializingBean
与 DisposableBean
或者使用JSR-250提供的两个注解 @PostConstruct
与 @preDestroy
,或者还可以使用Spring的 init-method
与 destroy-method
。
InitializingBean
与 DisposableBean
Spring的两个接口 InitializingBean
与 DisposableBean
实现了容器Bean的创建与销毁。开发中若实现了这两个接口方法,那么容器将会自动调用 afterPropertiesSet()
与 destroy()
以管理我们Bean的生命周期,实现如下所示:
@Data
public class Account {
private String userId;
private String accountId;
private BigDecimal accountBalance;
private static final BigDecimal BIG_DECIMAL = new BigDecimal(10);
public void addBalance(BigDecimal balance) {
if (this.accountBalance.compareTo(new BigDecimal(10000)) < -1) {
this.accountBalance = this.accountBalance.add(balance);
System.out.println("用户:" + userId + " 账号:" + accountId + "入账" + balance + "元");
}
}
public void reduceBalance(BigDecimal balance) {
if (this.accountBalance.compareTo(new BigDecimal(10000)) > -1) {
this.accountBalance = this.accountBalance.subtract(balance);
System.out.println("用户:" + userId + " 账号:" + accountId + "扣款" + balance + "元");
}
}
}
public class BeanLife implements InitializingBean, DisposableBean {
@Autowired
private Account account;
/**
* bean销毁的时候可执行的操作
*
* @throws Exception
*/
@Override
public void destroy() throws Exception {
System.out.println("销毁的操作 .. 执行扣钱的操作");
this.execute(account, "reduce");
}
/***
* 初始化对象可执行的操作
* @throws Exception
*/
@Override
public void afterPropertiesSet() throws Exception {
System.out.println("初始化对象的操作.....执行账号加钱操作");
this.execute(account, "add");
}
private void execute(Account account, String method) {
System.out.println("-----执行excute--------"+method);
Assert.state(account != null, "account must not empty!");
if ("add".equals(method)) {
account.addBalance(new BigDecimal(50001));
} else if ("reduce".equals(method)) {
account.reduceBalance(new BigDecimal(49999));
}
}
}
@PostConstruct
与 @preDestroy
注解使用注解的方式要比直接实现Bean接口方式要好,在一个类中标注 @PostConstruct
与 @preDestroy
两个注解,下面是这种方式的演示:
PostConstructDemo
类public class PostConstructDemo {
@PostConstruct
public void init() {
System.out.println("---我是postConstruct注解的init方法");
}
@PreDestroy
public void preDestroy() {
System.out.println("---我是preDestroy注解的destroy方法");
}
}
init-method
与 destroy-method
方法可以在XML元数据配置文件中使用Spring提供的 init-method
与 destroy-method
方法完成Bean创建与销毁的一些工作。
SpringInitDemo
类public class SpringInitDemo {
public void SpringInit() {
System.out.println("---我是spring调用的init方法");
}
public void SpringDestroy() {
System.out.println("---我是spring调用的destroy方法");
}
}
将上面三种控制Bean生命周期方法一起如下所示:
BeanLife
类public class BeanLife implements InitializingBean, DisposableBean {
@Autowired
private Account account;
/**
* bean销毁的时候可执行的操作
*
* @throws Exception
*/
@Override
public void destroy() throws Exception {
System.out.println("DisposableBean.destroy() 销毁的操作 .. 执行扣钱的操作");
this.execute(account, "reduce");
}
/***
* 初始化对象可执行的操作
* @throws Exception
*/
@Override
public void afterPropertiesSet() throws Exception {
System.out.println("InitializingBean.afterPropertiesSet() 初始化对象的操作.....执行账号加钱操作");
this.execute(account, "add");
}
@PostConstruct
public void init() {
System.out.println("---我是postConstruct注解的init方法");
}
@PreDestroy
public void preDestroy() {
System.out.println("---我是preDestroy注解的destroy方法");
}
public void SpringInit() {
System.out.println("---我是spring调用的init方法");
}
public void SpringDestroy() {
System.out.println("---我是spring调用的destroy方法");
}
private void execute(Account account, String method) {
// System.out.println("-----执行excute--------"+method);
Assert.state(account != null, "account must not empty!");
if ("add".equals(method)) {
account.addBalance(new BigDecimal(50001));
} else if ("reduce".equals(method)) {
account.reduceBalance(new BigDecimal(49999));
}
}
}
运行测试结果如下:
由上面运行结果可知:一个Bean如果配置了多个生命周期的方法,其执行顺序如下:
- 包含由@PostConstruct注解方法。
- 实现了InitializingBean接口的afterPropertiesSet()方法。
- 指定Spring XML的init-method属性的自定义方法。
- 包含由 @PreDestroy注解方法。
- 实现了DisposableBean接口的destroy()方法。
- 指定Spring XML的destroy-method属性的自定义方法。
ApplicationContext
开发中有时候需要在未被Spring管理Bean对象去获取Spring管理的JavaBean对象,这个功能很有用,比如调用Spring管理JavaBean对象逻辑层进行数据库的读写。而这只需我们定义一个类去实现 ApplicationContextAware
接口然后设置Spring的上下文对象 ApplicationContext
即可,下面演示是项目常用的工具类如下:
public class SpringUtils implements ApplicationContextAware {
public static ApplicationContext applicationContext;
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
if(SpringUtils.applicationContext == null) {
SpringUtils.applicationContext = applicationContext;
}
}
public static ApplicationContext getApplicationContext() {
return applicationContext;
}
//通过name获取 Bean.
public static Object getBean(String name){
return getApplicationContext().getBean(name);
}
//通过class获取Bean.
public static <T> T getBean(Class<T> clazz){
return getApplicationContext().getBean(clazz);
}
//通过name,以及Clazz返回指定的Bean
public static <T> T getBean(String name,Class<T> clazz){
return getApplicationContext().getBean(name, clazz);
}
}
BeanPostProcessor
处理器最新版本的Spring中的 BeanPostProcessor
提供的两个接口都为默认方法:
BeanPostProcessor
方法定义了回调方法,可以结合项目实现自己对Bean初始化前或销毁前所做的一些逻辑。
@Getter
@Setter
@AllArgsConstructor
public class Order {
private String orderName;
private Double price;
public Order() {
System.out.println("order被创建了");
}
public void init() {
System.out.println("order的init方法");
}
public void destroy() {
System.out.println("order的destroy方法");
}
}
public class BeanPostProcessorDemo implements BeanPostProcessor {
@Override
public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
System.out.println("postProcessBeforeInitialization1: "+beanName+"开始执行");
return bean;
}
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
System.out.println("postProcessAfterInitialization1: "+beanName+"开始执行");
return bean;
}
}
PropertyPlaceholderConfigurer
最新版本该类标记的过时,不过这个类还是比较实用的。
开发中可使用该类具体化值,也就是我们将常见数据库连接的用户名和密码进行加密这样别人看到我们的数据库连接信息是密文状态的,然后当IOC容器启动时再将其进行解密成明文然后连接数据库。
public class DESUtil {
private static Key key;
private static String KEY_STR = "myKey";
private static String CHAR_SET_NAME = "UTF-8";
private static String ALGORITHM = "DES";
static {
try {
KeyGenerator generator = KeyGenerator.getInstance(ALGORITHM);
SecureRandom secureRandom = SecureRandom.getInstance("SHA1PRNG");
secureRandom.setSeed(KEY_STR.getBytes());
generator.init(secureRandom);
key = generator.generateKey();
generator = null;
} catch (Exception e) {
throw new RuntimeException(e);
}
}
public static String getEncryptString(String str) {
Base64.Encoder encoder = Base64.getEncoder();
try {
byte[] bytes = str.getBytes(CHAR_SET_NAME);
Cipher cipher = Cipher.getInstance(ALGORITHM);
cipher.init(Cipher.ENCRYPT_MODE, key);
byte[] doFinal = cipher.doFinal(bytes);
return encoder.encodeToString(doFinal);
} catch (Exception e) {
throw new RuntimeException();
}
}
public static String getDecryptString(String str) {
Base64.Decoder decoder = Base64.getDecoder();
try {
byte[] bytes = decoder.decode(str);
Cipher cipher = Cipher.getInstance(ALGORITHM);
cipher.init(Cipher.DECRYPT_MODE, key);
byte[] doFinal = cipher.doFinal(bytes);
return new String(doFinal, CHAR_SET_NAME);
} catch (Exception e) {
throw new RuntimeException();
}
}
public static void main(String[] args) {
System.out.println(getEncryptString("123456")); //QAHlVoUc49w=
System.out.println(getEncryptString("root")); // WnplV/ietfQ=
}
}
public class EncryptPropertySourcesPlaceholderConfigurer extends
PropertyPlaceholderConfigurer {
private String[] encryptPropNames = {"jdbc.username", "jdbc.password"};
@Override
protected String convertProperty(String propertyName, String propertyValue) {
if (isEncryptProp(propertyName)) {
String decryptValue = DESUtil.getDecryptString(propertyValue);
System.out.println("解密后的值:"+decryptValue);
return decryptValue;
} else {
return propertyValue;
}
}
private boolean isEncryptProp(String propertyName) {
for (String encryptPropertyName : encryptPropNames) {
if (encryptPropertyName.equals(propertyName)) {
return true;
}
}
return false;
}
}
jdbc.username
与 jdbc.password
两个属性的值设置的是加密后的,当重写 PropertyPlaceholderConfigurer
的 convertProperty
方法然后在对其进行解密后即可。以上代码均可在 codegeekgao.git 下载查看,下一章将温习Spring基于注解配置相关知识的学习。