spring
Spring Framework主要包括几个模块:
- 支持IoC和AOP的容器;
- 支持JDBC和ORM的数据访问模块;
- 支持声明式事务的模块;
- 支持基于Servlet的MVC开发;
- 支持基于Reactive的Web开发;
- 以及集成JMS、JavaMail、JMX、缓存等其他模块。
6(>= 17)和5不同
IoC容器
容器:软件环境
Spring的核心就是提供了一个IoC容器,它可以管理所有轻量级的JavaBean组件,提供的底层服务包括组件的生命周期管理、配置和组装服务、AOP支持,以及建立在AOP基础上的声明式事务服务等。
创建组件时,依赖共同的组件,导致资源获取和释放重复和困难,依赖关系复杂
因此,核心问题是:
- 谁负责创建组件?
- 谁负责根据依赖关系组装组件?
- 销毁时,如何按依赖顺序正确销毁?
IoC:控制反转,控制权从应用程序转移到了IoC容器,所有组件不再由应用程序自己创建和配置,而是由IoC容器负责,应用程序只需要直接使用已经创建好并且配置好的组件。为了能让组件在IoC容器中被“装配”出来,需要某种“注入”机制。因此,IoC又称为依赖注入
将组件的创建+配置与组件的使用相分离,并且,由IoC容器负责管理组件的生命周期。
最简单的配置是通过XML文件来告诉IoC容器如何创建组件,以及各组件的依赖关系
所有组件统称为JavaBean,即配置一个组件就是配置一个Bean
Spring的IoC容器同时支持属性注入(set)和构造方法注入,并允许混合使用。
装配Bean
xml Schema的格式是固定的,写<bean>
的部分
<bean id = "" class = "">
// 注入bean
<property name="" ref="" />
// 注入boolean、int、String
<property name="maximumPoolSize" value="10" />
</bean>
Main中
创建IoC容器示例,加载配置文件
public class Main {
public static void main(String[] args) {
ApplicationContext context = new ClassPathXmlApplicationContext("application.xml");
// 根据Bean的类型获取Bean的引用
UserService userService = context.getBean(UserService.class);
User user = userService.login("bob@example.com", "password");
System.out.println(user.getName());
}
}
不想写xml怎么办?
使用Annotation配置
@Component
定义Bean,默认名称是小写开头的类名
@Autowired
可以写在set()方法上,还可以直接写在字段上,甚至可以写在构造方法中(传参上)
一般把@Autowired写在字段上,通常使用package权限的字段,便于测试。
编写一个AppConfig类启动容器,和上面的Main差不多,实现类不同
@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进行装配。
- 每个Bean被标注为
@Component
并正确使用@Autowired
注入; - 配置类被标注为
@Configuration
和@ComponentScan
; - 所有Bean均在指定包以及子包内。
注意包的层次结构。通常来说,启动配置AppConfig
位于自定义的顶层包(例如com.itranswarp.learnjava
),其他Bean按类别放入子包。
Bean
普通的是单例(Singleton),调用getBean(Class)
获取到的Bean总是同一个实例
Prototype(原型)Bean,每次调用getBean(Class)
,容器都返回一个新的实例
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE) // @Scope("prototype")
public class MailSession {
...
}
注入list
@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);
}
}
}
Spring会自动把所有类型为Validator
的Bean装配为一个List
注入进来,这样一来,我们每新增一个Validator
类型,就自动被Spring装配到Validators
中了。可以通过@Order(1)
指定顺序
@Component
@Order(1)
public class EmailValidator implements Validator {
...
}
可选注入
这个参数告诉Spring容器,如果找到一个类型为ZoneId
的Bean,就注入,如果找不到,就忽略。这种方式非常适合有定义就使用定义,没有就使用默认值的情况。
@Component
public class MailService {
@Autowired(required = false)
ZoneId zoneId = ZoneId.systemDefault();
...
}
创建第三方Bean
在@Configuration类中编写一个Java方法创建并返回它,注意给方法标记一个@Bean注解。(单例)
@Configuration
@ComponentScan
public class AppConfig {
// 创建一个Bean:
@Bean
ZoneId createZoneId() {
return ZoneId.of("Z");
}
}
初始化和销毁
初始化流程
调用构造方法创建实例;
根据@Autowired进行注入;
调用标记有@PostConstruct的init()方法进行初始化。
销毁时,容器会首先调用标记有@PreDestroy的shutdown()方法
*Spring只根据Annotation查找无参数方法,方法名可以不是init和shutdown。
别名(多个相同类型的Bean)
默认情况下,对一种类型的Bean,容器只创建一个实例。需要对一种类型的Bean创建多个实例,使用@Bean("z")
或者@Bean+@Qualifier("name")
指定别名。
注入时,要指定Bean的名称@Qualifier("z")
或者创建时@Primary // 指定为主要Bean,使用时不指定名称就会使用主要的Bean
怎么读取配置文件?
Resource
想在代码中应用配置文件中的配置?
AppService需要读取logo.txt这个文件,需要定位文件,打开InputStream。使用Properties 读出配置。
org.springframework.core.io.Resource
使用@Value注入
@Value("classpath:/logo.txt")
private Resource resource;
@Value("file:/path/to/logo.txt")
private Resource resource;
@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"));
}
try (var input = new InputStreamReader(properties.getInputStream(), StandardCharsets.UTF_8)) {
Properties props = new Properties();
props.load(input);
this.name = props.getProperty("app.name", "Cannot fetch the property [app.name]");
this.version = props.getProperty("app.version", "Cannot fetch the property [app.version]");
}
}
使用Maven的标准目录结构,所有资源文件放入src/main/resources即可。
读取properties
roperties properties = new Properties();
// System.getProperty获取jvm环境变量值
String cfgPath = System.getProperty("user.dir") + "/etc/123.cfg";
InputStream inputStream = new BufferedInputStream(new FileInputStream(new File(cfgPath)));
properties.load(inputStream);
String version = properties.getProperty("app.version");
注入配置,注入一个文件
在@Configuration配置类上再添加一个注解@PropertySource(“app.properties”) // 表示读取classpath的app.properties
@PropertySource读取的配置是针对IoC容器全局的,其他任何bean都可以直接引用已读取到的配置
@Configuration
@ComponentScan
@PropertySource("app.properties") // 表示读取classpath的app.properties
public class AppConfig {
@Value("${app.zone:Z}")
String zoneId;
@Bean
ZoneId createZoneId() {
return ZoneId.of(zoneId);
}
}
"${app.zone:Z}"表示读取key为app.zone的value,但如果key不存在,就使用默认值Z。也可以用于方法参数。
可以先通过一个简单的JavaBean持有所有的配置,在使用的地方用bean注入。
条件装配
创建某个Bean时,Spring容器可以根据注解@Profile来决定是否创建
@Configuration
@ComponentScan
public class AppConfig {
@Bean
@Profile("!test")
ZoneId createZoneId() {
return ZoneId.systemDefault();
}
@Bean
@Profile("test")
ZoneId createZoneIdForTest() {
return ZoneId.of("America/New_York");
}
}
使用-Dspring.profiles.active=test,master
。
还可以用@Conditional(OnSmtpEnvCondition.class)
。
AOP
为什么要有AOP
业务方法中有安全检查、日志、事务等功能,横跨”多个业务方法。不得不在每个业务方法上重复编写代码。
把权限检查视作一种切面(Aspect),把日志、事务也视为切面,然后,以某种自动化的方式,把切面织入到核心逻辑中,实现Proxy模式。
依次实现:
核心逻辑,即BookService;
切面逻辑,即:
- 权限检查的Aspect
- 日志的Aspect
- 事务的Aspect。
然后,以某种方式,让框架来把上述3个Aspect以Proxy的方式“织入”到BookService中。
如果客户端获得了BookService的引用,当调用bookService.createBook()时,如何对调用方法进行拦截,并在拦截前后进行安全检查、日志、事务等处理,就相当于完成了所有业务功能。
装配AOP
- 定义执行方法,并在方法上通过AspectJ的注解告诉Spring应该在何处调用此方法;
@Before("execution(public * com.itranswarp.learnjava.service.UserService.*(..))")
拦截器类型:
@Before:这种拦截器先执行拦截代码,再执行目标代码。如果拦截器抛异常,那么目标代码就不执行了;
@After:这种拦截器先执行目标代码,再执行拦截器代码。无论目标代码是否抛异常,拦截器代码都会执行;
@AfterReturning:和@After不同的是,只有当目标代码正常返回时,才执行拦截器代码;
@AfterThrowing:和@After不同的是,只有当目标代码抛出了异常时,才执行拦截器代码;
@Around:能完全控制目标代码是否执行,并可以在执行前后、抛异常后执行任意拦截代码,可以说是包含了上面所有功能。
2. 在切面类上标记@Component和@Aspect;
@Aspect
@Component
public class LoggingAspect
- 在@Configuration类上标注@EnableAspectJAutoProxy。
@Configuration
@ComponentScan
@EnableAspectJAutoProxy
public class AppConfig {
...
}
上面的问题:总不能每一个类都写一个注解吧?
写成包:包里每个Bean都加?面积有点大
前缀匹配:误伤更多
最好通过一个注解,让被自动代理的Bean知道自己被“安排”了。(反向,标这个注解的是希望被自动装配的)
// 定义注解
@Target(METHOD)
@Retention(RUNTIME)
public @interface MetricTime {
String value();
}
// 被装配的方法加注解标志
@Component
public class UserService {
// 监控register()方法性能:
@MetricTime("register")
public User register(String email, String password, String name) {
...
}
...
}
// 切面类
@Aspect
@Component
public class MetricAspect {
@Around("@annotation(a)")
public Object metric(ProceedingJoinPoint joinPoint, MetricTime a) throws Throwable {
String name = metricTime.value();
long start = System.currentTimeMillis();
try {
return joinPoint.proceed();
} finally {
long t = System.currentTimeMillis() - start;
// 写入日志或发送至JMX:
System.err.println("[Metrics] " + name + ": " + t + "ms");
}
}
}
真正重要的是MetricAspect metric方法第二个参数的引用类型。
注意
- 访问被注入的Bean时,不要直接访问字段的代码,改为通过方法get访问。
- 编写Bean时,如果可能会被代理,就不要编写public final方法。