系列文章:
Java | Spring框架学习笔记–(1)工厂
Java | Spring框架学习笔记–(2)AOP
Java | Spring框架学习笔记–(3)持久层整合
Java | Spring框架学习笔记–(4)MVC框架整合
Java | Spring框架学习笔记–(5)注解编程
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>