1. 背景
Spring 是为了简化企业级开发而创建的,在 Spring 框架全家桶中绝对是不可或缺技术。
2.基础概念
为了降低Java开发的复杂性,Spring 采用了以下四种关键策略:
- 基于POJO( Java Ben ) 轻量级和最小侵入性编程。
- 通过依赖注入和和面向接口实现松耦合。
- 基于切面和惯例进行声明式编程。
- 基于切面和样板减少样板代码。
Spring 所做的事情都是围绕这几点展开。
依赖注入 依赖注入( Dependency Injection , DI ) 听起来让人生畏,实际上并没有听上去那么复杂。
IoC (Inversion of Control,缩写为IoC)也称为依赖注入 ( Dependency Injection , DI )。是指“一个对象被创建时,先定义其构造方法的参数或者工厂方法的参数(即其使用的对象),然后容器在创建 bean 时注入这些依赖项的过程”。
对比区别:
- 传统方法是:Class A中用到了Class B,需要在A的代码中new一个B的对象。
- 依赖注入是:定义好A和B,用XML描述A依赖B的关系,在容器容器创建A时,将B对象注入到A的示例对象中。通过容器创建出来就可以直接使用了,无需再New 一个。
面向切面编程 ( AOP )
AOP ( Aspect Oriented Programming ) ,面向切面编程。其中的 Aspect 指 切面,中文的意思可理解为“维度”。
AOP是“关注点分离”的一项技术,软件系统往往由多个组件/模块组成,每个组件各负责一块特定的功能。这些组件往往还承担额外的职责,比如日志,事务,安全控制等系统服务逻辑,和业务功能混合在一起。这些系统服务逻辑会在多个组件/模块中存在,被称为“横切关注点”。
这些模块中调用的系统服务逻辑分散到多个组件/模块中去,导致你需要维护多个组件的代码,带来复杂性。即使把这些关注点抽离成一个独立的模块,但方法的调用还是出现在各个模块中。
而AOP可以使得这些关注点切面模块化,以声明的方式应用到具体业务组件/模块中去,使得这些业务模块更加内聚和更加关注自身的业务。
AOP
使用模块消除 “ 样板代码 ” 样板代码是指重复的代码,比如 传统JDBC 中要开启数据库连接,构造预处理语句等,每次都要写很多。借助使用 模板 Template 封装可以帮助消除样板代码,简化复杂性,模板 使得你的代码更关注与自身的业务职责。
Spring 容器,依赖注入( Dependency Injection , DI ),和面向切面编程( Aspect-Orientd Programming, AOP ) 是 Spring 框架的核心。下面分别介绍。
3. 容器 ( ApplicationContext )
3.1 容器的介绍
org.springframework.context.ApplicationContext 接口代表 Spring IoC 容器,负责实例化、配置和组装 bean。
- 容器通过读取 “配置元数据” 来获取如何创建和装配对象。
- “配置元数据” 可以是 XML配置文件,Java注解,或者Java代码来表示。
ApplicationContext 基于 BeanFactory 构建,BeanFactory 提供了配置框架和基本功能, 而 ApplicationContext 添加了更多企业特定的功能。我们更多使用的是 ApplicationContext 。
Spring 提供了几种 ApplicationContext 实现
- ClassPathXmlApplicationContext 从类路径下加载 XML 配置文件
- FileSystemXmlApplicationContext 从文件系统 加载 XML 配置文件
- AnnotationConfigApplicationContext 基于 注解 的上下文,从注解加载
示例:
代码语言:javascript复制ApplicationContext context = new ClassPathXmlApplicationContext("services.xml", "daos.xml");
3.2 Bean 的生命周期
有了容器,容器负责创建和管理Bean,还要进一步了解下 Bean 的生命周期:
Bean 声明周期
3.3 代码示例
依赖类库 以 Maven 方式时,添加 spring-context 依赖。
代码语言:javascript复制 <dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.3.4</version>
</dependency>
</dependencies>
Spring 支持多种方式加载配置
- (1) XML 方式
- (2) Java 方式
XML 使用 ClassPathXmlApplicationContext 或者 FileSystemXmlApplicationContext 从一个 XML 文件中初始化容器对象。
示例: xml 文件
代码语言:javascript复制<?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
https://www.springframework.org/schema/beans/spring-beans.xsd">
<bean id="hero" class="cn.zyfvir.demo.Hero">
<constructor-arg ref="swordAction"/>
</bean>
<bean id="swordAction" class="cn.zyfvir.demo.SwordAction">
<constructor-arg name="printStream" value="#{T(System).out}"/>
</bean>
</beans>
代码语言:javascript复制 // 使用 xml 方式 配置 spring
private static void demoXmlSpring() {
ApplicationContext context = new ClassPathXmlApplicationContext("spring.xml");
Hero bean = context.getBean(Hero.class);
bean.play();
}
AnnotationConfigApplicationContext 上下文支持从注解和Java代码方式配置对象。
示例:
代码语言:javascript复制@Configuration
public class HeroConfig {
@Bean
public Hero hero() {
return new Hero(action());
}
@Bean
public Action action() {
return new SwordAction(System.out);
}
}
代码语言:javascript复制 // 使用java 方式
private static void demoJavaSpring() {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
// 启用组件扫描,扫描查找任何带 @Component ,@Configuration 注解的类
context.scan("cn.zyfvir.demo");
context.refresh();
Hero bean = context.getBean(Hero.class);
bean.play();
}
我的 demo 示例代码见:https://github.com/vir56k/java_demo/tree/master/spring_demo1
3. 依赖注入 DI ( 装配Bean )
3.1 装配( Wiring )
装配( Wiring ): 在 Spring 中,对象无需自己查找和创建与其关联的其他对象。由 容器 负责把需要协作的各个对象赋予对象。创建对象之间协作关系的行为成为 装配( Wiring )。这也是依赖注入 DI 的本质
Spring 提供了三种 Bean 的装配方式:
- 在XML中配置
- 通过 Java 方式配置
- 自动装配
怎么选择呢?一些建议是:
- 尽量使用 自动装配 的方式,使用起来比较省事,它不用显示的针对 每个Bean 的依赖关系配置。
- 其次,使用Java 方式配置,它是类型安全的,比 XML 更强大直观。
- 最后才选择 XML 方式。
3.2 自动装配 Bean
如果 Spring 能自己装配的话,何必再用 XML 等方式具体声明呢?自动装配能带来很多的便利。
Spring 从两个角度实现自动装配:
- 组件扫描 ( Component Scanning ) :Spring 会自动扫描和发现需要创建的Bean
- 自动装配 ( autowiring ):Spring 自动满足 Bean 之间的依赖
@Component 注解可以作用于一个 类上,用于声明一个bean对象。 @ComponentScan 注解用于启用组件扫描。默认会扫描与其处于相同包下的类。它也可以通过 basePackages属性指定具体包。 @Autowired 注解声明了自动装配,Spring 会选择匹配合适的Bean来装配。它可以作用在构造方法和set方法上。
3.3 通过Java 代码配置
@Configuration 声明了这个类是个配置类,它不是必须的。 通过 @Bean 注解声明这个方法返回一个对象,这个对象要注册到 Spring 的上下文中。
比如:
代码语言:javascript复制@Configuration
public class AppConfig {
@Bean
public MyService myService() {
return new MyServiceImpl();
}
}
3.4 通过XML装配
通过<bean> 标签描述。
- id 属性是个 bean 的标识符
- class 属性定义 bean 的类型并使用完全限定的类名
<?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
https://www.springframework.org/schema/beans/spring-beans.xsd">
<bean id="..." class="..."> </bean>
<bean id="myService" class="com.acme.services.MyServiceImpl"/>
</beans>
3.5 混合使用
使用 @Import , 或者 @ImportResource 等注解。 详细参考:https://docs.spring.io/spring-framework/docs/current/reference/html/core.html#beans-introduction
3.6 代码示例
下面编写一个自动装配的示例:
代码语言:javascript复制/**
* @description: 光盘
* @author: zhangyunfei
* @date: 2021/7/3 22:22
*/
public interface CompactDisc {
void play();
}
/**
* @description: VCD 光盘
* @author: zhangyunfei
* @date: 2021/7/3 22:25
*/
@Component
public class VcdCompactDisc implements CompactDisc {
private String title = "经典歌曲-忘情水";
public void play() {
System.out.println(String.format(" %s 正在播放...", title));
}
}
在使用时,关键在于 Autowired 的注解,它会自动寻找到合适的对象注入到这里。
代码语言:javascript复制/**
* @description: 播放器
* @author: zhangyunfei
* @date: 2021/7/3 22:16
*/
@Component
public class Player {
private CompactDisc compactDisc;
// 自动装配
@Autowired
void insertCompactDisc(CompactDisc action) {
this.compactDisc = action;
}
/**
* 开始播放
*/
void startPlay() {
compactDisc.play();
}
}
还要配置“ 自动扫描要装配的组件 ”, ComponentScan 用于声明搜索当前的包,我这里是个空的类。
代码语言:javascript复制@Configuration
@ComponentScan
public class PlayerConfig {
}
main 方法演示如何调用:
代码语言:javascript复制public class MainClass {
public static void main(String[] args) {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(PlayerConfig.class);
Player bean = context.getBean(Player.class);
bean.startPlay();
}
}
我的代码示例见:https://github.com/vir56k/java_demo/tree/master/spring_demo2_autowired
4. 面向切面 ( AOP )
AOP ( Aspect Oriented Programming, AOP ) 面向切面编程,是指在编译时期方式或者运行时期动态代理的方式,将代码织入到指定位置(具体的类的方法)上的一种编程思想,就是面向切面的编程。
为什么要用AOP 具体要看场景和时机,比如下图:
AOP
类似于 日志,事务这样的功能要想模块化的话面临一些选择,比如对象继承和委托。继承的话整个 应用中都有同样的基类,往往导致一个脆弱的对象体系,而委托可能需要对委托对象进行复杂的调用。
在各个业务模块挨个写调用也太麻烦了,不利于维护。而 切面是一个可供选择方案,使用AOP可以以声明的方式的方式在外部应用,而不用修改(影响)到具体的业务功能模块。这也是 “关注点分离”的体现,每个关注点都集中于一个地方,而不是分散到各处的代码。
多种AOP实现 AOP 是一种编程范式,可以有多种方式实现:
- 代理方式,比如 Spring AOP
- 编译时方式,比如 AspectJ
- 类加载期
代理方式分为静态代理和动态代理,静态代理可理解为自己写的代理或者字节码方式的代理。动态代理是在运行时生成一个代理类(实现类)再由其去访问目标对象的方式。
Spring AOP
Spring AOP 是通过 动态代理
的方式实现的AOP
- 如果要代理的对象,声明其实现了某个接口,那么Spring AOP会使用JDK Proxy,去创建代理对象。创建代理类和新的目标代理实现,调用时通过代理类访问 目标代理实现。
- 没有接口声明时,Spring AOP会使用Cglib,生成一个被代理对象的子类,来作为代理。
AspectJ 是编译成class时织入的,拥有更强大的能力。
类加载期是在目标类加载到JVM时织入,需要特殊的类加载器,比如 AspectJ 5 的load-time weaving,LTW 就支持这种方式。
AOP 术语:
- advise 通知:接收的消息(通知),在什么时机被得知。
- pointcut 切点:描述了在哪里切,比如某个 名字的方法。
- join point 连接点:切落在那个点上,比如 3个叫做 getSome 的具体方法上。
Spring AOP 通知的类型:
- 前置通知(Before): 在目标方法被调用 "前" 的通知。
- 后置通知(After): 在被调用 "后" 的通知。
- 返回通知(After-returning): 在 "成功" 执行后的通知。
- 异常通知(After-throwing): 在 "抛出异常" 后的通知。
- 环绕通知(Around): 将方法 "完全包裹" 的通知,可以获得目标方法执行,因而可以调用前后等自定义时机,甚至多用多次来实现重试机制。
代码示例 (1) 引用类库
代码语言:javascript复制 <dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
<version>5.3.4</version>
</dependency>
(2) 声明 启用 Aspect 的自动代理配置
代码语言:javascript复制// 启用:组件搜索
// 启用:aspect 的自动代理
@Component
@ComponentScan
@EnableAspectJAutoProxy
public class MySrpingConfig {
}
编写AOP通知的切入点
代码语言:javascript复制// 博客服务
interface BlogService {
public void postBlog(String blogContet);
}
@Component
class BlogServiceImpl implements BlogService {
public void postBlog(String blogContet) {
System.out.println("发布了一遍博客");
}
}
// 日志记录员
// 把自己也注册成 Spring 组件
@Aspect
@Component
class LogAspect {
// 切点表达式
@Pointcut("execution(* cn.zyfvir.demo.BlogService.postBlog(..))")
public void doPostPoint() {
}
@Before("doPostPoint()")
public void before() {
System.out.println("## before...");
}
@After("doPostPoint()")
public void after() {
System.out.println("## after...");
}
@AfterReturning("doPostPoint()")
public void afterReturning() {
System.out.println("## AfterReturning...");
}
@AfterThrowing("doPostPoint()")
public void afterThrowing() {
System.out.println("## AfterThrowing...");
}
}
通过AOP,达到了对实际的业务调用无影响,正常使用即可,示例:
代码语言:javascript复制 public static void main(String[] args) {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(MySrpingConfig.class);
BlogService bean = context.getBean(BlogService.class);
bean.postBlog("《从入门到精通》");
System.out.println("执行结束," bean);
}
而在执行过程中,日志记录类会被触发和工作。 我的代码示例见:https://github.com/vir56k/java_demo/tree/master/spring_demo5_aop
5. 扩展:高级装配
5.1 环境与 @Profile 注解
在实际开发中经常会有多个环境,比如 dev 开发环境,test 测试环境,product 正式环境。Spring 对环境做了一层抽象,允许你定义多个环境,和激活使用的某个环境。
关键点是:
- 声明一个环境, 和在环境下才被使用的对象
- 激活一个环境
使用 @Profile 可以声明某个bean只在某个环境下可用(被激活)。比如下面的示例,它使用 @Profile 的注解来声明了 这个类 只有在 development 环境下才可用。
代码语言:javascript复制@Configuration
@Profile("development")
public class StandaloneDataConfig {
@Bean
public DataSource dataSource() {
return new EmbeddedDatabaseBuilder()
.setType(EmbeddedDatabaseType.HSQL)
.addScript("classpath:com/bank/config/sql/schema.sql")
.addScript("classpath:com/bank/config/sql/test-data.sql")
.build();
}
}
激活 配置的环境,可以使用代码的方式,比如:
代码语言:javascript复制AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();
ctx.getEnvironment().setActiveProfiles("development");
ctx.register(SomeConfig.class, StandaloneDataConfig.class, JndiDataConfig.class);
ctx.refresh();
也可以在 以java 命令行启动时指定: 使用 环境变量 “ spring.profiles.active ” 来声明激活的环境,如以下示例所示:
代码语言:javascript复制 -Dspring.profiles.active="profile1,profile2"
5.2 有条件的选择 Bean 与 @Conditional 注解
实际上面说的 @Profile 也是通过 @Conditional 注解来实现的。 @Conditional 注解可用于指示在特定情况下才 注册某个 Bean。
示例:
代码语言:javascript复制@Configuration
public class PersonConfig {
@Bean()
@Conditional({ConditionalDemo1.class})
public Person person1(){
return new Person("Bill Gates",62);
}
}
上面的示例使用了 @Conditional 注解,它指定了参数ConditionalDemo1.class 。@Conditional 的参数实际是这么一个接口,你可以根据你的需要来实现:
代码语言:javascript复制public interface Condition {
boolean matches(ConditionContext var1, AnnotatedTypeMetadata var2);
}
比如,@Profile 注解实际是这么实现的:
代码语言:javascript复制@Override
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
// Read the @Profile annotation attributes
MultiValueMap<String, Object> attrs = metadata.getAllAnnotationAttributes(Profile.class.getName());
if (attrs != null) {
for (Object value : attrs.get("value")) {
if (context.getEnvironment().acceptsProfiles(((String[]) value))) {
return true;
}
}
return false;
}
return true;
}
5.3 处理自动装配时的歧义
在自动装配时,如果有多个可被选中的对象无法被确定时,就出现异常了。 比如遇到下面的情形:
代码语言:javascript复制// 甜点
interface Dessert {
String getName();
}
@Component
class IceCream implements Dessert{
private String name = "冰淇淋";
public String getName() {
return name;
}
}
@Component
class Chocolate implements Dessert{
private String name = "巧克力";
public String getName() {
return name;
}
}
@Component
class Lollipop implements Dessert{
private String name = "棒棒糖";
public String getName() {
return name;
}
}
上面的示例实现了多个 甜点 类,小朋友不知吃哪个了。
代码语言:javascript复制@Component
public class Child {
Dessert dessert;
// 想要
@Autowired
public void wantDessert(Dessert dessert) {
this.dessert = dessert;
}
public void eating() {
System.out.println(String.format("正在吃 %s ...", dessert.getName()));
}
}
这时,可以选择处理歧义的方式:
- 通过 @Primary 声明一个 “优先被选择的”
- 通过 @Qualifier 限定名注解,指定一个 Bean 名称。
示例:
代码语言:javascript复制 // 想要
@Autowired
@Qualifier("lollipop") // @Qualifier 声明了一个 优先选择的 限定名。
public void wantDessert(Dessert dessert) {
this.dessert = dessert;
}
我的代码示例见:https://github.com/vir56k/java_demo/tree/master/spring_demo3
5.4 Bean 作用域 Scope
默认情况下,Spring 创建的实例 是都单例的,即 singleton。
Sping 支持多种 作用域(Scope),包括:
Scope | 描述 |
---|---|
singleton | 单个实例 |
prototype | 每次都创建一个新的实例 |
request | Web应用的一次请求期间 |
session | Web应用的会话期间 |
application | Web应用期间 |
websocket | websocket 范围 |
使用 @Scope 注解可以为一个 Bean 指定 Scope,示例:
代码语言:javascript复制@Scope("prototype")
@Component
class IceCream implements Dessert{
...
}
5.5 运行时装配
运行时装配的场景,比如动态获取 配置文件中的内容,或者 某个方法的执行结果,或者一个随机数,或者某个 表达式结果。
- 使用 @PropertySource 注解可以读取配置文件
- 使用 @Value 注解,可以获取外部的属性值
- 在 Value 注解中可以使用 ${ ... } 这样的表达式读取值 示例如下:
@Configuration
@PropertySource("classpath:myproperty_config.properties")
public class MyPropertyConfig {
// 读取 配置文件中的 author.name
@Value("${author.name}")
public String authorName;
}
5.6 SpEL
SpEL 是指 Spring 表达式语言( Spring Expression Language , SpEL ),它能够以简洁和强大的方式将值装配到Bean的属性中,使用表达式会在运行时计算得到值。
SpEL 特性:
- 引用 Bean
- 调用方法或者访问属性
- 算数运算,关系运算,逻辑运算
- 正则表达式
- 集合操作
SpEL 的格式: #{ .. } 它以 # 开头。
示例:
代码语言:javascript复制设置默认值
@Value("#{ systemProperties['user.region'] }")
private String defaultLocale;
@Configuration
@PropertySource("classpath:myproperty_config.properties")
public class MyPropertyConfig {
// 读取 配置文件中的 author.name
@Value("${author.name}")
public String authorName;
@Value("#{3.1415}")
public String pi;
@Value("#{'xxxxx'}")
public String string1;
@Value("#{myPropertyConfig.getMyName().toUpperCase()}")
public String myName;
@Value("#{T(java.lang.Math).PI}")
public String PI;
@Value("#{T(java.lang.Math).random()}")
public String random;
@Value("#{ myPropertyConfig.pi == 3.14 }")
public boolean is3_14;
@Value("#{ myPropertyConfig.pi ?:'333' }")
public String stirng2;
public String getMyName() {
return "zhang3";
}
@Value("#{ myPropertyConfig.getArray() }")
public String[] array;
@Value("#{ myPropertyConfig.array[1] }")
public String array1;
public String[] getArray() {
String[] arr = new String[3];
arr[0] = "#1";
arr[1] = "#2";
arr[2] = "#3";
return arr;
}
}
6.参考
https://docs.spring.io/spring-framework/docs/current/reference/html/core.html#beans
https://github.com/spring-projects/spring-framework/wiki/Spring-Framework-Artifacts