Skip to main content

回到 Spring

简介

本系列用最快的速度过一遍 Spring Cloud,Spring 组件和微服务技术。

网上现有很多教程都讲的太细太复杂,重点是太长了,不适合快速入门。本系列将尽量简化,只讲最重要的部分。

本文假设你会最基础的 Spring Web,只要会用 Spring Web 开发一个简单的 RESTful 服务就可以了。此外,你需要会使用 docker,因为我们会用到 docker 来启动一些服务,而且 k8s 中也不会讲 docker。当然,你可以把这些服务部署到云上或本机,但这系列文章中只会提供 docker-compose 文件。

但是在讲解 Spring Cloud 之前,我们需要学习一些速成 Spring Boot 可能没讲到的 Spring 的基础知识。

Spring 简介

Spring 是一个 Java 开发框架,它提供了很多功能,比如依赖注入、AOP、事务管理等等。其中最重要的是依赖注入,这是 Spring 的核心功能。

Spring 包含了很多包,例如 Spring Web、Spring Data、Spring Security 等等。

依赖注入

Spring 的依赖注入是通过Bean实现的,具体而言,你可以创建许多Config类,每个Config类都可以有多个Bean方法,每个Bean方法都规定了某一类对象的创建方式。Spring 会在启动时扫描所有的Config类,然后根据Bean方法的规则创建对象。

@Configuration
class MyConfig {
@Bean
public MyService myService() {
return new MyService();
}
}

这样,就规定了当MyService对象使用注入方法创建时,实际上是调用myService方法创建的。

如果要用依赖注入的方法获得注入的对象,使用 Spring Application Context。

public class SomeClass {
public void someFunc() {
ApplicationContext context = new AnnotationConfigApplicationContext(MyConfig.class);
MyService myService = context.getBean(MyService.class);
}
}

Application Context 包含了所有的Bean对象,可以通过getBean方法获得。上面一种使用的是基于 Annotation,即 Configuration 注解类获得 Context 对象,然后基于类对象反射获得 Bean 对象。

Spring 也支持 XML 配置,即在 XML 文件中配置 Bean 对象,然后通过 XML 文件获得 Context 对象,不过这种方法已经不常用了。

<beans>
<bean id="myService" class="com.example.MyService"/>
</beans>
public class SomeClass {
public void someFunc() {
ApplicationContext context = new ClassPathXmlApplicationContext("my-config.xml");
MyService myService = context.getBean(MyService.class);
}
}

这里也可以不基于类对象获得 Bean 对象,而是基于 Bean 的 ID 获得。

MyService myService = (MyService) context.getBean("myService");

当然,注解里也可以改变 Bean 的 ID。

@Bean(name = "myService")
public MyService myService() {
return new MyService();
}

除了手动创建配置类,还可以直接使用@Component注解。这个注解本身并不做什么,只有一个value属性。如果不写,默认用类名首字母小写作为 Bean 的 ID。

@Component("myService")
public class MyService {
}

我们常用的一些注解,如@RestController@Service@Repository@Configuration,都包含了@Component

但是,Spring 有另一个注解@ComponentScan,这个注解会扫描指定包下的所有@Component注解类,并将其注册为 Bean 对象到当前这个类下。例如,

@ComponentScan("com.example")
@Configuration
public class MyConfig {
}

Bean 的名字就是 Component 的 value 属性。之后就同之前一样获得上下文和对象即可。

事实上,我们每个 Spring 启动的 run 方法都会创建一个 Application Context,这个 Context 包含了所有的 Bean 对象。

@SpringBootApplication
public class DemoApplication {

public static void main(String[] args) {
ApplicationContext context = SpringApplication.run(DemoApplication.class, args);
}

}

前面我们说过,context 需要基于Config类创建,那这里的 Context 的 Config 在哪里?如果你去查看@SpringBootApplication的源码,你会发现这个注解是这样的,

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(excludeFilters = { @Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),
@Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) })
public @interface SpringBootApplication {
// ...
}

是的,它本身就有@ComponentScan注解。此外,如果再去看@SpringBootConfiguration,你会发现它包含了@Configuration注解。所以,@SpringBootApplication包含了@Configuration@ComponentScan

当然,比起这种传统的创建 Bean 的方法,你应该更熟悉@Resource@Autowired@Qualifier@Inject等注解。这些注解是 Spring 的依赖注入的核心。

@Inject@Autowired是等价的,是依赖类型注入,分别由 Java 和 Spring 提供。@Resource@Qualifier是依赖名称注入,分别由 Java 和 Spring 提供。一般推荐使用 Spring 提供的注解。

@Autowired@Qualifier的用法基本相同,只是依赖 ID 和类名的区别。

@Component
public class SomeClass {

}

public class AnotherClass {
@Autowired
private final SomeClass someClass;

public void someFunc(@Autowired @Qualifier("someClass") SomeClass someClass) {
this.someClass = someClass;
}
}

根据 Spring 的官方文档,还有 setter 注入法等等。但是,最推荐的方法是使用构建器注入加不可变私有字段。即下面这种方法。

@Component
public class SomeClass {

}

public class AnotherClass {
private final SomeClass someClass;

@Autowired
public AnotherClass(SomeClass someClass) {
this.someClass = someClass;
}
}

或,

public class AnotherClass {
private final SomeClass someClass;

public AnotherClass(@Autowired SomeClass someClass) {
this.someClass = someClass;
}
}

或者,你可以直接使用 lombok 的@RequiredArgsConstructor注解。

@RequiredArgsConstructor
public class AnotherClass {
private final SomeClass someClass;
}

AOP (Aspect-Oriented Programming)

AOP 即 Aspect-Oriented Programming,面向切面编程。这只是设计模式中模版方法模式的一种实现,即在方法执行前后插入一些代码。

Spring 的 AOP 是基于代理的,即创建一个代理对象,然后在代理对象的方法中插入一些代码。

具体而言,如果要为某个类的某个方法添加切面,即在不改变原有方法的情况下添加一些代码,可以使用@Aspect注解。

@Aspect
@Component
public class MyAspect {
@Before("com.example.MyService.s")
public void before() {
System.out.println("Before");
}

@After("execution(* com.example.MyService.*(..))", returning = "retVal")
public void after(Object retVal) {
System.out.println("After: " + retVal);
}
}

注意,切面类只是创建了一个代理对象,而不是更改了原对象的方法。如果直接使用原类来创建对象,切面是不会生效的。

@After@Before里的第一个参数是切入点表达式,即要切入的方法。

  • execution(* com.example.service..(..)):匹配com.example.service包下所有类的所有方法。
  • within(com.example.service..*):匹配com.example.service包及其子包下的所有类的所有方法。
  • bean(myBean):匹配名为myBean的bean的所有方法。
  • @annotation(org.springframework.transaction.annotation.Transactional):匹配所有带有@Transactional注解的方法。

After 的returning属性是返回值,可以用来获取返回值。

这样子创建切面类后,Spring 会在运行时创建一个代理对象,然后在代理对象的方法中插入一些代码。

要使用这个代理对象,需要在@SpringBootApplication类中添加@EnableAspectJAutoProxy注解。

@SpringBootApplication
@EnableAspectJAutoProxy
public class DemoApplication {

public static void main(String[] args) {
ApplicationContext context = SpringApplication.run(DemoApplication.class, args);
}

}

这样,使用@Autowired注入的对象就是代理对象了。

由于 AOP 在实际开发中用的不多,这里就不再展开讲解了。

Value 注解

虽然@Value注解并不能算是 Spring 的核心功能,但是在实际开发中用的很多,因此这里也简单介绍一下。

@Value注解可以用来注入配置文件中的属性。

@Component
public class MyService {
@Value("${my.property}")
private String property;
}

这样,Spring 会在启动时读取配置文件中的my.property属性,并将其注入到property字段中。这个配置文件就是application.yml,或bootstrap.yml(当然,也可以是其他格式的文件)。后者是在之后的 Spring Cloud 中用到的。如果配置文件没有,还会找环境变量,环境变量中,点号会被默认替换为下划线。

my:
property: value

等价于有一个环境变量MY_PROPERTY,值为value

但是,这样子只能注入字符串。如果要注入其它类型,例如一个整数,需要使用 Spring 值表达式,即#{}

@Component
public class MyService {
@Value("#{${my.property} + 1}")
private int property;
}

这个含义是,将my.property的值加一后注入到property字段中。

Spring Boot 简介

Spring Boot 不是 Spring 的某个模块。而是 Spring 的某个模块加上一些默认配置。以 Spring Data JPA 举例,它的核心功能在org.springframework.data:spring-data-jpa,即这个 maven repository,而 Spring Boot Starter Data JPA 是 Spring Data JPA 加上一些默认配置,它在org.springframework.boot:spring-boot-starter-data-jpa,即这个 maven repository

可以这样打比方,Spring Framework 是钢筋混泥土框架,其中的内饰、家具、电器等等都是 Spring 的各种模块,例如 Spring Web、Spring Data、Spring Security 等等。每个模块的 Spring Boot Starter 包即是包含了装修服务的内饰、家具、电器等等。如果安装完你觉得不满意,你可以自己再做小改动,而不必从头自己完成所有的装修工作。

具体到编程上,Spring Boot Starter 版的工具除了原本的框架外,还提供了一系列默认的 Config 类,如果需要自定义,只需要创建一个 Config 类,然后在其中添加@Bean注解,覆盖掉默认的 Bean 对象即可。

Spring Boot 配置的优先级顺序是,

  1. 命令行参数
  2. application.properties或application.yml(在config子目录或当前目录)
  3. application.properties或application.yml(在类路径的根目录)
  4. 通过@PropertySource注解加载的属性文件
  5. 默认属性(通过SpringApplication.setDefaultProperties指定)

Spring Framework 自己也有许多的配置,例如我们上面讲的@Configuration@Bean等等,因此它也有自己的 Spring Boot Starter,即org.springframework.boot:spring-boot-starter

注意,所有的 Spring Boot 都必须统一一个版本,否则会出现各种问题。它们的版本号总是保持一致的。

Spring Cloud 简介

Spring Cloud 与其说是一个统一的框架,不如说是一系列工具的集合。这些工具都是为了解决微服务架构中的一些问题而设计的。

这一系列工具中,有一些类似于编程中的接口,有一些类似于这些接口的实现。例如,Spring Cloud Circuit Breaker 是类似于接口,它有实现 Resilience4j 和 Hystrix。当然,有一些工具是没有统一的接口的,例如服务注册中心,Consul,Nacos,Eureka 是三个实现,但他们不遵循统一的接口。

当我们说 Spring Cloud 时,这也是一个类似与接口的东西,它也有实现,例如 Spring Cloud Netflix,Spring Cloud Alibaba 等等,当然你也可以自己选择一些工具来组合,完成 Spring Cloud 的功能。