精读《Spring 概念》

2022-03-14 17:58:27 浏览数 (1)

spring 是 Java 非常重要的框架,且蕴含了一系列设计模式,非常值得研究,本期就通过 Spring学习 这篇文章了解一下 spring。

spring 为何长寿

spring 作为一个后端框架,拥有 17 年历史,这在前端看来是不可思议的。前端几乎没有一个框架可以流行超过 5 年,就最近来看,react、angular、vue 三大框架可能会活的久一点,他们都是前端相对成熟阶段的产物,我们或多或少可以看出一些设计模式。然而这些前端框架与 spring 比起来还是差距很大,我们来看看 spring 到底强大在哪。

设计模式

设计模式是一种思想,不依附于任何编程语言与开发框架。比如你学会了工厂设计模式,可以在后端用,也可以转到前端用,可以在 Go 语言用,也可以在 Typescript 用,可以在 React 框架用,也可以在 Vue 里用,所以设计模式是一种具有迁移能力的知识,学会后可以受益整个职业生涯,而语言、框架则不具备迁移性,前端许多同学都把精力花在学习框架特性上,遇到前端技术迭代时期就尴尬了,这就是为什么大公司面试要问框架原理,就是看看你能否抓住一些不变的东西,所以洋洋洒洒的说上下文相关的细节也不是面试官想要的,真正想听到的是你抽象后对框架原理共性的总结。

spring 框架就用到了许多设计模式,包括:

工厂模式:用工厂生产对象实例来代替原始的 new。所谓工厂就是屏蔽实例话的细节,调用处无需关心实例化对象需要的环境参数,提升可维护性。spring 的 BeanFactory 创建 bean 对象就是工厂模式的体现。代理模式:允许通过代理对象访问目标对象。Spring 实现 AOP 就是通过动态代理模式。单例模式:单实例。spring 的 bean 默认都是单例。包装器模式:将几个不同方法通用部分抽象出来,调用时通过包装器内部引导到不同的实现。比如 spring 连接多种数据库就使用了包装器模式简化。观察者模式:这个前端同学很熟悉,就是事件机制,spring 中可以通过 ApplicationEvent 实践观察者模式。适配器模式:通过适配器将接口转换为另一个格式的接口。spring AOP 的增强和通知就使用了适配器模式。模板方法模式:父类先定义一些函数,这些函数之间存在调用关联,将某些设定为抽象函数等待子类继承时去重写。spring 的 jdbcTemplatehibernateTemplate 等数据库操作类使用了模版方法模式。

全家桶

spring 作为一个全面的 java 框架,提供了系列全家桶满足各种场景需求:spring mvc、spring security、spring data、spring boot、spring cloud。

  • spring boot:简化了 spring 应用配置,约定大于配置的思维。
  • spring data:是一个数据操作与访问工具集,比如支持 jdbc、redis 等数据源操作。
  • spring cloud:是一个微服务解决方案,基于 spring boot,集成了服务发现、配置管理、消息总线、负载均衡、断路器、数据监控等各种服务治理能力。
  • spring security:支持一些安全模型比如单点登录、令牌中继、令牌交换等。
  • spring mvc:MVC 思想的 web 框架。

IOC

IOC(Inverse of Control)控制反转。IOC 是 Spring 最核心部分,因为所有对象调用都离不开 IOC 模式。

假设我们有三个类:Country、Province、City,最大的类别是国家,其次是省、城市,国家类需要调用省类,省类需要调用城市类:

代码语言:javascript复制
public class Country {
  private Province province;
  public Country(){
    this.province = new Province()
  }
}

public class Province {
  private City city;
  public Province(){
    this.city = new City()
  }
}

public class City {
  public City(){
    // ...
  }
}

假设来了一个需求,City 实例化时需增加人口(people)参数,我们就要改动所有类代码:

代码语言:javascript复制
public class Country {
  private Province province;
  public Country(int people){
    this.province = new Province(people)
  }
}

public class Province {
  private City city;
  public Province(int people){
    this.city = new City(people)
  }
}

public class City {
  public City(int people){
    // ...
  }
}

那么在真实业务场景中,一个底层类可能被数以千计的类使用,这么改显然难以维护。IOC 就是为了解决这个问题,它使得我们可以只改动 City 的代码,而不用改动其他类的代码:

代码语言:javascript复制
public class Country {
  private Province province;
  public Country(Province province){
    this.province = province
  }
}

public class Province {
  private City city;
  public Province(City city){
    this.city = city
  }
}

public lass City {
  public City(int people){
    // ...
  }
}

可以看到,增加 people 属性只需要改动 city 类。然而这样做也是有成本的,就是类实例化步骤会稍微繁琐一些:

代码语言:javascript复制
City city = new City(1000);
Province province = new Province(city);
Country country = new Country(province);

这就是控制反转,由 Country 依赖 Province 变成了类依赖框架(上面的实例化代码)注入。

然而手动维护这种初始化依赖是繁琐的,spring 提供了 bean 容器自动做这件事,我们只需要利用装饰器 Autowired 就可以自动注入依赖:

代码语言:javascript复制
@Component
public class Country {  
    @Autowired  
    private Province province;
}
@Component
public class Province {  
    @Autowired
    public City city;
}
@Component
public class City {  
}

实际上这种自动分析并实例化的手段,不仅比手写方便,还能解决循环依赖的问题。在实际场景中,两个类相互调用是很常见的,假设现在有 A、B 类相互依赖:

代码语言:javascript复制
@Component
public class A {  
    @Autowired  
    private B b;
}
@Component
public class B {  
    @Autowired
    public A a;
}

那么假设我们想获取 A 实例,会经历这样一个过程:

代码语言:javascript复制
获取 A 实例 -> 实例化不完整 A -> 检测到注入 B -> 实例化不完整 B -> 检测到注入 A -> 注入不完整 A -> 得到完整 B -> 得到完整 A -> 返回 A 实例

其实 spring 仅支持单例模式下非构造器的循环依赖,这是因为其内部有一套机制,让 bean 在初始化阶段先提前持有对方引用地址,这样就可以同时实例化两个对象了。

除了方便之外,IOC 配合 spring 容器概念还可以使获取实例时不用关心一个类实例化需要哪些参数,只需要直接申明获取即可,这样在类的数量特别多,尤其是大量代码不是你写的情况下,不需要阅读类源码也可以轻松获取实例,实在是大大提升了可维护性。

说到这就提到了 Bean 容器,在 spring 概念中,Bean 容器是对 class 的加强,如果说 Class 定义了类的基本含义,那 Bean 就是对类进行使用拓展,告诉我们应该如何实例化与使用这个类。

举个例子,比如利用注解描述的这段 Bean 类:

代码语言:javascript复制
@Configuration
public class CityConfig {
  @Scope("prototype")
  @Lazy
  @Bean(initMethod = "init", destroyMethod = "destroy")
  public City city() {
    return new City()
  }
}

可以看到,额外描述了是否延迟加载,是否单例,初始化与析构函数分别是什么等等。

下面给出一个从 Bean 获取实例的例子,采用比较古老的 xml 配置方式:

代码语言:javascript复制
public interface City {
  Int getPeople();
}
代码语言:javascript复制
public class CityImpl implements City {
  public Int getPeople() {
    return 1000;
  }
}

接下来用 xml 描述这个 bean:

代码语言:javascript复制
<?xml version="1.0" encoding="UTF-8" ?>
<beans xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xmlns="http://www.springframework.org/schema/beans"
  xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd" default-autowire="byName">

  <bean id="city" class="xxx.CityImpl"/>
</beans>

bean 支持的属性还有很多,由于本文并不做入门教学,就不一一列举了,总之 id 是一个可选的唯一标志,接下来我们可以通过 id 访问到 city 的实例。

代码语言:javascript复制
public class App {
  public static void main(String[] args) {
    ApplicationContext context = new ClassPathXmlApplicationContext("classpath:application.xml");

    // 从 context 中读取 Bean,而不 new City()
    City city = context.getBean(City.class);

    System.out.println(city.getPeople());
  }
}

可以看到,程序任何地方使用 city 实例,只需要调用 getBean 函数,就像一个工厂把实例化过程给承包了,我们不需要关心 City 构造函数要传递什么参数,不需要关心它依赖哪些其他的类,只要这一句话就可以拿到实例,是不是在维护项目时省心了很多。

AOP

AOP(Aspect Oriented Program)面向切面编程。

AOP 是为了解决主要业务逻辑与次要业务逻辑之间耦合问题的。主要业务逻辑比如登陆、数据获取、查询等,次要业务逻辑比如性能监控、异常处理等等,次要业务逻辑往往有:不重要、和业务关联度低、贯穿多处业务逻辑的特性,如果没有好的设计模式,只能在业务代码里将主要逻辑与次要逻辑混合起来,但 AOP 可以做到主要、次要业务逻辑隔离。

使用 AOP 就是在定义在哪些地方(类、方法)切入,在什么地方切入(方法前、后、前后)以及做什么。

比如说,我们想在某个方法前后分别执行两个函数计算执行时间,下面是主要业务逻辑:

代码语言:javascript复制
@Component("work")
public class Work {
  public void do() {
    System.out.println("执行业务逻辑");
  }
}

再定义切面方法:

代码语言:javascript复制
@Component
@Aspect
class Broker {
  @Before("execution(* xxx.Work.do())")
  public void before(){
    // 记录开始时间
  }

  @After("execution(* xxx.Work.do())")
  public void after(){
    // 计算时间
  }
}

再通过 xml 定义扫描下这两个 Bean,就可以在运行 work.do() 之前执行 before(),之后执行 after()

还可以完全覆盖原函数,利用 joinPoint.proceed() 可以执行原函数:

代码语言:javascript复制
@Component
@Aspect
class Broker {
  @Around("execution(* xxx.Work.do())")
  public void around(ProceedingJoinPoint joinPoint) {
    // 记录开始时间

    try {
      joinPoint.proceed();
    } catch (Throwable throwable) {
      throwable.printStackTrace();
    }

    // 计算时间
  }
}

关于表达式 "execution(* xxx.Work.do())" 是用正则的方式匹配,* 表示任意返回类型的方法,后面就不用解释了。

可以看到,我们可以在不修改原方法的基础上,在其执行前后增加自定义业务逻辑,或者监控其报错,非常适合做次要业务逻辑,且由于不与主要业务逻辑代码耦合,保证了代码的简洁,且次要业务逻辑不容易遗漏。

总结

IOC 特别适合描述业务模型,后端天然需要这一套,然而随着前端越做越重,如果某个业务场景下需要将部分业务逻辑放到前端,也是非常推荐使用 IOC 设计模式来做,这是后端沉淀了近 20 年的经验,没有必要再另辟蹊径。

AOP 对前端有帮助但没有那么大,因为前端业务逻辑较为分散,如果要进行切面编程,往往用 window 事件监听来做会更彻底,可能这都是前端没有流行 AOP 的原因。当然前端约定大于配置的趋势下,比如打点或监控都集成到框架内部,往往也做到了业务代码无感,剩下的业务代码也就没有 AOP 的需求。

最后,spring 的低侵入式设计,使得业务代码不用关心框架,让业务代码能够快速在不同框架间切换,这不仅方便了业务开发者,更使得 spring 走向成功,这是前端还需要追赶的。

版权声明:自由转载-非商用-非衍生-保持署名(创意共享 3.0 许可证)

0 人点赞