写在前面,Spring 简介
Spring 是以 IoC 和 AOP 为核心的开源框架,目前使用最多的 Java EE 企业应用开源框架。
- Spring 官方网址:http://Spring.io/
为什么在项目中使用Spring?
简单的回答好用啊!
1.方便解耦,简化开发:通过Spring提供的IoC容器,可以将对象间的依赖关系交由Spring进行控制,避免了硬编码所造成的过度程序耦合。由于IoC解决对象之间的耦合问题,使得开发者更容易从单例模式类、属性文件解析等 底层编码的困扰中解脱出来,从而将更多的精力用于思考业务逻辑层面的编码设计。AOP在不改变原有业务逻辑情况下,增强横切逻辑代码,根本上解耦合,避免横切逻辑代码重复。
2.强大的AOP编程 与 声明式事务支持:可以轻松实现许多不容易用传统OOP(面向对象编程)实现的功能,如安全、事务、日志等。声明式的事务管理方式相比于硬编码方式,大大提高了框架的灵活性和易用性,使得开发效率和质量能得到显著提升。
3.降低Java EE API 的使用难度:Spring对Java EE API(如JDBC、JavaMail等)进行了封装,使得这些API的使用难度大大降低。难度降低,意味着更容易获得的开发成本。
4.完整的生态支持:Spring几乎能够满足所有的业务场景,而且 Spring全家桶里面还提供了非常多的这种工具,比如Spring Security 处理安全问题、Spring Data 简化数据访问、Spring Cloud 提供微服务的支持等等。
5.方便集成各种优秀框架(其中支持主流数据访问):Spring不排斥各种优秀的开源框架,其内部提供了对各种优秀框架(如Struts、Hibernate、MyBatis、Quartz等)的直接支持,降低了框架使用难度。像原来经典的 SSH、SSM 等框架组合的流行,就是很好证明。
6.方便测试:Spring提供了对JUnit4等测试框架的支持,可以通过注解 @Test 方便地测试 Spring 程序,使得测试工作变得更加简单和高效。
7.源码是经典的学习范例:Spring的源代码设计精妙,结构清晰,体现了Java设计模式的灵活运用和Java技术的高深造诣,是学习Java技术的最佳实践典范。源码几乎面试必问。
8.社区活跃:如果开发过程中遇到问题,网上搜一下,很容易找到解决方案。
做完了铺垫,开始记录Spring 的学习过程,下面先从 IoC开始。
Spring IOC 基础
1. 启动 IoC 容器的方式
- Java环境下启动IoC容器
- ClassPathXmlApplicationContext:从类的根路径下加载配置文件(推荐使用)
- FileSystemXmlApplicationContext:从磁盘路径上加载配置文件
- AnnotationConfigApplicationContext:纯注解模式下启动Spring容器
主要用于教学,需要讲授 Spring 原理,实际几乎用不到;
在某些简单测试场景下,需要本地开发测试而不是 线上 Linux 环境下 Web 容器启动,此时可以考虑以上三种 启动方式。
- Web环境下启动IoC容器
- 从xml启动容器,使用监听器启动Spring的IOC容器
- 从配置类启动容器
2. 基本用法
2.1 XML 配置,即标签
2.1.1 最简单的注入用法
<bean class=“org.boyjava.demo.model.User” id=“user”/>
<?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">
<!--可以直接在这个地方向 Spring 容器注册 Bean ,此时Bean 的实例化实际上是默认的反射调用该Bean 的无参构造方法 -->
<bean class="org.boyjava.demo.model.User" id="user"/>
</beans>
一旦注册成功,User 就已经被初始化了。
public class Demo01 {
public static void main(String[] args) {
// Spring 容器构建过程中,Bean 就已经被创建了
ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml");
}
}
2.1.2 指定构造器注入
<bean class="org.boyjava.demo.model.User" id="user2">
<!--通过构造器参数的下标去指定参数-->
<constructor-arg index="0" value="1"/>
<!--通过构造器参数的名字去指定参数-->
<constructor-arg name="address" value="北京"/>
<constructor-arg name="username" value="zhangsan"/>
</bean>
2.1.3 set方法注入
<bean class="org.boyjava.demo.model.User" id="user">
<!--这个本质上实际上就是调用set方法-->
<property name="id" value="99"/>
<property name="username" value="lisi"/>
<property name="address" value="上海"/>
</bean>
2.1.4 数组、集合等特殊属性注入
<bean class="org.boyjava.demo.model.Book" id="book">
<property name="authors">
<!--数组赋值-->
<array>
<value>zhangsan</value>
<value>lisi</value>
</array>
</property>
<property name="publishers">
<list>
<value>电子工业出版社</value>
<value>机械工业出版社</value>
</list>
</property>
<property name="info">
<map>
<entry key="price" value="20"/>
<entry key="num" value="300"/>
</map>
</property>
</bean>
2.1.5 获取Bean 方式总结
User user2 = ctx.getBean("user2",User.class);
getBean() 这个方法就是跟Sping 容器去要一个 Bean,将来无论是通过何种方式去跟容器要一个 Bean 回来,底层实现都是 getBean () 方法
- ctx.getBean(“根据名字获取 Bean,返回 Object,返回之后需要强转”) ctx.getBean(根据类型获取
- Bean,这个要求该 Bean 在容器中只存在一个实例)
- 无参呢?根据类型去获取,首先是根据类型查找 beanName,找到名字之后,再调用重载的 getBean(“beanName”)去获取bean 。
2.1.6 Bean 生命周期 的 钩子函数(初始化 和 销毁)
可自定义钩子函数,也可实现接口 InitializingBean, DisposableBean 。
2.2 Java配置,主要是@Bean 注解
2.2.1 通过 @Bean 注解向 Spring 容器注册 Bean
public class JavaConfig {
/**
* 这里 @Bean 的作用类似于刚才的 bean 标签,就表示向 Spring 容器中注册一个 Bean
* 默认情况下,beanName 就是方法名
*
* @Bean 注解中也有 name 表示表示为这个 bean 指定名称
* initMethod 和 destroyMethod 为这个 bean 指定生命周期钩子函数
* @return
*/
@Bean("user2")
User user() {
User user = new User();
user.setId(1);
user.setUsername("zhangsan");
return user;
}
}
验证一下吧
main{
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(JavaConfig.class);
User user = ctx.getBean("user2" , User.class );
sout ("user = " + user );
}
2.2.2 @Configuration 注解(实现原理:AOP代理)
2.2.3 通过@Primary 注解 指定 Bean 的优先级
/**
* 这个注解可以用来标记 Bean 的优先级,如果通过类型去 Spring 容器中查找 Bean 的话,
* 那么标记了该注解的 Bean 会被优先返回。
*/
@Primary
User user(Book book) {
User user = new User();
user.setId(1);
user.setUsername("zhangsan");
user.setBook(book);
return user;
}
2.2.4 @Bean z注解的 autowiredCandidate 属性
/**
* autowireCandidate 表示这个 Bean 是否作为注入的候选的 Bean
* 这个属性为 false 就表示这个 Bean 不作为候选 Bean,即其他 Bean 需要注入 User 的时候,不会查找到这个 Bean
*/
@Bean(value = "user",autowireCandidate = false)
User user(Book book) {
User user = new User();
user.setId(1);
user.setUsername("zhangsan");
user.setBook(book);
return user;
}
2.3 包扫描
2.3.1 Spring 注解 @ComponentScan
@Configuration
//这个就是组件扫描,默认情况下,扫描的是当前配置类所在包下的所有 Bean
@ComponentScan(basePackages = "org.boyjava.demo")
public class JavaConfig {
}
2.3.2 XML里配置扫描标签
<!--指定要扫描的包,会自动将指定包中的所有组件注册到 Spring 容器中-->
<context:component-scan base-package="org.boyjava.demo"/>
3. Bean 的作用域
Q:都是与容器绑定,一般情况下一个应用绑定一个 Spring 容器,那它们区别在哪里呢?
A:一个应用 允许存在多个容器。
3.1 XML配置单/多例
<bean class="org.boyjava.demo.User" id="user" scope="prototype"/>
3.2 @Bean + @Scope(“prototype”) 注解
@Configuration
public class JavaConfig {
@Bean
@Scope("prototype")
User user() {
return new User();
}
}
3.3 singleton 与 prototype 比较
默认情况下,多次从 Spring 容器中获取到的 Bean 是同一个 Bean。
- singleton:多次从 Spring 容器中获取到的是同一个 bean
- prototype:每次从 Spring 容器中获取到的都是一个新的 bean
singleton 会在容器初始化的时候,就初始化 bean,而 prototype 则是在去获取 bean 的时候,才初始化 bean。
4. 工厂Bean
4.1 静态工厂
public class OkHttpClientStaticFactory {
private static OkHttpClient client;
static {
OkHttpClient.Builder builder = new OkHttpClient.Builder();
builder.setConnectTimeout$okhttp(5000);
builder.setReadTimeout$okhttp(5000);
client = builder
.build();
}
public static OkHttpClient getInstance() {
return client;
}
}
<!--这个配置,将来向 Spring 容器中注册的就不是 OkHttpClientStaticFactory 类的实例,而是
OkHttpClientStaticFactory#getInstance 方法所返回的实例
-->
<bean class="org.boyjava.demo.OkHttpClientStaticFactory" factory-method="getInstance"/>
4.2 实例工厂
public class OkHttpClientFactory {
private OkHttpClient okHttpClient;
public OkHttpClient okHttpClient() {
if (okHttpClient == null) {
OkHttpClient.Builder builder = new OkHttpClient.Builder();
builder.setConnectTimeout$okhttp(5000);
builder.setReadTimeout$okhttp(5000);
okHttpClient = builder
.build();
}
return okHttpClient;
}
}
<bean class="org.boyjava.demo.OkHttpClientFactory" id="okHttpClientFactory"/>
<!--实例工厂必须要先获取到工厂类的实例,然后才能调用-->
<bean id="client2" factory-bean="okHttpClientFactory" factory-method="okHttpClient"/>
4.3 FactoryBean
以 okhttp 为例,说明 FactoryBean。
public class OkHttpClientFactoryBean implements FactoryBean<OkHttpClient> {
@Override
public OkHttpClient getObject() throws Exception {
OkHttpClient.Builder builder = new OkHttpClient.Builder();
builder.setConnectTimeout$okhttp(5000);
builder.setReadTimeout$okhttp(5000);
return builder
.build();
}
@Override
public Class<?> getObjectType() {
return OkHttpClient.class;
}
@Override
public boolean isSingleton() {
return true;
}
}
<!--这里向 Spring 容器中注册的也不是 OKHttpClientFactoryBean ,而是 其 方法getObject() 的返回值 -->
<bean class="org.boyjava.demo.OKHttpClientFactoryBean" id="client3"/>
// 如果想获取 OKHttpClientFactoryBean,而不是其方法getObject() 的返回值,那就需要在Java 代码中 id 值前边 添加 “&”符即可,如下代码示意。
// 获取 工厂 Bean
Object client3 = ctx.getBean("&client3");
4.3.1 FactoryBean的经典使用场景有哪些?
okhttp client、MyBatis SqlSessionFactory、shiro
4.3.2 FactoryBean 的特点
1. 工厂模式:FactoryBean 采用了工厂模式的思想,它充当了一个工厂的角色,用于创建和返回 Bean 实例。通过FactoryBean,我们可以将 Bean 的创建逻辑封装在 FactoryBean 的实现类中,从而实现 Bean 的解耦和灵活配置。
2. 延迟加载:FactoryBean 支持延迟加载,即只有在需要获取 Bean 实例时才会调用 FactoryBean 的 getObject() 方法创建 Bean。这种延迟加载的机制可以提高系统的性能和启动速度。
3. 支持多种类型的 Bean:FactoryBean 可以创建并返回多种类型的 Bean 实例,包括单例 Bean、原型 Bean 等。这使得 FactoryBean 具有更强的灵活性和可扩展性。
4. 支持自定义配置:FactoryBean 允许我们自定义 Bean 的创建和配置过程。通过实现 FactoryBean 接口,我们可以按照自己的需求来定义 Bean 的创建逻辑和配置信息。
5. 条件注解
5.1 条件注解案例
可以定义不同的条件,当条件满足的时候,这个 Bean 才会被注册到 Spring 容器中。
public class LinuxCondition implements Condition {
/**
* 这个方法返回 true,就表示条件是满足的,返回 false,就表示条件不满足
*
* @param context the condition context
* @param metadata the metadata of the {@link org.springframework.core.type.AnnotationMetadata class}
* or {@link org.springframework.core.type.MethodMetadata method} being checked
* @return
*/
@Override
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
//获取当前操作系统名字
String osName = context.getEnvironment().getProperty("os.name");
return osName.toLowerCase().contains("linux");
}
}
public class WindowsCondition implements Condition {
/**
* 参见LinuxCondition即可
* @return
*/
}
}
当条件满足的时候,再去注册 bean :
@Configuration
public class JavaConfig {
@Bean
//这个表示WindowsCondition#matches方法返回 true,这个 bean 才会注册到 Spring 容器
@Conditional(WindowsCondition.class)
ShowCmd winCmd() {
System.out.println("winCmd");
return new WindowsShowCmd();
}
@Bean
//这个表示LinuxCondition#matches方法返回 true,这个 bean 才会注册到 Spring 容器
@Conditional(LinuxCondition.class)
ShowCmd linuxCmd() {
System.out.println("linuxCmd");
return new LinuxShowCmd();
}
}
5.2 多环境切换
@Configuration
public class JavaConfig {
@Bean
@Profile("dev")
DataSource devDs() {
return new DataSource("jdbc:mysql:///dev", "dev", "dev");
}
@Bean
@Profile("prod")
DataSource prodDs() {
return new DataSource("jdbc:mysql:///prod", "prod", "prod");
}
}
public class Demo {
public static void main(String[] args) {
//如果直接传入启动类,这容器直接就初始化了,Bean 也都初始化了
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);
}
}