简介
Spring 是一款开源的 J2EE 框架,它有许多项目,为 Java 应用开发提供了一整套的工具,其中最核心的就是 Spring Framework 和 Spring Boot 项目。
文本是一个系列文章的第一篇,下面就这两个项目的核心内容做一些速查整理,同时辅以生产源码,便于理解。
相关文章
- Spring 速查指南(一)- 依赖注入
- Spring 速查指南(二)- 环境、资源、事件、定时任务
- Spring 速查指南(三)- SpEL & 缓存
- Spring 速查指南(四)- Spring AOP
- Spring 速查指南(五)- Spring Web
环境
有时候应用程序需要根据当前运行时的环境(开发、测试、生产)进行不同的处理,Spring 提供了 Environment 接口,可以用来获取当前运行时的 Profile、属性、环境变量等。
要获取 Environment 接口的实例,可以参考指南一通过注入实例、Aware 接口或先获取 ConfigurableApplicationContext,从它的 getEnvironment 方法等方式获取。
获取当前激活的 Profile
获取 Environment 接口的实例后,它的 getActiveProfiles 方法可以获取当前激活的 Profile(也等价于配置 spring.profiles.active
)。
ConfigurableEnvironment 接口(Environment 子接口)还提供了 setActiveProfiles 方法,来设置当前激活的 Profile。
@Profile 注解
@Profile 注解可以指定 Bean 在特定的 Profile 下才注册。
@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();
}
}
这个配置类只在 development 时才注册。当需要多个 Profile 时,可以使用逻辑运算符(!非、& 和、| 或)和括号。
也可以写一个自定义注解,更便于理解。
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Profile("production")
public @interface Production {
}
这等价于 @Profile("production")
。
@Profile 注解也可以和 @Bean 注解一起用在方法上。
获取属性
可以通过指南一提到的 @Value 注解获取属性,也可以从 Environment 接口获取属性,还可以获取环境变量、命令行 -D 参数等。
@SpringBootApplication
public class Application {
public static void main(String[] args) {
ConfigurableApplicationContext context = SpringApplication.run(Application.class, args);
Environment env = context.getEnvironment();
String port = env.getProperty("server.port");
String path = env.getProperty("server.servlet.context-path");
System.out.println("^O^ 后台服务启动成功 o(^@^)o");
System.out.println("服务地址:http://localhost:" + port + path);
}
}
当然不同的来源可能重名,获取时有个优先级:
- ServletConfig 参数(Web 应用)
- ServletContext 参数(web.xml 配置)
- JNDI 环境变量(java:comp/env/ 条目)
- JVM 系统属性(命令行 -D 参数)
- JVM 环境变量(包含系统环境变量)
资源
在应用程序中往往需要载入资源(如配置文件、图片等),Spring 提供了 Resource 接口(位于 org.springframework.core.io 包)来代表资源,可以用来获取文件内容、URL、流等。
内置常用的 Resource 实现有:
- UrlResource:代表一个 URL 表示的资源,如
file:
,https:
,ftp:
等 - ClassPathResource:代表从 classpath 获取的资源
- FileSystemResource:代表文件系统资源,操作的是 java.io.File 对象
- PathResource:代表的是路径资源,操作的是 java.nio.file.Path 对象
- ServletContextResource:代表 ServletContext 的资源
- InputStreamResource:代表 InputStream 的资源,如果可以的话,优先应该使用 ByteArrayResource 或其他文件资源
- ByteArrayResource:代表 ByteArrayInputStream 的资源
我们可以通过 ResourceLoader 接口来获取资源,可根据资源类型自动返回对应的实现,ApplicationContext 就继承了这个接口或者使用 ResourceLoaderAware 注入。
Resource template = contetxt.getResource("some/resource/path/myTemplate.txt");
也可以指定资源类型
// 载入 classpath 资源
Resource template = contetxt.getResource("classpath:some/resource/path/myTemplate.txt");
// 载入 URL 文件资源
Resource template = contetxt.getResource("file:///some/resource/path/myTemplate.txt");
// 载入 URL https 资源
Resource template = contetxt.getResource("https://myhost.com/resource/path/myTemplate.txt");
ResourcePatternResolver 接口是 ResourceLoader 接口的子接口,可以支持通配符(*)。
如果配置文件指定了资源路径,我们甚至可以快速创建资源,而不用关心资源的类型。
@Component
public class MyBean {
private final Resource template;
public MyBean(@Value("${template.path}") Resource template) {
this.template = template;
}
// ...
}
如果配置中使用了通配符,则支持数组。
@Component
public class MyBean {
private final Resource[] templates;
public MyBean(@Value("${templates.path}") Resource[] templates) {
this.templates = templates;
}
// ...
}
异步处理
异步处理可用于事件处理或定时任务处理等多线程异步处理的场景。
首先需要启用配置,在主类或配置类上添加 @EnableAsync 注解。然后在方法上添加 @Async 注解即可,它们就像普通方法一样调用,但是由 TaskExecutor 来异步执行方法。
@Async
void doSomething(String s) {
// 异步执行代码
}
@Async 注解标记的异步方法的返回值没有用,一般都是 void,除非是 Future。
异步方法中的异常处理
如果异步方法返回 Future 对象,则比较容易处理异常,在 Future 对象上调用 get 时会抛出异常。如果异步方法返回 void,则无法捕获异常。可以通过实现 AsyncUncaughtExceptionHandler 来处理异常。
public class MyAsyncUncaughtExceptionHandler implements AsyncUncaughtExceptionHandler {
@Override
public void handleUncaughtException(Throwable ex, Method method, Object... params) {
// 处理异常
}
}
任务执行器
Spring 使用 TaskExecutor 接口的实例来执行异步任务。Spring 根据场景提供了许多内置的实现。
- SyncTaskExecutor:这种实现方式不会异步运行调用,而是都在调用线程中进行。它主要用于不需要多线程的情况,如简单的测试用例。
- SimpleAsyncTaskExecutor:这种实现方式不会重复使用任何线程。而是为每次调用启动一个新线程。不过,它支持并发限制,可以阻止任何超过限制的调用,直到有空闲的槽位。
- ConcurrentTaskExecutor:该实现是 java.util.concurrent.Executor 实例的适配器。很少直接使用这个实现,而是用 ThreadPoolTaskExecutor,除非需要定制化或 ThreadPoolTaskExecutor 无法满足需求。
- ThreadPoolTaskExecutor:这种实现最常用。
- DefaultManagedTaskExecutor:这种实现主要是兼容 JSR-236 标准。
如果需要自定义任务执行器配置,可以实现 AsyncConfigurer 接口,通过 getAsyncExecutor 方法返回 Executor。
@Configuration
@EnableAsync
@RequiredArgsConstructor
@Slf4j
public class AsyncConfig implements AsyncConfigurer {
@Override
public Executor getAsyncExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
// 自定义配置
executor.setThreadNamePrefix('--');
executor.setMaxPoolSize(10);
executor.setWaitForTasksToCompleteOnShutdown(true);
// 拒绝处理策略:抛出异常
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.AbortPolicy());
executor.initialize();
return executor;
}
@Override
public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
// 异常处理
return (ex, method, params) -> log.error("{} 发生异常:{}", method.getName(), ex.getMessage());
}
}
事件处理
Spring 支持事件驱动的处理方式,可以有效地在业务模块间解耦。事件处理主要涉及三个核心对象:
- 事件源:发送事件的对象
- 事件对象:一个可序列化的对象,包含了事件的所有信息
- 事件监听器:当接收到事件后执行相应的操作
事件源
事件源主要是通过 ApplicationEventPublisher 对象来发布事件,要获取 ApplicationEventPublisher 接口的实现,可以参考指南一通过注入 ApplicationEventPublisher 接口的实例,或实现 ApplicationEventPublisherAware 接口注入,或获取 ApplicationContext(也继承了 ApplicationEventPublisher 接口)实例,其他工具类(如 Hutool 的 SpringUtil)等方法。
this.getApplicationEventPublisher().publishEvent(event);
事件对象
事件对象可以继承自 ApplicationEvent 抽象类,或者是其他自定义类型(Spring 会自动包装)。
// 自定义事件类
public class MyEvent extends ApplicationEvent {
private final String content;
public MyEvent(Object source, String content) {
super(source);
this.content = content;
}
}
事件监听器
事件监听器是处理事件的业务对象,可以在任意组件中通过 @EventListener 注解标记的方法来实现。
// 事件监听器
@Component
public class MyEventListener {
@EventListener
public void handleMyEvent(MyEvent event) {
System.out.println("接收到事件: " + event.toString());
// 事件处理
}
}
@EventListener 注解可支持多个类型的事件,还可以支持 SpEL 表达式筛选。
@EventListener(condition = "#event.content == 'my-event'")
public void processMyEvent(MyEvent event) {
// 事件处理
}
如果想处理完事件后发送另一个事件,则可以添加事件作为返回值。
@EventListener
public AnotherEvent handleMyEvent(MyEvent event) {
// 处理事件并返回另一个事件
}
Spring 会根据事件的类型来分发事件,这样的监听方式是同步执行的,也就是发布事件后会阻塞,直到该事件的所有事件监听器处理完。如果要异步执行,则需要添加 @Async 注解(也要记得启用 @EnableAsync),详见上面异步处理。
使用异步事件处理要注意以下 3 点:
- 异步事件处理中抛出的异常不会反馈给事件发布者
- 异步事件处理方法不支持返回值,如果需要发布另一个事件,需要用 ApplicationEventPublisher 再次发布。
- 部分异步实现(如 ThreadLocal)不支持异步事件处理
另一种实现监听器的方式是组件实现 ApplicationListener 接口并注册到 IoC 容器中。该接口有个 onApplicationEvent 方法来处理事件,事件类型通过范型来指定。
如果需要让事件监听器按某个顺序处理事件,可以在处理方法或类上添加 @Order 注解。
Spring 内置事件
Spring 有许多内置事件,可以用来与 IoC 容器的生命周期交互。
- ContextRefreshedEvent:ApplicationContext 初始化或刷新后发布
- ContextStartedEvent:ApplicationContext 使用 start() 方法启动后发布
- ContextStoppedEvent:ApplicationContext 使用 stop() 方法停止后发布
- ContextClosedEvent:ApplicationContext 使用 close() 方法关闭后发布
- RequestHandledEvent:Web 应用中 HTTP 请求完成后发布
- ServletRequestHandledEvent:RequestHandledEvent 的子类,添加了 Servlet 上下文信息
详细列表可参考官方文档。
定时任务
Spring 提供了一个简单的定时任务调度能力,类似于 Linux Crontab,支持固定间隔或 Cron 表达式来定制执行的周期。使用 @EnableScheduling 注解启用,然后在方法上加上 @Scheduled 注解即可。
@Scheduled(fixedDelay = 5000)
public void doSomething() {
// 周期执行代码
}
默认的单位是毫秒,可以通过 timeUnit 参数指定单位。它有几个参数,分别是不同的调度规则。
- fixedDelay:代表每隔一定时间执行,执行完成开始计算
- fixedRate:代表固定的间隔时间执行(即启动完成就开始计算时间)
- initialDelay:对于 fixedDelay 或 fixedRate 的任务,第一次执行的延迟时间
- cron:配置 Cron 表达式,参考配置
@Scheduled 当然也可以和 @Async 结合使用,提供异步定时任务,详见上面异步处理。
如果需要更强大的调度能力,就可以整合第三方库,例如 Quartz,这里不再展开。
空安全
Java 中的对象都可以为空,如果不加以检查,容易在运行时产生空指针异常。Spring 提供了 4 个注解用来处理空指针问题。
- @Nullable:标记参数、返回值或字段可能为 null
- @NonNull:标记参数、返回值或字段不为 null
- @NonNullApi:标记整个包,默认的参数和返回值不为 null
- @NonNullFields:标记整个包,默认的字段不为 null
注:这 4 个注解只是标记,不是真正的解决空指针问题。使用后在 Idea 这些 IDE 中就会提示,显示在问题中(IDE 也支持其他类似注解)。可以在团队中作为开发规范。
另外,Kotlin 中真正地解决了空指针问题,参考这篇文章中的空值。
(未完待续)
如果觉得有用,请多多支持,点赞收藏吧!