学习链接:https://www.liaoxuefeng.com/wiki/1252599548343744/1266263217140032#0

大部分内容来自此教程,以下只是我的学习笔记。

Spring概念

Spring是一个可以快速开发JavaEE的框架,提供了一系列底层容器和基础设置,可以和大量常用的开源框架无缝集成。在Spring Framework的基础上,后续又延伸了Spring Boot、Spring Cloud等流行框架。因此,在接触这些框架之前,我们需要先了解核心的Spring框架内容。

核心模块:

  • Spring Core Container
    • Spring Bean
    • IoC
  • Spring AOP (Aspect-Oriented Programming)
  • Spring Data Access/Integration
    • JDBC支持
    • ORM支持
    • 事务管理
  • Spring Web
    • Spring MVC
    • Web View
    • RESTful Web Service
  • Spring Security
  • Spring Test

容器

容器是一种为某种特定组件的运行提供必要支持的一个软件环境和许多底层服务。例如Tomcat为Servlet提供的环境、Docker提供的Linux环境等。我们主要介绍Spring提供的容器:IoC容器。

IoC (Inversion of Control) 控制反转

在传统的方法中,是由程序来控制各种组件的创建和销毁等,会产生很多问题,比如依赖问题、无法共享问题、紧耦合等问题。

而使用IoC,可以将这个控制权转移到IoC容器中,所有组件不再由应用程序自己创建和配置,而是由IoC容器负责,这样,应用程序只需要直接使用已经创建好并且配置好的组件Bean。

为了能让组件在IoC容器中被“装配”出来,需要某种“注入”机制。也就是常说的依赖注入(Dependency Injection):是指将一个对象所依赖的其他对象注入到该对象中,而不是在该对象内部创建它们。通过DI,对象可以从容器中获取它所需的依赖,从而实现松耦合。

IoC的优点:

  • 松耦合
  • 可测试性
  • 可维护性
  • 可重用性

传统和IoC管理的代码对比如下:

  1. 传统方法
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
public class BookService {
    private HikariConfig config = new HikariConfig();
    private DataSource dataSource = new HikariDataSource(config);

    public Book getBook(long bookId) {
        try (Connection conn = dataSource.getConnection()) {
            ...
            return book;
        }
    }
}
  1. 采用IoC
1
2
3
4
5
6
7
public class BookService {
    private DataSource dataSource;

    public void setDataSource(DataSource dataSource) {
        this.dataSource = dataSource;
    }
}

IoC的使用

在使用之前,需要在maven中引入spring-context

  1. UserService通过setMailService()注入了一个MailService
1
2
3
4
5
6
7
8
9
public class UserService {
    private MailService mailService;

    public void setMailService(MailService mailService) {
        this.mailService = mailService;
    }

	...
}
  1. 编写application.xml配置文件,告知如何创建和组装。通过反射完成。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
<?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="userService" class="com.itranswarp.learnjava.service.UserService">
        <property name="mailService" ref="mailService" />
    </bean>

    <bean id="mailService" class="com.itranswarp.learnjava.service.MailService" />
</beans>
  • 每个<bean ...>都有一个id标识,相当于Bean的唯一ID;
  • userServiceBean中,通过<property name="..." ref="..." />注入了另一个Bean;
  • Bean的顺序不重要,Spring根据依赖关系会自动正确初始化。

相当于:

1
2
3
UserService userService = new UserService();
MailService mailService = new MailService();
userService.setMailService(mailService);

如果注入的是booleanintString这样的数据类型,则通过value注入。

  1. 创建IoC容器实例,加载配置文件
1
ApplicationContext context = new ClassPathXmlApplicationContext("application.xml");
  1. 获取装配好的Bean
1
2
3
4
// 获取Bean:
UserService userService = context.getBean(UserService.class);
// 正常调用:
User user = userService.login("bob@example.com", "password");

除了ApplicationContext之外,另一种IoC容器叫BeanFactory。两者的在于,后者是按需创建,即第一次获取Bean时才创建这个Bean。实际上,ApplicationContext接口是从BeanFactory接口继承而来的,并且,ApplicationContext提供了一些额外的功能,包括国际化支持、事件和通知机制等。通常情况下,我们总是使用ApplicationContext,很少会考虑使用BeanFactory

Annotation配置

使用Annotation,可以简化配置,使得无需将每一个依赖关系都写在配置文件中。可以完全不需要XML,让Spring自动扫描Bean并组装它们。包含这几个步骤:

  1. 给类MailService添加一个@Component注解和注入注解Autowired
1
2
3
4
5
6
7
@Component
public class UserService {
    @Autowired
    MailService mailService;

    ...
}

2.写一个启动类容器

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
@Configuration
@ComponentScan
public class AppConfig {
    public static void main(String[] args) {
        ApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
        UserService userService = context.getBean(UserService.class);
        User user = userService.login("bob@example.com", "password");
        System.out.println(user.getName());
    }
}

@Configuration表示这是一个配置类。@ComponentScan告诉容器,自动搜索当前类所在的包以及子包,把所有标注为@Component的Bean自动创建出来,并根据@Autowired进行装配。

需要特别注意:特别注意包的层次结构。通常来说,启动配置AppConfig位于自定义的顶层包(例如com.itranswarp.learnjava),其他Bean按类别放入子包。

定制Bean

本节介绍了几种类型的定制Bean,即通过不同的注解来实现。

  1. @Scope注解 默认当我们使用@Component标记Bean时,容器初始化时就会创建Bean,属于单例(singleton)的情况。也就是getBean(Class)获取到的都是一个实例。当要实现getBean(Class)获取到的都是新实例时(也就是原型Prototype的概念),需要额外的@Scope注解。

  2. 注入List 指的是存在若干类实现某个interface,那么在入口类中通过引入以这个interface为类型的List,则为注入List。Spring会自动把所有类型为Validator的Bean装配为一个List注入进来。如以下代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
@Component
public class Validators {
    @Autowired
    List<Validator> validators;

    public void validate(String email, String password, String name) {
        for (var validator : this.validators) {
            validator.validate(email, password, name);
        }
    }
}
  1. 可选注入 @Autowired(required = false) 当注入的是不存在的Bean时,会有NoSuchBeanDefinitionException异常抛出。可以使用在注入注解中加入required = false参数,表示若没有这个Bean,则忽略。

这种方式非常适合有定义就使用定义,没有就使用默认值的情况。

  1. @Bean注解 一个Bean不在我们自己的package管理之内,要如何注入呢? 答案是我们自己在@Configuration类中编写一个Java方法创建并返回它,注意给方法标记一个@Bean注解:
1
2
3
4
5
6
7
8
9
@Configuration
@ComponentScan
public class AppConfig {
    // 创建一个Bean:
    @Bean
    ZoneId createZoneId() {
        return ZoneId.of("Z");
    }
}
  1. 初始化和销毁方法注解 分别使用@PostConstruct@PreDestroy注解来标记初始化和清理方法。

  2. 使用别名 默认情况下,对一种类型的Bean,容器只创建一个实例。当需要对一种类型的Bean创建多个实例时(例如连接多个数据库),可以用以下两种方法指定别名来区分:

  • Bean("name")
  • @Qualifier("name")

还可以使用@Primary将某个Bean指定为主Bean,适用于存在多个相同类型的Bean时,如果没有指定名称,则默认使用主Bean。

  1. FactoryBean 工厂模式 通过创建一个Bean工厂类,用它来创建Bean。实例代码:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
@Component
public class ZoneIdFactoryBean implements FactoryBean<ZoneId> {

    String zone = "Z";

    @Override
    public ZoneId getObject() throws Exception {
        return ZoneId.of(zone);
    }

    @Override
    public Class<?> getObjectType() {
        return ZoneId.class;
    }
}

Resource加载文件

当需要读取配置文件、资源文件时,Spring提供了Resource对象来将文件注入进来,避免繁琐的代码。示例如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
@Component
public class AppService {
	// 可以是指定classpath,或者给出具体的路径
    @Value("classpath:/logo.txt")
    private Resource resource;

    private String logo;

    @PostConstruct
    public void init() throws IOException {
        try (var reader = new BufferedReader(
                new InputStreamReader(resource.getInputStream(), StandardCharsets.UTF_8))) {
            this.logo = reader.lines().collect(Collectors.joining("\n"));
        }
    }
}

直接调用resource.getInputStream()就可以获取到输入流

注入配置

方式1

除了使用上面的Resource对象来读取配置文件,还可以使用更简单的@PropertySource实现自动读取配置文件。文中给的例子:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
@Configuration
@ComponentScan
@PropertySource("app.properties") // 表示读取classpath的app.properties
public class AppConfig {
    @Value("${app.zone:Z}")
    String zoneId;

    @Bean
    ZoneId createZoneId() {
        return ZoneId.of(zoneId);
    }
}

定义了配置文件的路径后,就可以自动读取此配置文件。并且使用@Value进行配置值的注入。@Value注入有两种语法:

  • @Value("${app.zone}"):读取key为app.zone的值,若key不存在,启动会报错
  • @Value("${app.zone:Z}"):与上个用法不同的是,当key不存在时,则使用默认值Z

也可以把注入直接应用到方法的参数中:

1
2
3
4
@Bean
ZoneId createZoneId(@Value("${app.zone:Z}") String zoneId) {
    return ZoneId.of(zoneId);
}

方式2

先用一个JavaBean持有配置,然后在其他类中使用这个JavaBean的变量来实现注入。例子如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
@Component
public class SmtpConfig {
    @Value("${smtp.host}")
    private String host;

    @Value("${smtp.port:25}")
    private int port;

    public String getHost() {
        return host;
    }

    public int getPort() {
        return port;
    }
}
1
2
3
4
5
6
7
8
@Component
public class MailService {
    @Value("#{smtpConfig.host}")
    private String smtpHost;

    @Value("#{smtpConfig.port}")
    private int smtpPort;
}

注意:使用JavaBean读取属性的方法,用的是#而不是$号。

条件装配

概念:根据指定的条件是否满足,实现bean的创建与否。给出了两种方式:

@profile

根据Profile的设置,决定是否创建。示例代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
@Configuration
@ComponentScan
public class AppConfig {
    @Bean
    @Profile("!test")
    ZoneId createZoneId() {
        return ZoneId.systemDefault();
    }

    @Bean
    @Profile("test")
    ZoneId createZoneIdForTest() {
        return ZoneId.of("America/New_York");
    }
}

在启动程序的JVM参数设置-Dspring.profiles.active=test可以指定以test环境。此时调用方法2创建ZoneId。

@Conditional

1
2
3
4
5
@Component
@Conditional(OnSmtpEnvCondition.class)
public class SmtpMailService implements MailService {
    ...
}
1
2
3
4
5
public class OnSmtpEnvCondition implements Condition {
    public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
        return "true".equalsIgnoreCase(System.getenv("smtp"));
    }
}

上面是使用@Conditional实现条件装配,下面是定义Conditional条件(OnSmtpEnvCondition)的代码。

除此之外,还有其他形式的@Conditional 写法。比如:

1
2
3
4
5
@Component
@ConditionalOnProperty(name = "app.storage", havingValue = "file", matchIfMissing = true)
public class FileUploader implements Uploader {
    ...
}

上面的程序表示:当配置文件的app.storage=file,则使用此方法。