Spring笔记(1) 工厂
什么是Spring
Spring是⼀个轻量级的JavaEE解决⽅案,整合众多优秀的设计模式 。
- 轻量级:
- 对于运行环境是没有额外要求的。
可以在开源的tomcat resion jetty里运行,也可以在收费的weblogic websphere里运行。 - 代码移植性高:不需要事先额外的接口。
- JavaEE解决方案:
- 整合设计模式(工厂、代理等)
设计模式
- ⼴义概念
⾯向对象设计中,解决特定问题的经典代码 - 狭义概念
GOF4⼈帮定义的23种设计模式:⼯⼚、适配器、装饰器、⻔⾯、代理、模板…
工厂设计模式
- 概念:通过⼯⼚类,创建对象,不提倡通过直接new的方法的创建对象
User user = new User();
UserDAO userDAO = new UserDAOImpl();
- 好处:解耦合
耦合:指定是代码间的强关联关系,⼀⽅的改变会影响到另⼀⽅
问题:不利于代码维护
简单:把接⼝的实现类,硬编码在程序中
UserService userService = new UserServiceImpl();
比如说我们目前使用UserServiceImpl
作为UserService
的一个实现类,当突然有一天我们需要升级或者其他说明原因,需要把实现类改成UserServiceImpl2
,就还需要再改动一次代码,就还需要重新编译一次。
public static void main(String[] args) {
UserService userService = new UserServiceImpl();
userService.login("abcabc","123456");
}
一个比较好的解决方法就是使用工厂来创建对象
public static void main(String[] args) {
UserService userService = Factory.getUserService();
userService.login("abcabc","123456");
}
总结:
Spring本质:⼯⼚ ApplicationContext (applicationContext.xml)
第一个Spring
环境搭建
jar包
在maven的中心仓库,搜索spring,导入相应的依赖,这里选择spring context
<!-- https://mvnrepository.com/artifact/org.springframework/spring-context -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.3.0</version>
</dependency>
配置文件
配置文件放置的位置:任意位置
配置文件的命名:没有硬性要求,建议applicationContext.xml
以后使用Spring框架时,需要进行配置文件路径的设置。
<?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">
</beans>
核心API
ApplicationContext:Spring提供的一个工厂对象,用于对象的创建。好处:解耦合。
ApplicationContext是一个接口类型,屏蔽实现的差异。
在非web环境下,主要使用ClassPathXmlApplicationContext
实现类,在web环境下主要使用XmlWebApplicationContext
实现类
ApplicationContext工厂对象需要占用大量的内存(重量级),所以我们不会频繁地创建这个对象(一个应用智慧创建一个工厂对象),而且这个对象一定是线程安全的。
程序开发
- 新建一个类
- 在applicationContext.xml中引入
<?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">
<!--
id:唯一
class:全限定类名
-->
<bean id="person" class="com.prince.Person"></bean>
</beans>
- Main
public class TestPerson {
@Test
public void test(){
//创建Spring的工厂对象,并且在构造方法中指定配置文件的位置
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("/applicationContext.xml");
//根据id创建对象
Person person = (Person)applicationContext.getBean("person");
//测试
System.out.println(person);
}
}
细节分析
由Spring创建的对象,叫做Bean或者Component
常见方法
- 直接通过id值获得bean对象,但是需要强制转换
Person person = (Person)applicationContext.getBean("person");
- 传入一个class字节码对象,就可以不用强制类型转换了
Person person = applicationContext.getBean("person",Person.class);
- 甚至可以直接只传入一个class字节码对象,表示根据类型来获取值
Person person = applicationContext.getBean(Person.class);
注意:当Spring的容器中只存在一个Person对象时,才能够这样子获取,如果有多个,会报异常(他不知道你想要的是哪个)。
- 获取bean的所有id
String[] beanDefinitionNames = applicationContext.getBeanDefinitionNames();
System.out.println(Arrays.toString(beanDefinitionNames));
- 获取指定类的所有id
String[] beanNamesForType = applicationContext.getBeanNamesForType(Person.class);
for (String s : beanNamesForType) {
System.out.println(s);
}
- 判断是否存在指定的id
applicationContext.containsBeanDefinition("person");//当配置文件显式定义了id时,只判断id 不判断name ,没有显式定义id时,也可以判断name
applicationContext.containsBean("person");//id 和 name 都可以判断
配置文件
- 只配置class属性,不配置id属性
<bean class="com.prince.Person"></bean>
问:没有手动设置id值,他有没有id
验证:
String[] beanDefinitionNames = applicationContext.getBeanDefinitionNames();
for (String beanDefinitionName : beanDefinitionNames) {
System.out.println(beanDefinitionName);
}
输出结果为:
com.prince.Person#0
可以看到,当不指定id值的时候,spring 默认会为我们提供一个id值。
如果这个bean只需要使用一次,那么久可以省略id值。如果会使用多次,或者需要被其他bean引用时,就必须要指定id属性!
- name属性:用于给bean定义别名(小名)
相同点:在getBean()方法,可以同时传id和name来获取对象。(name可以取代id的作用)
<bean name="p" class="com.prince.Person"></bean>
Person p = applicationContext.getBean("p",Person.class);
System.out.println(p);
区别:
- 别名可以定义多个(用逗号分隔),但是ID只能定义一个
<bean name="p1,p2,p3" class="com.prince.Person"></bean>
- 在XML中,id属性的命名要求:必须以字母开头,不能以特殊字符开头。name则没有要求。所以name属性会应用在特殊命名的场景下。
但是xml发展到今天,id的限制已不存在。 containsBeanDefinition
和containsBean
的区别。
Spring与日志框架
Spring与⽇志框架进⾏整合,⽇志框架就可以在控制台中,输出Spring框架运⾏过程中的⼀些重要的信息。
好处:便于了解Spring框架的运⾏过程,利于程序的调试
如何整合日志框架?
默认情况下:
Spring 1 2 3等早期版本都是使用commons-logging.jar
Spring 5.x默认整合的是 logback log4j2
如何整合log4j(我们不要log4j2)?
- 引入log4j的jar包
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.17</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
<version>1.7.25</version>
</dependency>
- 创建log4j.properties
log4j.rootLogger=debug,stdout
log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.Target=System.out
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern=[%-5p] %d{yyyy-MM-dd HH:mm:ss,SSS} method:%l%n%m%n
之后就可以很清楚地看到Spring的运行情况:
注入
注入:通过Spring工厂及配置文件,为所创建的成员变量赋值。
平常的时候为成员变量赋值(使用代码的方式,存在耦合问题):
@Test
public void test4(){
Person p = applicationContext.getBean("p",Person.class);
p.setName("xiaoming");
p.setAge(18);
System.out.println(p);
}
使用Spring的配置文件来注入:
<bean name="p1" class="com.prince.Person">
<property name="name">
<value>aaa</value>
</property>
<property name="age">
<value>18</value>
</property>
</bean>
<bean name="p1" class="com.prince.Person">
<property name="name" value="aaa"/>
<property name="age" value="18" />
</bean>
好处:解耦合
Set注入
Spring调用Set方法,通过配置文件,为成员变量赋值。
String+8种基本类型
property标签里面嵌套value即可。
<property name="name">
<value>aaa</value>
</property>
数组
property标签里面嵌套list,然后再在list里嵌套多个value即可。
<property name="emails">
<list>
<value>123456@qq.com</value>
<value>123456@163.com</value>
<value>123456@126.com</value>
<value>123456@gmail.com</value>
</list>
</property>
Set集合
property标签里面嵌套set,然后再在set里嵌套多个value即可。
<property name="tels">
<set>
<value>18888888888</value>
<value>18888888889</value>
<value>18888888890</value>
<value>18888888890</value>
</set>
</property>
细节:由于Set集合本身是无序的,所以最终输出的顺序不一定会和这个一样。由于Set是无重复的,即使加入了重复的元素,也会自动去重。
List集合
和数组一样,都是property里嵌套list
Map集合
<property name="map">
<map>
<entry key="k1" value="v1"/>
<entry>
<key><value>k2</value></key>
<value>v2</value>
</entry>
<entry>
<key><ref bean=""></ref></key>
<ref bean=""></ref>
</entry>
</map>
</property>
注意:key有专属的标签,写在key里面的内容就是key,因为第5行我的key是String类型,所以我在里面嵌套value标签,如果key是一个对象,那么key标签里面嵌套的是ref标签。
key外面的都是value。
Property
<property>
<props>
<prop key="k1">v1</prop>
</props>
</property>
用户自定义类型
第一种方式:直接在property
里面加bean 标签即可,因为那个bean仅使用一次,所以不需要id属性
<bean id="userServer" class="com.prince.basic.UserServiceImpl">
<property name="userDao">
<bean class="com.prince.basic.UserDaoImpl"></bean>
</property>
</bean>
第二种方式:
<bean id="userDao" class="com.prince.basic.UserDaoImpl"></bean>
<bean id="userServer" class="com.prince.basic.UserServiceImpl">
<property name="userDao">
<ref bean="userDao"></ref>
</property>
</bean>
简化方法
- 基于属性简化
<property name="name" value="aaa"/>
<property name="userDao" ref="userDao">
- 基于p命名空间简化
<bean name="p" class="com.prince.Person" p:name="bbb" p:age="180"></bean>
<bean id="userServer" class="com.prince.basic.UserServiceImpl" p:userDao-ref="userDao"></bean>
直接写在bean标签上,p是property的缩写。
构造注入
Spring调用构造方法来赋值。前提:提供有参构造方法。
- 必须提供有参构造方法。
public class People {
public String name;
public int age;
public People() {
}
public People(String name, int age) {
this.name = name;
this.age = age;
}
}
- 配置bean标签的时候,里面不再是property标签,而是
constructor-arg
,
<bean id="people" class="com.prince.People">
<constructor-arg>
<value>zhangsan</value>
</constructor-arg>
<constructor-arg>
<value>18</value>
</constructor-arg>
</bean>
这里需要注意的是,constructor-arg标签的个数,和顺序,必须要和构造方法里的保持一致!
注:当构造函数的参数个数不一样时,可以通过<constructor-arg>
标签的个数进行区分。
当参数个数一样时,需要指定type属性,如果不指定,他将会随机选一个来注入。
<bean id="people1" class="com.prince.People">
<constructor-arg type="int">
<value>123</value>
</constructor-arg>
</bean>
反转控制与依赖注入
反转控制
控制:对于成员变量赋值的控制权
反转控制:把对于成员变量赋值的控制权,从代码中转移到Spring工厂的配置文件中完成。
好处:解耦合
底层实现:工厂设计模式
依赖注入
注入:通过Spring的工厂及配置文件,为对象(bean,组件)的成员变量赋值。
依赖注⼊:当⼀个类需要另⼀个类时,就意味着依赖,⼀旦出现依赖,就可以把另⼀个类作为本类的成员变量,最终通过Spring配置⽂件进⾏注⼊(赋值)
复杂对象创建
简单对象:可以直接通过new的方式创建
复杂对象:不能通过new的方式创建,比如Connection、SqlSessionFactory
实现FactoryBean接口
开发步骤:
- 新建一个类,继承FactoryBean接口。使用FactoryBean接口的时候,要指定泛型。
public class ConnectionFactory implements FactoryBean<Connection> {
/**
* 创建复杂对象的过程放在这,Spring会拿它的返回值来当做要创建的对象。
* @return 复杂对象
* @throws Exception
*/
@Override
public Connection getObject() throws Exception {
Class.forName("com.mysql.jdbc.Driver");
return DriverManager.getConnection("jdbc:mysql:///mydata","root","root");
}
/**
*
* @return 复杂对象的Class字节码文件
*/
@Override
public Class<?> getObjectType() {
return Connection.class;
}
/**
* 是否单例
* @return true或者false
*/
@Override
public boolean isSingleton() {
return false;
}
}
- Spring配置文件的配置
<!--错误认知:通过getBean获取conn,得到的是ConnectionFactory对象。
其实,Spring会对者做特殊处理,如果class指定的是FactoryBean接口,那么通过getBean获取到的就是那个复杂对象。
-->
<bean id="conn" class="com.factorybean.ConnectionFactory"></bean>
细节:如果就是想获取FactoryBean对象,而不是对应的复杂对象,可以在getBean里面的id前面加&
applicationContext.getBean("&conn");
依赖注入改造
Class.forName("com.mysql.jdbc.Driver");
return DriverManager.getConnection("jdbc:mysql:///mydata","root","root");
这四个参数对于Connection来说都是非常重要的,也就是依赖。
我们可以通过依赖注入的方式改造这个类:
public class ConnectionFactory1 implements FactoryBean<Connection> {
public String driverName;
public String url;
public String username;
public String password;
//getter setter略
@Override
public Connection getObject() throws Exception {
Class.forName(driverName);
return DriverManager.getConnection(url,username,password);
}
//getObjectType isSingleton 略
}
配置文件中通过Set注入,好处:解耦合!!
<bean id="conn1" class="com.factorybean.ConnectionFactory1">
<property name="driverName" value="com.mysql.jdbc.Driver"/>
<property name="url" value="jdbc:mysql:///mydata"/>
<property name="username" value="root"/>
<property name="password" value="root"/>
</bean>
FactoryBean总结:FactoryBean是Spring中用于创建复杂对象的一种方式,后续讲Spring整合其他框架会大量使用这种模式!
实例工厂
为什么使用实例工厂?
- 避免Spring框架的侵入(FactoryBean是Spring框架提供的)
- 整合遗留系统
开发步骤:
- 创建一个工厂类,创建一个
getInstance()
或者getXxxx()
方法来创建一个对象。
/**
* 使用实例工厂创建
*/
public class ConnectionFactory2 {
public Connection getConnection(){
Connection conn = null;
try {
Class.forName("com.mysql.jdbc.Driver");
conn = DriverManager.getConnection("jdbc:mysql:///mydata","root","root");
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (SQLException throwables) {
throwables.printStackTrace();
} finally {
}
return conn;
}
}
- Spring配置文件,
factory-bean
填的是工厂类的bean,factory-method
填的是工厂类创建工厂的方法。
<!--实例工厂-->
<bean id="connfactory" class="com.factorybean.ConnectionFactory2"></bean>
<bean id="conn2" factory-bean="connfactory" factory-method="getConnection"></bean>
静态工厂
静态工厂和实例工厂的区别是:实例工厂的类的创建对象的方法不是静态的,而静态工厂的方法是静态的,所以使用静态工厂就省去了创建工厂类这一步。
<bean id="conn3" class="com.factorybean.ConnectionFactory3" factory-method="getConnection"></bean>
控制Spring工厂创建对象的次数
简单对象
添加一个scope属性即可
<bean id="account" scope="singleton|prototype" class="xxxx.Account"/>
sigleton:只会创建⼀次简单对象 默认值
prototype:每⼀次都会创建新的对象
复杂对象
FactoryBean{
isSingleton(){
return true 只会创建⼀次
return false 每⼀次都会创建新的
}
}
如没有isSingleton⽅法 还是通过scope属性 进⾏对象创建次数的控制
对象的生命周期
生命周期:指的是一个对象创建、存活、消亡的一个完整过程
创建阶段
创建阶段:Spring工厂何时创建对象
-
scope="singleton"
Spring工厂创建的同时,创建对象 -
scope="prototype"
Spring工厂在获取对象的同时,创建对象
验证:xml文件里:
<bean id="pro" class="com.life.Product" scope="singleton"></bean>
效果:在执行完下面代码后,对象创建(可以看到控制台中输出了构造方法打印的文字)
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("/life.xml");
而 scope="prototype"
的对象,只有调用了getBean()
方法之后才会创建对象。
如果,singleton
的对象也想实现prototype
的那种效果(只有调用了getBean()
才会创建对象),可以加入懒加载属性lazy-init
:
<bean id="pro" class="com.life.Product" scope="singleton" lazy-init="true"></bean>
初始化阶段
Spring工厂在创建完对象后,会调用对象的初始化方法,完成对应的初始化操作。
初始化方法,由程序员根据需求来提供;初始化方法的调用,由Spring来完成。
实现初始化的方式
- 实现InitializingBean接口,重写afterPropertiesSet()方法,在方法里面执行初始化语句。
public class Product implements InitializingBean {
public Product() {
System.out.println("Product对象已创建");
}
@Override
public void afterPropertiesSet() throws Exception {
System.out.println("初始化");
}
}
<bean id="pro" class="com.life.Product" scope="singleton"></bean>
执行代码,输出:
- 类中提供一个普通方法,然后再在配置文件中指定
init-method
属性
我这里配置的初始化方法是init()
<bean id="pro1" class="com.life.Product1" scope="singleton" init-method="init"></bean>
细节:
- 如果一个对象实现InitializingBean接口的同时,又提供普通的初始化方法,那么两个初始化方法都会执行。
- 注入在初始化之前执行
销毁阶段
Spring销毁对象前,会调用对象的销毁方法,完成销毁操作。
Spring什么时候销毁对象? 答:工厂关闭的时候
((ClassPathXmlApplicationContext)applicationContext).close();
实现销毁的方式
- 实现
DisposableBean
接口
public class Product2 implements DisposableBean {
public Product2() {
System.out.println("Product对象已创建");
}
@Override
public void destroy() throws Exception {
System.out.println("销毁方法");
}
}
- 自定义销毁方法
<bean id="pro3" class="com.life.Product3" scope="singleton" destroy-method="myDestroy"></bean>
细节:
- 销毁方法只使用于
scope="singleton"
- 如果两种销毁方法都存在,那么两种方法都执行
配置文件参数化
把Spring配置文件中需要经常修改的字符串信息,转移到一个更小的配置文件中。
1. Spring的配置⽂件中存在需要经常修改的字符串?
存在 以数据库连接相关的参数 代表
2. 经常变化字符串,在Spring的配置⽂件中,直接修改
不利于项⽬维护(修改)
3. 转移到⼀个⼩的配置⽂件(.properties)
利于维护(修改)
比如一个druid的链接池:
<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
<property name="driverClassName" value="com.mysql.jdbc.Driver"/>
<property name="url" value="jdbc:mysql:///mydata"/>
<property name="username" value="root"/>
<property name="password" value="root"/>
</bean>
原来是那些value都整合在一个配置文件中,可以通过下面的方法来把那些value分离出去
dp.properties
driverClassName=com.mysql.jdbc.Driver
url=jdbc:mysql:///mydata
username=root
password=root
applicationContext.xml
<context:property-placeholder location="dp.properties"></context:property-placeholder>
<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
<property name="driverClassName" value="${driverClassName}"/>
<property name="url" value="${url}"/>
<property name="username" value="${username}"/>
<property name="password" value="${password}"/>
</bean>
类型转换器
类型转换器
作⽤: Spring通过类型转换器把配置⽂件中字符串类型的数据,转换成了对象中成员变量对应类型的数据,进⽽完成了注⼊
自定义类型转换器
当Spring内部没有提供特定的类型转换器,而程序员在应用的过程中还需要使用,那么就需要自定义类型转换器
如:日期格式
<bean id="person" class="com.prince.Person">
<property name="id" value="111" />
<property name="birthday" value="2020-11-11"/>
</bean>
运行的时候直接报错:
原因:缺少转换器,没法把字符串"2020-11-11"转成Date对象
解决方法:添加自定义类型转换器(实现Converter接口,然后在Spring配置文件中注册)
- 新建一个类,实现Converter接口
可以发现Converter
是一个泛型,Converter<S,T>
中,S
表示源类型,T
表示要转换的类型。
踩坑:Converter是org.springframework.core.convert.converter.Converter
,不要导错包。
public class MyConvert implements Converter<String, Date> {
@Override
public Date convert(String s) {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
try {
return sdf.parse(s);
} catch (ParseException e) {
e.printStackTrace();
}
return null;
}
}
- 在Spring配置文件中注册,让Spring知道有这个类
<!--第一步:创建这个转换器类的对象(最基本)-->
<bean id="myConvert" class="com.prince.MyConvert" />
<!--第二步:创建ConversionServiceFactoryBean对象,注入那个转换器,用于告诉Spring-->
<!--踩坑:id一定是conversionService,否则不起作用-->
<bean id="conversionService" class="org.springframework.context.support.ConversionServiceFactoryBean">
<property name="converters">
<!--观察源码发现,converters是一个Set集合-->
<set>
<ref bean="myConvert" />
</set>
</property>
</bean>
细节
- 自定义类型转换器的
"yyyy-MM-dd"
可以用依赖注入。
public class MyConvert implements Converter<String, Date> {
public String pattern;
public String getPattern() {
return pattern;
}
public void setPattern(String pattern) {
this.pattern = pattern;
}
@Override
public Date convert(String s) {
SimpleDateFormat sdf = new SimpleDateFormat(pattern);
try {
return sdf.parse(s);
} catch (ParseException e) {
e.printStackTrace();
}
return null;
}
}
<bean id="myConvert" class="com.prince.MyConvert" >
<property name="pattern" value="yyyy-MM-dd"/>
</bean>
- ConversionServiceFactoryBean的id值必须是conversionService
- Spring内置的String–>Date转换器只支持
yyyy/MM/dd
后置处理Bean
BeanPostProcessor
BeanPostProcessor
作⽤:对Spring⼯⼚所创建的对象,进⾏再加⼯。
底层实现:
程序员实现BeanPostProcessor规定接⼝中的⽅法:
Object postProcessBeforeInitiallization(Object bean String beanName)
作⽤: Spring创建完对象,并进⾏注⼊后,可以运⾏Before⽅法进⾏加⼯
获得Spring创建好的对象 :通过⽅法的参数
最终通过返回值交给Spring框架
Object postProcessAfterInitiallization(Object bean String beanName)
作⽤: Spring执⾏完对象的初始化操作后,可以运⾏After⽅法进⾏加⼯
获得Spring创建好的对象 :通过⽅法的参数
最终通过返回值交给Spring框架
开发步骤
- 新建一个类,实现
BeanPostProcessor
接口
public class MyBeanPostProcessor implements BeanPostProcessor {
@Override
public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
return bean;
}
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
Person person = (Person)bean;
person.setId(9999);
return person;
}
}
- 在Spring配置文件中配置(只需要把这个对象创建出来就行了)
<bean id="beanPostProcessor" class="com.prince.MyBeanPostProcessor"></bean>
注意:
- 不是Person类实现
BeanPostProcessor
接口,而是重新写一个类来实现这个接口 - 一旦配置了这个
BeanPostProcessor
,那么这个工厂里创建的所有bean都会经过这个处理器,所以为了避免类型转换异常,需要加一个判断:
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
if(bean instanceof Person){
Person person = (Person)bean;
person.setId(9999);
}
return bean;
}
- 在Spring配置文件中配置(只需要把这个对象创建出来就行了)
<bean id="beanPostProcessor" class="com.prince.MyBeanPostProcessor"></bean>