一、Spring框架:
IOC控制反转 ——> 降低耦合度
1、私有属性保存依赖对象,且通过构造函数传入。
2、Spring控制new过程:Spring启动时先实例化依赖,再实例化所需的类。
Bean 是 Bean容器(Spring IOC容器) 生成的对象。
Bean 容器:管理类的实例化、依赖的实例化、依赖的传入
DI:依赖传入 ——》传递服务给客户端
spring IoC
在给类内成员上直接写@AutoWired的时候, IDE提示尽量不要给成员直接注入, 查了一番资料后找到了一个靠谱的说法
1. 必要的依赖使用构造方法注入
2. 可选的依赖使用set方法注入
3. 尽量不使用字段注入
1. 降低类之间的耦合度
不使用IoC,类的创建形成多对多依赖关系,耦合度高。IoC相当于一个中间层,IoC与类之间是一对多依赖,将多对多关系变为一对多关系,减少系统整体耦合度
2. 实例复用
不使用IoC,获取一个类的实例时,每次都要实例化,增加空间和时间。增加了IoC,IoC对实例提供了多种生命周期,可以提高实例的复用。
3. 更方便根据不同环境创建不同实例
方便程序在不同环境下运行
另外,IoC跟依赖注入很像,貌似前者是设计原则,后者是设计模式的一种,不知道这种理解对不对
AOP面向切面编程——〉 动态代理
二、注解 Annotation
注解可以理解为标签!!!
本质是 @interface 修饰的类
总共7个内置注解:
3个作用于代码的注解- @Override、@Deprecated、@SuppressWarnings (java.lang)
- @Override - 检查该方法是否是重写方法。如果发现其父类,或者是引用的接口中并没有该方法时,会报编译错误。
- @Deprecated - 标记过时方法。如果使用该方法,会报编译警告。
- @SuppressWarnings - 指示编译器去忽略注解中声明的警告。
4个用于注解其他注解,也称为元注解- @Retention、@Documented、@Target、@Inherited
- @Retention - 标识这个注解怎么保存,是只在代码中,还是编入class文件中,或者是在运行时可以通过反射访问。(用来指定Annotation的RetentionPolicy属性)
- @Documented - 标记这些注解是否包含在用户文档中。
- @Target - 标记这个注解应该是哪种 Java 成员。(用来指定Annotation的ElementType属性)
- @Inherited - 标记这个注解是继承于哪个注解类(默认 注解并没有继承于任何子类)
ElementType.java
package java.lang.annotation;
public enum ElementType {
TYPE, /* 类、接口(包括注释类型)或枚举声明 */
FIELD, /* 字段声明(包括枚举常量) */
METHOD, /* 方法声明 */
PARAMETER, /* 参数声明 */
CONSTRUCTOR, /* 构造方法声明 */
LOCAL_VARIABLE, /* 局部变量声明 */
ANNOTATION_TYPE, /* 注释类型声明 */
PACKAGE /* 包声明 */
}
RetentionPolicy.java
package java.lang.annotation;
public enum RetentionPolicy {
SOURCE, /* Annotation信息仅存在于编译器处理期间,编译器处理完之后就没有该Annotation信息了 */
CLASS, /* 编译器将Annotation存储于类对应的.class文件中。默认行为 */
RUNTIME /* 编译器将Annotation存储于class文件中,并且可由JVM读入 */
}
一个Annotation对应唯一个RetentionPolicy,可对应多个ElementType.
Annotation通用定义:
@Documented
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface MyAnnotation1 {
}
@Component ——〉 识别为 Spring Bean类
@Repository ——〉 标注一个DAO组件类
@Service ——〉 标注业务逻辑组件类
@Controller ——〉标注控制器组件类
@Autowired ——〉随拿随用,组件类对象不需要初始化(自动装配)
@Resource ——〉@Autowired的子集,注解偏基础设施类(中间件)
@SLF4J ——〉日志接口
代码结构:
@Common ——〉通用配置
@Controller
@Service ——〉具体业务逻辑服务层
@Manager ——〉通用业务逻辑层
@wrapper ——〉 外调层、HSF(RPC)等。
三、Spring IOC容器
Bean
什么是javaBean? 简单的说:就是普通Java类,只是满足一定的特征
(1)JavaBean 类必须有一个没有参数的构造函数;此构造函数在使用《jsp:useBean》实例化JavaBean 类时调用,若JavaBean 类内没有任何构造函数,则系统会自动生成一个没有任何参数的构造函数。
(2)JavaBean 内的属性都应该定义为私有的。这样可以保证数据的完整性和封装性。
(3)属性值可以通过setXxx 和getXxxx 来操作。需要注意的是,变量的第一个字母是小写的,但方法名内的第一个字母必须大写,此为必须遵守的约定。
javaBean在MVC设计模型中是model,又称模型层,在一般的程序中,我们称它为数据层,就是用来设置数据的属性和一些行为。
- 每个<bean ...>都有一个id标识,相当于Bean的唯一ID;
- 在userServiceBean中,通过<property name="..." ref="..." />注入了另一个Bean;
- Bean的顺序不重要,Spring根据依赖关系会自动正确初始化。
<?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>
把上述XML配置文件用Java代码写出来,就像这样:
UserService userService = new UserService();
MailService mailService = new MailService();
userService.setMailService(mailService);
Bean自动装配
最后一步,我们需要创建一个Spring的IoC容器实例,然后加载配置文件,让Spring容器为我们创建并装配好配置文件中指定的所有Bean,这只需要一行代码
ApplicationContext context = new ClassPathXmlApplicationContext("application.xml");
接下来,我们就可以从Spring容器中“取出”装配好的Bean然后使用它:
// 获取Bean:
UserService userService = context.getBean(UserService.class);
// 正常调用:
User user = userService.login("bob@example.com", "password");
BeanFactory和ApplicationContext的区别在于,BeanFactory的实现是按需创建,即第一次获取Bean时才创建这个Bean,而ApplicationContext会一次性创建所有的Bean。
实际上,ApplicationContext接口是从BeanFactory接口继承而来的,并且,ApplicationContext提供了一些额外的功能,包括国际化支持、事件和通知机制等。通常情况下,我们总是使用ApplicationContext,很少会考虑使用BeanFactory。
Annotation配置Bean
使用Spring的IoC容器,实际上就是通过类似XML这样的配置文件,把我们自己的Bean的依赖关系描述出来,然后让容器来创建并装配Bean。一旦容器初始化完毕,我们就直接从容器中获取Bean使用它们。
使用XML配置的优点是所有的Bean都能一目了然地列出来,并通过配置注入能直观地看到每个Bean的依赖。它的缺点是写起来非常繁琐,每增加一个组件,就必须把新的Bean配置到XML中。
我们可以使用Annotation配置,可以完全不需要XML,让Spring自动扫描Bean并组装它们。
@Component
public class UserService {
@Autowired
MailService mailService;
...
}
这个@Component注解就相当于定义了一个Bean,它有一个可选的名称,默认是mailService,即小写开头的类名。
使用@Autowired就相当于把指定类型的Bean注入到指定的字段中。和XML配置相比,@Autowired大幅简化了注入,因为它不但可以写在set()方法上,还可以直接写在字段上,甚至可以写在构造方法中:
@Component
public class UserService {
MailService mailService;
public UserService(@Autowired MailService mailService) {
this.mailService = mailService;
}
...
}
编写一个AppConfig类启动容器:
@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());
}
}
AppConfig标注了@Configuration,表示它是一个配置类
使用的实现类是AnnotationConfigApplicationContext,必须传入一个标注了@Configuration的类名。
此外,AppConfig还标注了@ComponentScan,它告诉容器,自动搜索当前类所在的包以及子包,把所有标注为@Component的Bean自动创建出来,并根据@Autowired进行装配。
整个工程结构如下:
使用Annotation配合自动扫描能大幅简化Spring的配置,我们只需要保证:
- 每个Bean被标注为@Component并正确使用@Autowired注入;
- 配置类被标注为@Configuration和@ComponentScan;
- 所有Bean均在指定包以及子包内。
使用@ComponentScan非常方便,但是,我们也要特别注意包的层次结构。通常来说,启动配置AppConfig位于自定义的顶层包(例如com.itranswarp.learnjava),其他Bean按类别放入子包。
@Scope — Bean多个实例
对于Spring容器来说,当我们把一个Bean标记为@Component后,它就会自动为我们创建一个单例(Singleton),即容器初始化时创建Bean,容器关闭前销毁Bean。在容器运行期间,我们调用getBean(Class)获取到的Bean总是同一个实例。
还有一种Bean,我们每次调用getBean(Class),容器都返回一个新的实例,这种Bean称为Prototype(原型),它的生命周期显然和Singleton不同。声明一个Prototype的Bean时,需要添加一个额外的@Scope注解:
@Component
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE) // @Scope("prototype")
public class MailSession {
...
}
Spring支持的几种bean的作用域
当定义一个bean在Spring里,我们还能给这个bean声明一个作用域。它可以通过bean的scope属性来定义。Spring框架支持以下五种bean的作用域:
作用域 | 描述 |
singleton | 单例模式,在spring IoC容器仅存在一个Bean实例,默认值 |
prototype | 原型模式,每次从容器中获取Bean时,都返回一个新的实例,即每次调用getBean()时,相当于执行newXxxBean() |
request | 每次HTTP请求都会创建一个新的Bean,该作用域仅在基于web的Spring ApplicationContext环境下有效 |
session | 同一个HTTP Session共享一个Bean,不同Session使用不同的Bean,该作用域仅在基于web的Spring ApplicationContext环境下有效 |
global-session | 同一个全局的HTTP Session中共享一个Bean,一般用于Portlet应用环境,该作用域仅在基于web的Spring ApplicationContext环境下有 |
注入List
有些时候,我们会有一系列接口相同,不同实现类的Bean。例如,注册用户时,我们要对email、password和name这3个变量进行验证。为了便于扩展,我们先定义验证接口:
public interface Validator {
void validate(String email, String password, String name);
}
通过一个Validators
作为入口:
@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);
}
}
}
注意到Validators
被注入了一个List<Validator>
,Spring会自动把所有类型为Validator
的Bean装配为一个List
注入进来,这样一来,我们每新增一个Validator
类型,就自动被Spring装配到Validators
中了,非常方便。
因为Spring是通过扫描classpath获取到所有的Bean,而List
是有序的,要指定List
中Bean的顺序,可以加上@Order
注解。
可选注入
默认情况下,当我们标记了一个@Autowired
后,Spring如果没有找到对应类型的Bean,它会抛出NoSuchBeanDefinitionException
异常。
可以给@Autowired
增加一个required = false
的参数:
@Component
public class MailService {
@Autowired(required = false)
ZoneId zoneId = ZoneId.systemDefault();
...
}
这个参数告诉Spring容器,如果找到一个类型为ZoneId
的Bean,就注入,如果找不到,就忽略。
创建第三方Bean
如果一个Bean不在我们自己的package管理之内,例如ZoneId
,如何创建它?
答案是我们自己在@Configuration
类中编写一个Java方法创建并返回它,注意给方法标记一个@Bean
注解:
@Configuration
@ComponentScan
public class AppConfig {
// 创建一个Bean:
@Bean
ZoneId createZoneId() {
return ZoneId.of("Z");
}
}
Spring对标记为@Bean
的方法只调用一次,因此返回的Bean仍然是单例。
初始化和销毁
在Bean的初始化和清理方法上标记@PostConstruct
和@PreDestroy
:
@Component
public class MailService {
@Autowired(required = false)
ZoneId zoneId = ZoneId.systemDefault();
@PostConstruct
public void init() {
System.out.println("Init mail service with zoneId = " + this.zoneId);
}
@PreDestroy
public void shutdown() {
System.out.println("Shutdown mail service");
}
}
使用别名
默认情况下,对一种类型的Bean,容器只创建一个实例。但有些时候,我们需要对一种类型的Bean创建多个实例。例如,同时连接多个数据库,就必须创建多个DataSource
实例。
可以用@Bean("name")
指定别名,也可以用@Bean
+@Qualifier("name")
指定别名。
Resource使用 -@Value注入资源路径
在Java程序中,我们经常会读取配置文件、资源文件等。使用Spring容器时,我们也可以把“文件”注入进来,方便程序读取。
@Value("classpath:/logo.txt") //资源路径
private Resource resource;
但使用classpath是最简单的方式。上述工程结构如下:
注入配置 — 读取配置文件 @PropertySource
Spring容器提供了一个简单的@PropertySource
来自动读取配置文件。我们只需要在@Configuration
配置类上再添加一个注解:
@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}"
表示读取key为app.zone
的value,如果key不存在,启动将报错;"${app.zone:Z}"
表示读取key为app.zone
的value,但如果key不存在,就使用默认值Z
。
这样一来,我们就可以根据app.zone
的配置来创建ZoneId
。
还可以把注入的注解写到方法参数中:
@Bean
ZoneId createZoneId(@Value("${app.zone:Z}") String zoneId) {
return ZoneId.of(zoneId);
}
以#{bean.property}
形式注入时,Spring容器自动把指定Bean的指定属性值注入。
四、Spring AOP
1、装配AOP
Sprint AOP相当于Python中的装饰器,增强函数功能,同时抽象出复用的函数。
Aspect:切面,即一个横跨多个核心逻辑的功能,或者称之为系统关注点;
Joinpoint:连接点,即定义在应用程序流程的何处插入切面的执行;
Pointcut:切入点,即一组连接点的集合;
Advice:增强,指特定连接点上执行的动作;
Introduction:引介,指为一个已有的Java对象动态地增加新的接口;
Weaving:织入,指将切面整合到程序的执行流程中;
Interceptor:拦截器,是一种实现增强的方式;
Target Object:目标对象,即真正执行业务的核心逻辑对象;
AOP Proxy:AOP代理,是客户端持有的增强后的对象引用。
1、通过Maven引入Spring对AOP的支持:
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
<version>${spring.version}</version>
</dependency>
依赖会自动引入AspectJ框架,使用AspectJ实现AOP比较方便
2、定义切面和执行方法
@Aspect
@Component
public class LoggingAspect {
// 在执行UserService的【每个方法】前执行:
@Before("execution(public * com.itranswarp.learnjava.service.UserService.*(..))")
public void doAccessCheck() {
System.err.println("[Before] do access check...");
}
// 在执行MailService的【每个方法】前后执行:
@Around("execution(public * com.itranswarp.learnjava.service.MailService.*(..))")
public Object doLogging(ProceedingJoinPoint pjp) throws Throwable {
System.err.println("[Around] start " + pjp.getSignature());
Object retVal = pjp.proceed();
System.err.println("[Around] done " + pjp.getSignature());
return retVal;
}
}
拦截器:@Around
注解,它和@Before
不同,@Around
可以决定是否执行目标方法。
3、然后,需要给@Configuration
类加上一个@EnableAspectJAutoProxy
注解:
@Configuration
@ComponentScan
@EnableAspectJAutoProxy
public class AppConfig {
...
}
Spring的IoC容器看到这个注解,就会自动查找带有@Aspect
的Bean,然后根据每个方法的@Before
、@Around
等注解把AOP注入到特定的Bean中。
其实AOP的原理非常简单。我们以LoggingAspect.doAccessCheck()
为例,要把它注入到UserService
的每个public
方法中,最简单的方法是编写一个子类,并持有原始实例的引用。
public UserServiceAopProxy extends UserService {
private UserService target;
private LoggingAspect aspect; //切面
public UserServiceAopProxy(UserService target, LoggingAspect aspect) {
this.target = target;
this.aspect = aspect;
}
public User login(String email, String password) {
// 先执行Aspect的代码:
aspect.doAccessCheck();
// 再执行UserService的逻辑:
return target.login(email, password);
}
public User register(String email, String password, String name) {
aspect.doAccessCheck();
return target.register(email, password, name);
}
...
}
这些都是Spring容器启动时为我们自动创建的注入了Aspect的子类,它取代了原始的UserService
(原始的UserService
实例作为内部变量隐藏在UserServiceAopProxy
中)
Spring对接口类型使用JDK动态代理,对普通类使用CGLIB创建子类。如果一个Bean的class是final,Spring将无法为其创建子类。
可见,虽然Spring容器内部实现AOP的逻辑比较复杂(需要使用AspectJ解析注解,并通过CGLIB实现代理类),但我们使用AOP非常简单,一共需要三步:
- 定义执行方法,并在方法上通过AspectJ的注解告诉Spring应该在何处调用此方法;
- 标记
@Component
和@Aspect
; - 在
@Configuration
类上标注@EnableAspectJAutoProxy
。
拦截器类型
-
@Before:这种拦截器先执行拦截代码,再执行目标代码。如果拦截器抛异常,那么目标代码就不执行了;
-
@After:这种拦截器先执行目标代码,再执行拦截器代码。无论目标代码是否抛异常,拦截器代码都会执行;
-
@AfterReturning:和@After不同的是,只有当目标代码正常返回时,才执行拦截器代码;
-
@AfterThrowing:和@After不同的是,只有当目标代码抛出了异常时,才执行拦截器代码;
-
@Around:能完全控制目标代码是否执行,并可以在执行前后、抛异常后执行任意拦截代码,可以说是包含了上面所有功能。
2、使用注解装配AOP
主要实现精准拦截目标方法!
@Around("execution(public * update*(..))")
public Object doLogging(ProceedingJoinPoint pjp) throws Throwable {
// 对update开头的方法切换数据源:
String old = setCurrentDataSource("master");
Object retVal = pjp.proceed();
restoreCurrentDataSource(old);
return retVal;
}
我们在使用AOP时,要注意到虽然Spring容器可以把指定的方法通过AOP规则装配到指定的Bean的指定方法前后,但是,如果自动装配时,因为不恰当的范围,容易导致意想不到的结果,即很多不需要AOP代理的Bean也被自动代理了,并且,后续新增的Bean,如果不清楚现有的AOP装配规则,容易被强迫装配。
如果我们自己写的Bean希望在一个数据库事务中被调用,就标注上@Transactional
:
@Component
public class UserService {
// 有事务:
@Transactional
public User createUser(String name) {
...
}
// 无事务:
public boolean isValidName(String name) {
...
}
// 有事务:
@Transactional
public void updateUser(User user) {
...
}
}
或者直接在class级别注解,表示“所有public方法都被安排了”:
@Component
@Transactional
public class UserService {
...
}
例子:
@Target(METHOD)
@Retention(RUNTIME)
public @interface MetricTime {
String value(); //注解的method名
}
在需要被监控的关键方法上标注该注解:
@Component
public class UserService {
// 监控register()方法性能:
@MetricTime("register")
public User register(String email, String password, String name) {
...
}
...
}
定义MetricAspect
:
@Aspect
@Component
public class MetricAspect {
@Around("@annotation(metricTime)")
public Object metric(ProceedingJoinPoint joinPoint, MetricTime metricTime) throws Throwable {
String name = metricTime.value(); //获取被监控的方法名
long start = System.currentTimeMillis();
try {
return joinPoint.proceed(); //连接点的执行,即register方法执行
} finally {
long t = System.currentTimeMillis() - start;
// 写入日志或发送至JMX:
System.err.println("[Metrics] " + name + ": " + t + "ms");
}
}
}
注意metric()
方法标注了@Around("@annotation(metricTime)")
,它的意思是,符合条件的目标方法是带有@MetricTime
注解的方法,因为metric()
方法参数类型是MetricTime
(注意参数名是metricTime
不是MetricTime
),我们通过它获取性能监控的名称。
有了@MetricTime
注解,再配合MetricAspect
,任何Bean,只要方法标注了@MetricTime
注解,就可以自动实现性能监控。
Welcome, Bob!
[Metrics] register: 16ms
五、Spring Bean的生命周期
中间件:
MetaQ (RocketMQ) : pull
Notify: push
缓存:
TDDL - 改造Mysql
分布式缓存:
Tair:(K-V 缓存) MDB - 基于内存的KV缓存
LDB - 基于SSD的KV持久化缓存
RDB - 基于内存的KV持久化缓存