春之绽放:Spring的魅力

发表时间: 2023-10-24 19:51

Spring 初始化流程,容器初始化,主要流程在
AbstractApplicationContext.refresh()

1.容器预先准备 - prepareRefresh()

2.创建容器对象同时加载xml配置文件信息到当前工厂 beafDefintion - obtainFreshBeanFactory()

3.配置 Bean 工厂 - prepareBeanFactory()

4.修改容器 beanFactory - postProcessBeanFactory()--空实现

5.调用工厂后处理器 -
invokeBeanFactoryPostProcessors
()

6.注册 Bean 后处理器 -
registerBeanPostProcessors
()

7.初始化消息源 - initMessageSource()

8.初始化上下文事件广播器 -
initApplicationEventMulticaster()

9.模板方法 onRefresh - onRefresh() --空实现

10.注册监听器 - registerListeners()

11.创建单例对象 -
finishBeanFactoryInitialization()
初始化剩余的单例bean

12.发布上下文刷新事件 - finishRefresh()

1.1 概念

Spring 是一款轻量级开源框架,主要是为简化企业级应用开发而生。提高了开发者的开发效率及系统可维护性,核心思想开箱即用,提供的核心功能主要是 IOC 和 AOP

Spring、SpringMVC、SpringBoot

Spring MVC 是 Spring 中的一个很重要的模块,主要赋予 Spring 快速构建 MVC 架构的 Web 程序的能力。MVC 是模型(Model)、视图(View)、控制器(Controller)的简写,其核心思想是通过将业务逻辑、数据、显示分离来组织代码。

Spring 旨在简化 J2EE 企业应用程序开发。Spring Boot 旨在简化 Spring 开发(减少配置文件,开箱即用!)。

1.1.1 IOC

IOC(Inversion of Control--控制反转):是一种设计思想,而不是一个具体的技术实现。将原来在程序中手动创建对象的控制权交由Spring框架管理。 IOC 在其他语言中也有应用,并非 Spring 特有。IOC 容器是 Spring 用来实现 IOC 的载体, IOC 容器实际上就是个 Map(key,value),Map 中存放的是各种对象。IOC 最常见以及最合理的实现方式叫做依赖注入Dependency Injection简称 DI)。

控制 :指的是对象创建(实例化、管理)的权力。

反转 :控制权交给外部环境(Spring 框架、IOC 容器)。

DI:Spring 使用 Java Bean 对象的 Set 方法或者带参数的构造方法为我们在创建所需对象时将其属性自动设置所需要的值的过程就是依赖注入的基本思想。就是由IOC容器在运行期间,动态地将某种依赖关系注入到对象之中。

1.1.1.1IOC接口

Spring提供IOC容器实现的两种方式:(两个接口)BeanFactory和ApplicationContext。BeanFactory 提供 IOC 思想所设想所有的功能,同时也融入 AOP 等相关功能模块,可以说 BeanFactory 是 Spring 提供的一个基本的 IOC 容器。ApplicationContext 构建于 BeanFactory 之上,同时提供了诸如容器内的时间发布、统一的资源加载策略、国际化的支持等功能,是 Spring 提供的更为高级的 IOC 容器。例如 ApplicationContext 继承自 ResourceLoader 和 MessageSource,那么当我们实现 ResourceLoaderAware 和 MessageSourceAware 相关接口时,就将其自身注入到业务对象中即可。


BeanFactory


ApplicationContext---BeanFactory的子接口

底层

Spring 框架的基础设施,面向 Spring 本身。原始的 Factory。不提供给开发者使用

面向使用 Spring 框架的开发者,它是由 BeanFactory 接口派生而来。具有 BeanFactory 所有的功能,所有应用场合都可以直接使用它而非底层的 BeanFactory。

扩展功能

1. MessageSource, 提供国际化的消息访问

2. 资源访问,如 URL 和文件

3. 事件传播

4.同时加载多个配置文件

5. 载入多个(有继承关系)上下文 ,使得每一个上下文都专注于一个特定的层次,比如应用的 web 层。

加载方式

默认懒加载(lazy-init=true);缺点:不能及时发现 Spring 错误配置的问题。BeanFacotry 加载后,直至第一次使用调用 getBean 方法才会抛出异常。

默认非懒加载(lazy-init=false);优点:使用时无需等待,因为都创建好了。缺点:占用内存空间,Bean 多时,程序启动较慢。

创建方式

以编程的方式被创建

还能以声明的方式创建,如使用 ContextLoader。


支持 BeanPostProcessor、BeanFactoryPostProcessor 的使用,但需手动注册。

支持 BeanPostProcessor、BeanFactoryPostProcessor 的使用,自动注册。

1.1.1.2IOC操作Bean管理

Spring 中提供配置元数据的方式:1.基于 XML 的配置;2.基于注解的配置;3.基于 Java 的配置。

(Bean注入spring容器的方式:1.xml方式;2.注解方式:a.@Configuration+@Bean,b.@Import;3.FactoryBean;4.BDRegistryPostProcessor)

一、基于xml配置文件实现

1、基于xml创建对象:基于xml注入对象默认执行无参构造创建。

2、基于xml注入属性:1.setter 方法注入;2.有参构造器;3.p名称空间注入(底层基于setter)。

xml缺点:1.配置麻烦,既要维护代码又要维护配置文件,开发效率低,配置文件过多,维护困难;2.程序编译期间无法对配置项的正确性进行验证,只能在运行期发现并且出错之后不易排查。3.解析xml时,无论是一次性装进内存,还是一行一行解析,都会占用内存资源,影响性能。

注入:① 通过index设置参数的位置;②通过type设置参数类型;3.静态工厂注入;4.实例工厂

二、基于注解方式实现

1.1.2 AOP

AOP(Aspect-Oriented Programming--面向切面编程)是OOP的一种延续,OOP的编程思想可以解决大部分重复代码问题,有一些还是解决不了。AOP能够将那些与业务无关,却为业务模块所共同调用的逻辑或责任(例如事务处理、日志管理、权限控制等)封装成一个切面,然后注入到目标对象 (具体业务逻辑)中去,减少系统的重复代码,降低模块间的耦合度,并有利于未来的可拓展性和可维护性。AOP可以对某个对象或某些对象的功能进行增强。

:指的是横切逻辑,原有业务逻辑代码不动,只能操作横切逻辑代码,所以面向横切逻辑。

:横切逻辑代码往往要影响的是很多个方法,每个方法如同一个点,多个点构成一个面。

AOP 作用:在不改变原有业务逻辑的情况下,增强横切逻辑代码,根本上解耦合,避免横切逻辑代码重复。有效的将横切逻辑代码和业务逻辑代码分离。

1.1.2.1代理

AOP 实现的关键就在于 AOP 框架自动创建的 AOP 代理,AOP 代理则可分为静态代理和动态代理两大类,其中静态代理是指使用 AOP 框架提供的命令进行编译,从而在编译阶段就可生成 AOP 代理类,因此也称为编译时增强;而动态代理则在运行时借助于 JDK 动态代理、CGLIB 等在内存中"临时"生成 AOP 动态代理类,因此也被称为运行时增强

Spring AOP 就是基于动态代理的,默认的策略是如果要代理的对象,实现了某个接口,那么 Spring AOP 会使用 JDK Proxy,去创建代理对象,而对于没有实现接口的对象,就无法使用 JDK Proxy 去进行代理了,这时候 Spring AOP 会使用 Cglib 生成一个被代理对象的子类来作为代理。如图:

AspectJ 是一套独立的面向切面编程的解决方案,比Spring AOP功能强大。通常被称为编译时增强的 AOP 框架。当切面太多时最好选择AspectJ,比Spring AOP快很多。AspectJ 和 Spring AOP 在实现上几乎无关。

Spring AOP 也是对目标类增强,生成代理类。但是与 AspectJ 的最大区别在于:Spring AOP 的运行时增强,而 AspectJ 是编译时增强。Spring AOP 基于代理(Proxying),而 AspectJ 基于字节码操作(Bytecode Manipulation)。

Spring AOP 虽然使用了 AspectJ 的 Annotation,使用了 Aspect 来定义切面,使用 Pointcut 来定义切入点,使用 Advice 来定义增强处理。但是并没有使用它的编译器和织入器。其实现原理是 JDK 动态代理,在运行时生成代理类。为了启用 Spring 对@AspectJ 方面配置的支持,并保证 Spring 容器中的目标 Bean 被一个或多个方面自动增强,必须在 Spring 配置文件中添加配置:<aop:aspectj-autoproxy/>。要想 Spring AOP 通过 CGLIB 生成代理,只需要在 Spring 的配置文件引入:<aop:aspectj-autoproxy proxy-target-class="true"/>

Spring中的AOP,代理对象有接口就用JDK动态代理,没有接口就用Cglib动态代理。Spring Boot中的AOP,2.0之前和Spring一样;之后首选Cglib,若用户想要使用JDK需手动配置。
spring.aop.proxy-target-class=false

1.1.2.2自定义注解

1、创建注解

使用@Target、@Retention、@Documented自定义一个注解。

@Target(ElementType.METHOD)@Retention(RetentionPolicy.RUNTIME)@Documentedpublic @interface PermissionAnnotation{}

@Target: 表示该注解的作用范围,是ElementType类型的,该类型是一个枚举类型,最常用的几个:FIELD(用于字段、枚举的常量)、TYPE(用于接口、类、枚举、注解)、PARAMETER(用于方法形参:值传递不是简单的把实参传递给形参,而是,实参建立了一个副本,然后把副本传递给了形参,所以形参改变并不会影响到实参)、METHOD(用于方法)。

@Retention: 表示该注解的生命周期,是RetentionPolicy类型的,该类型是一个枚举类型,可提供三个值选择,分别是:CLASS、RUNTIME、SOURCE。

  • RetentionPolicy.CLASS: 注解被保留到class文件但jvm加载class文件时候被遗弃,是默认的生命周期;
  • RetentionPolicy.RUNTIME: 注解不仅被保存到class文件中,jvm加载class文件之后,仍然存在;
  • RetentionPolicy.SOURCE: 注解只保留在源文件,当Java文件编译成class文件的时候,注解被遗弃; 由此可见生命周期关系:SOURCE < CLASS < RUNTIME,我们一般用RUNTIME

2、定义注解行为

创建一个AOP切面类,在类上加个 @Aspect 注解。@Component 注解将该类交给 Spring 来管理。

@Aspect@Component@Order(1)public class PermissionFirstAdvice {  // 定义一个切面,括号内写入第1步中自定义注解的路径  @Pointcut("@annotation(com.mu.demo.annotation.PermissionAnnotation)")  private void permissionCheck() {  }  @Before("pointCut()")  public void doBefore(JoinPoint joinPoint) {    log.info("====doBefore方法进入了====");    // 获取签名    Signature signature = joinPoint.getSignature();    // 获取切入的包名    String declaringTypeName = signature.getDeclaringTypeName();    // 获取即将执行的方法名    String funcName = signature.getName();    log.info("即将执行方法为: {},属于{}包", funcName, declaringTypeName);    // 也可以用来记录一些信息,比如获取请求的 URL 和 IP    ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();    HttpServletRequest request = attributes.getRequest();    // 获取请求 URL    String url = request.getRequestURL().toString();    // 获取请求 IP    String ip = request.getRemoteAddr();    log.info("用户请求的url为:{},ip地址为:{}", url, ip);  }  @After("pointCut()")  public void doAfter(JoinPoint joinPoint) {    log.info("==== doAfter 方法进入了====");    Signature signature = joinPoint.getSignature();    String method = signature.getName();    log.info("方法{}已经执行完", method);  }  @Around("permissionCheck()")  public Object permissionCheckFirst(ProceedingJoinPoint joinPoint) throws Throwable {    System.out.println("===================第一个切面===================:" + System.currentTimeMillis());    //获取请求参数,详见接口类    Object[] objects = joinPoint.getArgs();    Long id = ((JSONObject) objects[0]).getLong("id");    String name = ((JSONObject) objects[0]).getString("name");    System.out.println("id1->>>>>>>>>>>>>>>>>>>>>>" + id);    System.out.println("name1->>>>>>>>>>>>>>>>>>>>>>" + name);    // id小于0则抛出非法id的异常    if (id < 0) {      return JSON.parseObject("{\"message\":\"illegal id\",\"code\":403}");    }    return joinPoint.proceed();  }  //@AfterRetunning(pointcut = "pointCut()", returning = "result"pointcut = "pointCut()", returning = "result")  public void doAfterReturning(JoinPoint joinPoint, Object result) {    Signature signature = joinPoint.getSignature();    String classMethod = signature.getName();    log.info("方法{}执行完毕,返回参数为:{}", classMethod, result);      // 实际项目中可以根据业务做具体的返回值增强    log.info("对返回参数进行业务上的增强:{}", result + "增强版");  }  @AfterThrowing(pointcut = "pointCut()", throwing = "ex")  public void afterThrowing(JoinPoint joinPoint, Throwable ex) {    Signature signature = joinPoint.getSignature();    String method = signature.getName();    // 处理异常的逻辑    log.info("执行方法{}出错,异常为:{}", method, ex);  }}

@Pointcut 注解,用来定义一个切点,即上文中所关注的某件事情的入口,切入点定义了事件触发时机。两个常用的表达式:一个是使用 execution(),另一个是使用 annotation()。

execution表达式:

以 execution(* com.mutest.controller...(..))) 表达式为例:

  • 第一个 * 号的位置:表示返回值类型,* 表示所有类型。
  • 包名:表示需要拦截的包名,后面的两个句点表示当前包和当前包的所有子包,在本例中指 com.mutest.controller包、子包下所有类的方法。
  • 第二个 * 号的位置:表示类名,* 表示所有类。
  • *(..):这个星号表示方法名,* 表示所有的方法,后面括弧里面表示方法的参数,两个句点表示任何参数。

annotation() 表达式:

annotation() 方式是针对某个注解来定义切点,如对具有 @PostMapping 注解的方法做切面,可定义切面:

@Pointcut("@annotation(org.springframework.web.bind.annotation.PostMapping)")public void annotationPointcut() {}

然后使用该切面的话,就会切入注解是 @PostMapping 的所有方法。这种方式很适合处理 @GetMapping、@PostMapping、@DeleteMapping不同注解有各种特定处理逻辑的场景。

还有就是如上面案例所示,针对自定义注解来定义切面。

@Pointcut("@annotation(com.example.demo.PermissionsAnnotation)")private void permissionCheck() {}

@Around注解用于修饰Around增强处理,Around增强处理非常强大,表现在:

  • @Around可以自由选择增强动作与目标方法的执行顺序,也就是说可以在增强动作前后,甚至过程中执行目标方法。这个特性的实现在于,调用ProceedingJoinPoint参数的procedd()方法才会执行目标方法。
  • @Around可以改变执行目标方法的参数值,也可以改变执行目标方法之后的返回值。

Around增强处理有以下特点:

  • 当定义一个Around增强处理方法时,该方法的第一个形参必须是 ProceedingJoinPoint 类型(至少一个形参)。在增强处理方法体内,调用ProceedingJoinPoint的proceed方法才会执行目标方法:这就是@Around增强处理可以完全控制目标方法执行时机、如何执行的关键;如果程序没有调用ProceedingJoinPoint的proceed方法,则目标方法不会执行。
  • 调用ProceedingJoinPoint的proceed方法时,还可以传入一个Object[ ]对象,该数组中的值将被传入目标方法作为实参——这就是Around增强处理方法可以改变目标方法参数值的关键。这就是如果传入的Object[ ]数组长度与目标方法所需要的参数个数不相等,或者Object[ ]数组元素与目标方法所需参数的类型不匹配,程序就会出现异常。

@Around功能虽然强大,但通常需要在线程安全的环境下使用。因此,如果使用普通的Before、AfterReturning就能解决的问题,就没有必要使用Around了。如果需要目标方法执行之前和之后共享某种状态数据,则应该考虑使用Around。尤其是需要使用增强处理阻止目标的执行,或需要改变目标方法的返回值时,则只能使用Around增强处理了。

@Before 注解指定的方法在切面切入目标方法之前执行,可以做一些 Log 处理,也可以做一些信息的统计。JointPoint 对象很有用,可以用它来获取一个签名,利用签名可以获取请求的包名、方法名,包括参数(通过 joinPoint.getArgs() 获取)等。

@After 注解和 @Before 注解相对应,指定的方法在切面切入目标方法之后执行,也可以做一些完成某方法之后的 Log 处理。

@AfterReturning 注解和 @After 有些类似,区别在于 @AfterReturning 注解可以用来捕获切入方法执行完之后的返回值,对返回值进行业务逻辑上的增强处理。

需要注意的是,在 @AfterReturning 注解 中,属性 returning 的值必须要和参数保持一致,否则会检测不到。该方法中的第二个入参就是被切方法的返回值。

@AfterThrowing 当被切方法执行过程中抛出异常时,会进入 @AfterThrowing 注解的方法中执行,在该方法中可以做一些异常的处理逻辑。要注意的是 throwing 属性的值必须要和参数一致,否则会报错。该方法中的第二个入参即为抛出的异常。

3、使用

创建接口类,并在目标方法上标注自定义注解 PermissionsAnnotation:

@RestController@RequestMapping(value = "/permission")public class TestController {  @RequestMapping(value = "/check", method = RequestMethod.POST)  @PermissionsAnnotation()  public JSONObject getGroupList(@RequestBody JSONObject request) {    return JSON.parseObject("{\"message\":\"SUCCESS\",\"code\":200,\"data\":" + request + "}");  }}

1.1.3 装配

Spring 装配包括手动和自动装配,手动装配是基于 xml 装配、构造方法、setter 方法等。自动装配有五种,可以用来指导 Spring 容器用自动装配方式来进行依赖注入。

no:这是 Spring 框架的默认设置,在该设置下自动装配是关闭的,开发者需要自行在 bean 定义中用标签明确的设置依赖关系 。

byName:根据 bean 名称设置依赖关系 。当向一个 bean 中自动装配一个属性时,容器将根 bean 的名称自动在在配置文件中查询一个匹配的 bean。 如果找到的话就装配这个属性,如果没找到就报错 。

byType:根据 bean 类型设置依赖关系 。当向一个 bean 中自动装配一个属性时,容器将根据 bean 的类型自动在在配置文件中查询一个匹配的 bean。 如果找到的话就装配这个属性,如果没找到就报错 。

constructor:构造器的自动装配和 byType 模式类似,但是仅仅适用于与有构造器相同参数的 bean,如果在容器中没有找到与构造器参数类型一致的 bean,那么将会抛出异常 。

autodetect:该模式自动探测使用构造器自动装配或者 byType 自动装配 。 首先会尝试找合适的带参数的构造器,如果找到的话就是用构造器自动装配,如果在 bean 内部没有找到相应的构造器或者是无参构造器,容器就会自动选择 byTpe 的自动装配方式 。

注入方式

可靠性

可维护性

可测试性

灵活性

循环关系检测

性能影响

Filed Injection

不可靠

很灵活

不检测

启动快

Constructor Injection

可靠

不灵活

自动检测

启动慢

Setter Injection

不可靠

很灵活

不检测

启动快

总结:1.依赖注入的使用上,Constructor Injection是首选;2.使用@Autowired注解的时候,要使用Setter Injection方式,这样代码更容易写单元测试。

1.2Spring 框架的好处

序号

好处

说明

1

轻量

Spring 是轻量的,基本的版本大约 2MB。

2

控制反转(IOC)

Spring 通过控制反转实现了松散耦合,对象们给出它们的依赖,而不是创建 或查找依赖的对象们。

3

面向切面编程(AOP)

Spring 支持面向切面的编程,并且把应用业务逻辑和系统服务分开

4

容器

Spring 包含并管理应用中对象的生命周期和配置。

5

MVC 框架

Spring 的 WEB 框架是个精心设计的框架,是 Web 框架的一个很好的替代品。

6

事务管理

Spring 提供一个持续的事务管理接口,可以扩展到上至本地事务下至全局事 务(JTA)。

7

异常处理

Spring 提供方便的 API 把具体技术相关的异常(比如由 JDBC,Hibernate or JDO 抛出的)转化为一致的 unchecked 异常。

1.3Spring 核心模块

核心模块

说明

Spring Core

核心容器:核心容器提供 Spring 框架的基本功能。Spring 以 bean 的方式组织和管理 Java 应用中的各个组件及其关系。Spring 使用 BeanFactory 来产生和管理 Bean,它是工厂模式的实现。BeanFactory 使用控制反转(IoC)模式将应用的配置和依赖性规范与实际的应用程序代码分开。

Spring Context

应用上下文: 是一个配置文件,向 Spring 框架提供上下文信息。Spring 上下文包括企业 服务,如 JNDI、EJB、电子邮件、国际化、校验和调度功能。

Spring AOP

面向切面编程: 是面向对象编程的有效补充和完善,Spring 的 AOP 是基于动态代理实现的,实现的方式有两种分别是 Schema 和 AspectJ 这两种方式。

Spring Dao

DBC 和 Dao 模块: JDBC、DAO 的抽象层提供了有意义的异常层次结构,可用该结构来 管理异常处理,和不同数据库供应商所抛出的错误信息。异常层次结构简化了错误处 理,并且极大的降低了需要编写的代码数量,比如打开和关闭链接。

Spring ORM

对象实体映射: Spring 框架插入了若干个 ORM 框架,从而提供了 ORM 对象的关系工 具,其中包括了 Hibernate、JDO 和 IBatis SQL Map 等,所有这些都遵从 Spring 的通用 事物和 DAO 异常层次结构。

Spring Web

Web 模块: Web 上下文模块建立在应用程序上下文模块之上,为基于 web 的应用程序提供了上下文。所以 Spring 框架支持与 Struts 集成,web 模块还简化了处理多部分请求以及将请求参数绑定到域对象的工作 。

Spring Web MVC

MVC 模块:MVC 框架是一个全功能的构建 Web 应用程序的 MVC 实现。通过策略接口,MVC 框架变成为高度可配置的。MVC 容纳了大量视图技术,其中包括 JSP、POI 等,模型由 JavaBean 构成,存放于 m 当中,而视图是一个接口,负责实现模型,控制器表示逻辑代码,由 c 的事情。Spring 框架的功能可以用在任何 J2EE 服务器当中,大多数功能也适用于不受管理的环境。Spring 的核心要点就是支持不绑定到特定 J2EE 服务的可重用业务和数据的访问的对象,毫无疑问这样的对象可以在不同的 J2EE 环境,独立应用程序和测试环境之间重用。

1.4Spring 使用的设计模式

1、工厂模式:Spring使用工厂模式通过 BeanFactory、ApplicationContext 创建bean 对象。

2、代理模式:Spring AOP 功能的实现。

3、单例模式:Spring 中的 Bean 默认都是单例的。

4、模板模式:Spring 中 jdbcTemplate、hibernateTemplate 等以 Template 结尾的对数据库操作的类,它们就使用到了模板模式。

5、包装模式:动态切换不同的数据源。

6、观察者模式:Spring 事件驱动模型就是观察者模式很经典的一个应用。

7、适配器模式:Spring AOP 的增强或通知(Advice)使用到了适配器模式、spring MVC 中也是用到了适配器模式适配 Controller。

8、原型模式;9、策略模式;

1.5Spring Bean

Spring 将管理的一个个的依赖对象称之为 Bean。Spring框架中的单例Bean不是线程安全的。Spring有两种Bean:1、普通Bean--在配置文件中定义Bean类型就是返回类型;2、工厂Bean(FactoryBean)--在配置文件中定义Bean类型可以和返回类型不一样。(一:创建类,让其作为工厂bean,实现接口FatcoryBean;二、实现接口里面的方法,而实现的方法中定义返回的bean类型)

FactoryBean:是Bean,一个能产生或者修饰对象生成的工厂Bean,实现与工厂模式和修饰器模式类似。BeanFactory:是Factory,IOC容器或者对象工厂,所有的Bean都由它进行管理。本身只是一个接口。常用的实现有XmlBeanFactory类,根据xml文件中的定义加载Bean。

Bean创建源码流程:

1.5.1 生命周期

Servlet 的生命周期:实例化,初始 init,接收请求 service,销毁 destroy;Spring 上下文中的 Bean 生命周期也类似。Bean 实例化之前首先要进行准备,即容器启动阶段

1.5.1.1 容器启动阶段

1、配置元信息

创建对象所需要的必要信息称为配置元信息。

2、BeanDefination

Spring 在内存中表示这些配置元信息的方式就是 BeanDefination,即配置的元信息被加载到内存之后是以 BeanDefination 的形式存在。

3、BeanDefinationReader

BeanDefinationReader 的作用就是加载配置元信息,并将其转化为内存形式的 BeanDefination。

4、BeanDefinationRegistry

Spring 通过 BeanDefinationReader 将配置元信息加载到内存生成相应的 BeanDefination 之后,就将其注册到 BeanDefinationRegistry 中,BeanDefinationRegistry 就是一个存放 BeanDefination 的大篮子,它也是一种键值对的形式,通过特定的 Bean 定义的 id,映射到相应的 BeanDefination。

5、BeanFactoryPostProcessor

BeanFactoryPostProcessor 是容器启动阶段 Spring 提供的一个扩展点,主要负责对注册到 BeanDefinationRegistry 中的一个个的 BeanDefination 进行一定程度上的修改与替换。

例如配置 Jdbc 的 DataSource 连接的时候可以这样配置:

BeanFactoryPostProcessor 就会对注册到 BeanDefinationRegistry 中的 BeanDefination 做最后的修改,替换$占位符为配置文件中的真实的数据。

至此,整个容器启动阶段就完成了,容器启动阶段的最终产物就是注册到 BeanDefinationRegistry 中的一个个 BeanDefination 了,这就是 Spring 为 Bean 实例化所做的预热工作。整个容器启动阶段如下所示:

容器启动阶段与 Bean 实例化阶段存在多少时间差,由我们选择的容器和设置来决定:

一、用 BeanFactory 作为 SpringBean 工厂类,则所有的 bean 都是在第一次使用该 Bean 的时候实例化。因为 BeanFactory 就是采用延迟加载来注入 Bean 的,即只有在使用到某个 Bean(调用 getBeam())时才对该 Bean 进行加载实例化。这样就不能及时发现一些有问题的配置。比如,Bean 的某个属性没有注入,BeanFactory 加载后,直到第一次使用时调用 getBean 方法才会抛出异常。

二、用 ApplicationContext 作为 SpringBean 工厂类时:

1.如果 bean 的 scope 是 singleton(默认是 singleton)的,并且 lazy-init 为 false(默认是 false,不用设置),则 ApplicationContext 启动的时候就实例化该 Bean,并且将实例化的 Bean 放在一个 map 结构的缓存中,下次再使用该 Bean 的时候,直接从这个缓存中取;

2.如果 bean 的 scope 是 singleton(默认是 singleton)的,并且 lazy-init 为 true,则该 Bean 的实例化是在第一次使用该 Bean 的时候进行实例化。

3.如果 bean 的 scope 是 prototype 的,则该 Bean 的实例化是在第一次使用该 Bean 的时候进行实例化。

从以上可以知道 Bean 在默认情况下(scope=singleton):

a、选择懒加载(lazy-init=true)方式时,Bean 只有在第一次使用时才实例化。

b、不选择懒加载(lazy-init=false,默认情况)方式时,容器启动完成后会将所有 Bean 实例化到缓存中。

1.5.1.2 Bean实例化阶段

对 Prototype Bean 来说,当用户 getBean 获得 Prototype Bean 的实例后,IOC 容器就不再对当前实例进行管理,而是把管理权交由用户,此后再 getBean 生成的是新的实例。所以我们描述 Bean 的生命周期,都是指的 Singleton Bean。

public class LouzaiBean implements InitializingBean,   BeanFactoryAware, BeanNameAware, DisposableBean {    private String name;  public LouzaiBean() {    System.out.println("1.调用构造方法:我出生了!");  }  public String getName() {    return name;  }  public void setName(String name) {    this.name = name;    System.out.println("2.设置属性:我的名字叫"+name+"!");  }  @Override  public void setBeanName(String s) {    System.out.println("3.调用BeanNameAware#setBeanName方法:我要上学了,起了个学名!");  }  @Override  public void setBeanFactory(BeanFactory beanFactory) throws BeansException {    System.out.println("4.调用BeanFactoryAware#setBeanFactory方法:选好学校了!");  }  @Override  public void afterPropertiesSet() throws Exception {    System.out.println("6.InitializingBean#afterPropertiesSet方法:入学登记!");  }  public void init() {    System.out.println("7.自定义init方法:努力上学ing!");  }  @Override  public void destroy() throws Exception {    System.out.println("9.DisposableBean#destroy方法:平淡的一生落幕了!");  }  public void destroyMethod() {    System.out.println("10.自定义destroy方法:睡了,别想叫醒我!");  }  public void work(){    System.out.println("Bean使用中:工作,只有对社会没有用的人才放假。。");  }}

自定义一个后处理器 MyBeanPostProcessor

public class MyBeanPostProcessor implements BeanPostProcessor {  @Override  public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {    System.out.println("5.BeanPostProcessor.postProcessBeforeInitialization方法:到学校报名啦!");    return bean;  }  @Override  public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {    System.out.println("8.BeanPostProcessor#postProcessAfterInitialization方法:终于毕业,拿到毕业证啦!");    return bean;  }}

applicationContext.xml 配置文件(部分)

<bean name="myBeanPostProcessor" class="demo.MyBeanPostProcessor" /><bean name="louzaiBean" class="demo.LouzaiBean"      init-method="init" destroy-method="destroyMethod">    <property name="name" value="楼仔" /></bean>

测试入口:

public class MyTest {  public static void main(String[] args) {    ApplicationContext context =new ClassPathXmlApplicationContext("classpath:applicationContext.xml");    LouzaiBean louzaiBean = (LouzaiBean) context.getBean("louzaiBean");    louzaiBean.work();    ((ClassPathXmlApplicationContext) context).destroy();  }}

执行结果:

1.调用构造方法:我出生了!

2.设置属性:我的名字叫楼仔!

3.调用BeanNameAware#setBeanName方法:我要上学了,起了个学名!

4.调用BeanFactoryAware#setBeanFactory方法:选好学校了!


5.BeanPostProcessor.postProcessBeforeInitialization方法:到学校报名啦!

6.InitializingBean#afterPropertiesSet方法:入学登记!

7.自定义init方法:努力上学ing!

8.BeanPostProcessor#
postProcessAfterInitialization方法:终于毕业,拿到毕业证啦!

Bean使用中:工作,只有对社会没有用的人才放假。。

9.DisposableBean#destroy方法:平淡的一生落幕了!

10.自定义destroy方法:睡了,别想叫醒我!

1、实例化原生Bean

对象的创建采用了策略模式,借助 BeanDefinationRegistry 中的 BeanDefination,使用反射的方式创建对象,或者使用 CGlib 字节码生成创建对象。同时我们可以灵活的配置来告诉 Spring 采用什么样的策略创建指定的依赖对象。Spring 中 Bean 的创建是策略设计模式的经典应用。

对于BeanFactory容器,当客户向容器请求一个尚未初始化的bean时,或初始化bean的时候需要注入另一个尚未初始化的依赖时,容器就会调用createBean进行实例化。对于ApplicationContext 容器,当容器启动结束后,通过获取BeanDefinition对象中的信息,实例化所有的bean。

2、BeanWrapper——对象的外衣

Spring 中的 Bean 并不是以一个个的本来模样存在的,由于 Spring IOC 容器中要管理多种类型的对象,因此为了统一对不同类型对象的访问,Spring 给所有创建的 Bean 实例穿上了一层外套,这个外套就是 BeanWrapper。BeanWrapper 实际上是对反射相关 API 的简单封装,使得上层使用反射完成相关的业务逻辑大大的简化。

3、设置对象属性

包裹在 BeanWrapper 中的对象还是一个少不经事的孩子,需要为其设置属性以及依赖对象。

对于基本类型的属性,若配置元信息中有配置,那么将直接使用配置元信息中的设置值赋值即可,即使基本类型的属性没有设置值,那么得益于 JVM 对象实例化过程,属性依然可以被赋予默认的初始化零值。

对于引用类型的属性,Spring 会将所有已经创建好的对象放入一个 Map 结构中,此时 Spring 会检查所依赖的对象是否已经被纳入容器的管理范围之内,也就是 Map 中是否已经有对应对象的实例了。如果有,那么直接注入,如果没有,那么 Spring 会暂时放下该对象的实例化过程,转而先去实例化依赖对象,再回过头来完成该对象的实例化过程。

这里有一个 Spring 中的经典问题,那就是 Spring 是如何解决循环依赖的?(默认单例场景下)

Spring 是通过三级缓存解决循环依赖,并且只能解决 Setter 注入的属性循环依赖。(本质是实例化和初始化分开执行的,所以可以解决,构造器的实例化和初始化绑定到一起的,实例化后必须初始化才能用,因此构造器的循环依赖无法通过3级缓存解决)

总结:实例化后的对象被封装在BeanWrapper对象中,紧接着,Spring根据BeanDefinition中的信息以及通过BeanWrapper提供的设置属性的接口完成依赖注入。

4、检查 Aware 相关接口

我们知道,我们如果想要依赖 Spring 中的相关对象,使用 Spring 的相关 API,那么可以实现相应的 Aware 接口,Spring IOC 容器就会为我们自动注入相关依赖对象实例。

Spring会检测该对象是否实现了xxxAware接口,并将相关的xxxAware实例注入给Bean:

  • 若这个Bean已经实现了BeanNameAware接口,会调用它实现的setBeanName(String beanId)方法,此处传递的就是Spring配置文件中Bean的id值;
  • 若这个Bean已经实现了BeanFactoryAware接口,会调用它实现的setBeanFactory()方法, 传递的是Spring工厂自身。
  • 若这个Bean已经实现了ApplicationContextAware接口,会调用 setApplicationContext(ApplicationContext)方法,传入Spring上下文;

5、BeanPostProcessor 前置处理

BeanFactoryPostProcessor 和 BeanPostProcessor 区别

  • BeanFactoryPostProcessor 存在于容器启动阶段而 BeanPostProcessor 存在于对象实例化阶段。
  • BeanFactoryPostProcessor 关注对象被创建之前那些配置的修修改改,缝缝补补,而 BeanPostProcessor 阶段关注对象已经被创建之后的功能增强,替换等操作。
  • BeanPostProcessor 与 BeanFactoryPostProcessor 都是 Spring 在 Bean 生产过程中强有力的扩展点。Spring 中著名的 AOP(面向切面编程),其实就是依赖 BeanPostProcessor 对 Bean 对象功能增强的。

BeanPostProcessor 前置处理就是在要生产的 Bean 实例放到容器之前,允许我们程序员对 Bean 实例进行一定程度的修改,替换等操作。如果想对Bean进行一些自定义处理,那么可以让Bean实现了BeanPostProcessor接口,那将会调用
postProcessBeforeInitialization(Object obj, String s)方法。由于这个方法是在Bean初始化结束时调用的,所以可以被
应用于内存或缓存技术

6、自定义初始化逻辑

所有的准备工作完成之后,如果我们的 Bean 还有一定的初始化逻辑,那么 Spring 将允许我们通过两种方式配置我们的初始化逻辑:(一般通过配置 init-method 方法比较灵活)

  • InitializingBean
  • 配置 init-method 参数

初始化执行顺序:构造器方法>@PostConstruct>InitializingBean的afterPropertiesSet()>Bean定义的initMethod

@Component@Scope(value="Prototype")public class TestPostConstruct implements InitializingBean{  static {    System.out.println("static");  }  public TestPostConstruct() {    System.out.println("constructer");  }  @PostConstruct  public void postConstruct() {    System.out.println("PostConstruct");  }  @Override  public void afterPropertiesSet() throws Exception{    System.out.println("InitializingBean");  }  // 在xml中定义initMethod  public void init() {    System.out.println("init-method");  }}

7、BeanPostProcess 后置处理

与前置处理类似,这里是在 Bean 自定义逻辑也执行完成之后,Spring 又留给我们的最后一个扩展点。我们可以在这里在做一些我们想要的扩展。如果这个Bean实现了BeanPostProcessor接口,将会调用
postProcessAfterInitialization(Object obj, String s)方法;
AOP在此扩展-根据策略判断jdk和cglib动态代理

至此,Bean就已经被正确的创建,之后就可以使用这个Bean了。

8、自定义销毁逻辑

这一步对应自定义初始化逻辑,同样有两种方式:

  • 实现 DisposableBean 接口,会调用其实现的 destory() 方法。
  • 配置 destory-method 属性,会自动调用其配置的销毁方法。

这里一个比较典型的应用就是配置 dataSource 的时候 destory-method 为数据库连接的 close()方法

9、使用

10、调用回调销毁接口

Bean 在为我们服务完之后,马上就要消亡了(通常是在容器关闭的时候),别忘了我们的自定义销毁逻辑,这时候 Spring 将以回调的方式调用我们自定义的销毁逻辑,然后 Bean 就这样走完了光荣的一生!

Bean 实例化阶段的执行顺序:

总结几个重要方法:

①、doCreateBean():这个是入口;

②、createBeanInstance():用来初始化 Bean,里面会调用对象的构造方法;(反射创建对象,反射时若构造器有多个,先拿带@Autowired注解的构造方法,若有多个带@Autowired注解的构造方法,报错;无带@Autowired注解的构造方法时,优先拿无入参的构造方法,若有多个有参构造报错)

③、populateBean():属性对象的依赖注入,以及成员变量初始化;

④、initializeBean():里面有 4 个方法,

先执行 invokeAwareMethods 的 BeanNameAware、BeanFactoryAware 接口;

再执行 BeanPostProcessor 前置接口;

然后执行 InitializingBean 接口,以及配置的 init();

最后执行 BeanPostProcessor 的后置接口。

destory():先执行 DisposableBean 接口,再执行配置的 destroyMethod()。

1.5.2 作用域

作用域

说明

singleton

范围是默认的,这种范围确保不管接受到多少个请求,每个容器中只有一个 bean 的实例,单例的模式由 bean factory 自身来维护 。

prototype

原形范围与单例范围相反,为每一个 bean 请求提供一个实例 。多实例。

request

在请求 bean 范围内会每一个来自客户端的网络请求创建一个实例,在请求完成以后,bean 会失效并被垃圾回收器回收 。 这种作用域仅存在spring web应用中。

Session

与请求范围类似,确保每个 session 中有一个 bean 的实例,在 session 过期后,bean 会随之失效 。 这种作用域仅存在spring web应用中。

global-session

global-session 和 Portlet 应用相关。当你的应用部署在 Portlet 容器中工作时,它包含很多 portlet。 如果你想要声明让所有的 portlet 共用全局的存储变量的话,那么这全局变量需要存储在 global-session 中 。

application

整个ServletContext生命周期里,只有一个bean,这种作用域仅存在spring web应用中。

websocket

一个websocket生命周期内一个bean实例,这种作用域仅存在spring web应用中。

Spring Bean设计成默认单例的好处:

1、避免频繁的创建实例,减少开销,提示性能;

2、避免频繁的拆改那就对象导致OOM或者频繁GC;

3、可以充分利用缓存,加快获取速度

缺点:多线程环境下可能出现线程安全问题。

1.5.3 Spring内部Bean

当一个bean仅被用作另一个bean的属性时,它能被声明为一个内部bean,为了定义inner bean。内部bean通常是匿名的,它们的 Scope一般是prototype。

<?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/beanshttp://www.springframework.org/schema/beans/spring-beans.xsd">  <bean id="CustomerBean" class="com.dpb.common.Customer">    <property name="person" ref="PersonBean"/>  </bean>  <bean id="PersonBean" class="com.dpb.common.Person">    <property name="name" value="波波烤鸭"/>    <property name="address" value="深圳"/>    <property name="age" value="17"/>  </bean></beans><!-- 改为内部bean的方式 --><?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/beanshttp://www.springframework.org/schema/beans/spring-beans.xsd">  <bean id="CustomerBean" class="com.dpb.common.Customer">    <property name="person">      <bean class="com.dpb.common.Person">        <property name="name" value="波波烤鸭"/>        <property name="address" value="深圳"/>        <property name="age" value="17"/>      </bean>    </property>  </bean></beans><!-- 内部 bean 也支持构造器注入 --><?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/beanshttp://www.springframework.org/schema/beans/spring-beans.xsd">  <bean id="CustomerBean" class="com.dpb.common.Customer">    <constructor-arg>      <bean class="com.dpb.common.Person">        <property name="name" value="波波烤鸭"/>        <property name="address" value="深圳"/>        <property name="age" value="17"/>      </bean>    </constructor-arg>  </bean></beans>

1.5.4 Controller的并发安全

根据Tomcat官网介绍,对于一个浏览器请求,tomcat会指定一个处理线程,或是在线程池中选取空闲的,或者新建一个线程。在Tomcat容器中,每个servlet是单例的。在SpringMVC中,Controller 默认也是单例。 采用单例模式的最大好处,就是可以在高并发场景下极大地节省内存资源,提高服务抗压能力

但是单例模式也容易出现问题,如:在Controller中定义的实例变量,在多个请求并发时会出现竞争访问,Controller中的实例变量不是线程安全的。正因为Controller默认是单例,所以不是线程安全的。如果用SpringMVC 的 Controller时,尽量不在 Controller中使用成员变量,否则会出现线程不安全性的情况,导致数据逻辑混乱。类中声明成员变量,且有读写操作(有状态)线程不安全。

@Controllerpublic class TestController {  private int num = 0;  @RequestMapping("/addNum")  public void addNum() {    System.out.println(++num);  }}// 首先访问 http:// localhost:8080 / addNum,得到的答案是1;// 再次访问 http:// localhost:8080 / addNum,得到的答案是 2

Controller并发安全的解决办法:

1、将全局变量都变成局部变量,通过方法参数来传递。

2、尽量不要在controller中定义成员变量;如果必须要定义一个非静态成员变量,那么可以通过注解 @Scope(“prototype”) ,将Controller设置为多例模式。

@Controller@Scope(value="prototype")public class TestController {  private int num = 0;  @RequestMapping("/addNum")  public void addNum() {    System.out.println(++num);  }}

3、Controller中使用ThreadLocal变量。每个线程都有一个变量的副本。

public class TestController {  private int num = 0;  private final ThreadLocal <Integer> uniqueNum =    new ThreadLocal <Integer> () {      @Override protected Integer initialValue() {        return num;    }  };  @RequestMapping("/addNum")  public void addNum() {    int unum = uniqueNum.get();    uniqueNum.set(++unum);    System.out.println(uniqueNum.get());  }}// 更严格的做法是用AtomicInteger类型定义成员变量,对于成员变量的操作使用AtomicInteger的自增方法完成。

4、使用@Scope("session"),会话级别

@Controller //把这个bean 的范围设置成session,表示这bean是会话级别的, @Scope("session") public class XxxController{   private List list ;   //@PostConstruct当bean加载完之后,就会执行init方法,并且将list实例化;   @PostConstruct   public void init(){     list = new ArrayList();   } }

1.6Spring 注解

@Controller

用于 Spring MVC 项目中的控制器类,标有它的 Bean 会自动导入到 IoC 容器中,默认单例模式。

@Service

此注解是组件注解的特化。它不会对@Component 注解提供任何其他行为。可以在服务层类中使用@Service 而不是@Component,因为它以更好的方式指定了意图。

@Repository

对应持久层即 Dao 层,主要用于数据库相关操作

@RequestMapping

用于在控制器处理程序方法中配置 URI 映射。

@ResponseBody

用于发送 Object 作为响应,通常用于发送 XML 或 JSON 数据作为响应。

@PathVariable

用于将动态值从 URI 映射到处理程序方法参数。

@required

适用于 bean 属性 setter 方法

@Autowired

通过类型来实现自动注入 bean。和@Qualifier 注解配合使用可以实现根据 name 注入 bean。适用于 bean 属性 setter 方法,non-setter 方法、构造函数和属性。

@Qualifier

和@Autowired 一块使用,在同一类型的 bean 有多个的情况下可以实现根据 name 注入的需求。避免多个混乱保证唯一的 bea 注入。

@Scope

用于配置 spring bean 的范围。

@Resource

默认是根据 name 注入 bean 的,可以通过设置类型来实现通过类型来注入。

@Required

应用于 bean 属性 setter 方法。此注解仅指示必须在配置时使用 bean 定义中的显式属性值或使用自动装配填充受影响的 bean 属性。如果尚未填充受影响的 bean 属性,则容器将抛出
BeanInitializationException。

@Value

@Value("${property}") 读取比较简单的配置信息

@Transactional

作用于类:当把@Transactional 注解放在类上时,表示所有该类的 public 方法都配置相同的事务属性信息。

作用于方法:当类配置了@Transactional,方法也配置了@Transactional,方法的事务会覆盖类的事务配置信息

@Component

通用的注解,可标注任意类为 Spring 组件。如果一个 Bean 不知道属于哪个层, 可以使用 @Component 注解标注。

@Configuration,@ComponentScan 和 @Bean - 用于声明配置类。

@Aspect,@Before,@After,@Around,@Pointcut - 用于切面编程(AOP)

使用 @Configuration 的约束:

  • 配置类必须以类的方式提供(比如不能是由工厂方法返回的实例)。
  • 配置类必须是非 final 的。
  • 配置类必须是非本地的(即可能不在方法中声明),native 标注的方法。
  • 任何嵌套的配置类必须声明为 static。
  • @Bean 方法可能不会反过来创建更多的配置类。

Spring中获取配置的三种方式:

  • 通过 @Value 方式动态获取单个配置
  • 通过 @ConfigurationProperties + 前缀方式批量获取配置:

@Component + @ConfigurationProperties(prefix = "user");

@Component + @
EnableConfigurationProperties(UserProperties.class) //写在主启动类上

@Component + @PropertySource(value = "classpath:user.properties")

  • 通过 Environment 动态获取单个配置

方式

优点

缺点

使用场景

@Value

使用简单,且使用关联的链路较短。

a.配置名不能被有效枚举到。

b.每一个配置的使用都需重新定义,使用较为麻烦。

c.项目强依赖配置的定义,配置不存在则会导致项目无法启动。

a.项目强依赖该配置的加载,想要从源头避免因配置缺失导致的未知问题。

b.只想使用少数几个配置。

@ConfigurationProperties + 前缀方式

a.使用配置只需确定 key 的前缀即能使用,有利于批量获取场景的使用。

b.因采用前缀匹配,所以在使用新的相同前缀 key 的配置时无需改动代码。

a.使用复杂,需定义配置类或者手动创建 bean 后引入使用。

b.增加新的前缀相同 key 时可能会引入不稳定因素。

a.需要同时使用多前缀相同 key 的配置。

b.期望增加新配置但不修改代码的 properties 注入。

Environment

a.获取配置的 key 可不提前定义,程序灵活性高。

b.配置 key 可使用枚举统一放置与管理。

a.使用较复杂,需继承 Environment 接口形成工具类进行获取。

b.获取 key 对应的枚举与 key 定义分离,value 获取链路较长。

a.只需使用少量的配置。

b.获取配置的 key 无提前定义,需要根据对配置的有无进行灵活使用。

1.6.1@Value

@Value推荐用:设置默认值。如果没有设置默认值,一旦配置文件中没有此项启动就会报错,设置了默认值,在配置文件中没有此项时会使用默认值。如:@Value("${xxx.xxx.username:}")设置默认值为空。

1.6.1.1关于属性名

@Value注解中定义的系统属性名必须和配置文件中的一样

@ConfigurationProperties配置类中,定义的参数名可以跟配置文件中的系统属性名不同。比如:

@Configuration@ConfigurationProperties(prefix = "susan.test")@Datapublic class MyConfig {  private String userName;}// 配置文件中配置的系统属性名是susan.test.user-name=\u5f20\u4e09// 类中的userName和配置文件中用的user-name不一样。// 配置文件中的系统属性名用驼峰标识或小写字母加中划线的组合,spring都能找到配置类中的属性名userName进行赋值。

1.6.1.2关于乱码

Properties文件中中文会乱码,使用.yml或.yaml格式的配置文件,并不会出现中文乱码问题。

susan.test.user-name=\u5f20\u4e09

susan.test.user-name=张三-------假如这样写,最后获取数据时,会发现是乱码。

原因:在springboot的CharacterReader类中,默认的编码格式是ISO-8859-1,该类负责.properties文件中系统属性的读取。如果系统属性包含中文字符,就会出现乱码。

.yml或.yaml格式的配置文件,最终会使用UnicodeReader类进行解析,它的init方法中,首先读取BOM文件头信息,如果头信息中有UTF8、UTF16BE、UTF16LE就采用对应的编码,若无则采用默认UTF8编码。

需要注意的是:乱码问题一般出现在本地环境,因为本地直接读取的.properties配置文件。在dev、test、生产等环境,如果从zookeeper、apollo、nacos等配置中心中获取系统参数值,走的是另外的逻辑,并不会出现乱码问题。

目前主要有三种解决方案:

  • 手动将ISO-8859-1格式的属性值,转换成UTF-8格式。
  • 设置encoding参数,不过这个只对@PropertySource注解有用。
  • 将中文字符用unicode编码转义。

@Value不支持encoding参数,所以方案2不行。

// 方案1,针对个别少量可行。@Servicepublic class UserService {  @Value(value = "${susan.test.userName}")  private String userName;  public String test() {    String userName1 = new String(userName.getBytes(StandardCharsets.ISO_8859_1), StandardCharsets.UTF_8);    System.out.println();    return userName1;  }}// 方案3,类似于如下方式:// 推荐使用这个工具转换:http://www.jsons.cn/unicode/susan.test.userName=\u5f20\u4e09

1.6.1.3关于默认值

@Value默认值使用:推荐使用:,且使用@Value尽量都设置一个默认值。若不需要默认值,宁可设置一个空。比如:@Value(value = "${susan.test.userName:}")

// 1.定义参数时直接给个默认值。这样设置userName默认值的时机,// 比@Value注解依赖注入属性值要早,也就是说userName初始化好了默认值,// 后面还是会被覆盖。且配置文件中没有此项会报错。@Value(value = "${susan.test.userName}")private String userName = "susan";// 2.使用:。在需要设置默认值的系统属性名后,加:符号。紧接着,在:右边设置默认值。@Value(value = "${susan.test.userName:susan}")private String userName;

1.6.1.4关于static变量

被static修饰的变量通过@Value会注入失败。 如果需要给静态变量注入系统属性值,代码如下:

@Servicepublic class UserService {  private static String userName;  // 在setter方法上使用@Value注入,不是在静态变量上。  @Value("${susan.test.userName}")  public void setUserName(String userName) {    UserService.userName = userName;  }  public String test() {    return userName;  }}

1.6.1.5关于变量类型

@Value注解除了支持字符串类型的注入,还支持其他多种类型的系统属性值的注入。比如:8种基本类型,8种包装类型,数组、集合

// 基本类型和包装类型@Value("${susan.test.a:1}")private byte a;@Value("${susan.test.b:100}")private short b;@Value("${susan.test.c:3000}")private int c;@Value("${susan.test.d:4000000}")private long d;@Value("${susan.test.e:5.2}")private float e;@Value("${susan.test.f:6.1}")private double f;@Value("${susan.test.g:false}")private boolean g;@Value("${susan.test.h:h}")private char h;@Value("${susan.test.a:1}")private byte a1;@Value("${susan.test.b:100}")private Short b1;@Value("${susan.test.c:3000}")private Integer c1;@Value("${susan.test.d:4000000}")private Long d1;@Value("${susan.test.e:5.2}")private Float e1;@Value("${susan.test.f:6.1}")private Double f1;@Value("${susan.test.g:false}")private Boolean g1;@Value("${susan.test.h:h}")private Character h1;// 数组,spring默认使用逗号分隔参数值。定义数组时一定要注意属性值的类型,必须完全一致才可以。@Value("${susan.test.array:1,2,3,4,5}")private int[] array;// 数组定义成:short、int、long、char、string类型,spring是可以正常注入属性值的。// 但如果把数组定义成:float、double、包装类,启动项目时就会直接报错。/*错误写法@Value("${susan.test.array:1,2,3,4,5}")private Integer[] array;@Value("${susan.test.array:1.0,abc,3,4,5}")private int[] array;*/// 集合类--list和set写法一样// List// 有值且指定分隔符为逗号。默认分割是逗号。下面三种写法都可以// @Value("#{'${susan.test.list:}'.split(',')}")// @Value("#{'${susan.test.list:}'}")@Value("${susan.test.list:}")private List<String> list;// susan.test.list=10,11,12,13// 判断是否为空,为空值输出null,否则输出以逗号分割的值@Value("#{'${susan.test.list1:}'.empty?null:'${susan.test.list1:}'.split(',')}")public List<String> list1;// 指定默认值,默认值不以逗号分割时,要用第二种指定分割。分隔符和配置项保持一致。// @Value("${susan.test.list2:a,b,c}")@Value("#{'${susan.test.list2:a;b;c}'.split(';')}")public String[] list2;// Set--下面三种写法都可以//    @Value("#{'${susan.test.set:}'.split(',')}")//    @Value("#{'${susan.test.set:}'}")@Value("${susan.test.set:}")public Set<String> set;// susan.test.set=10,11,12,1// Set设置默认空,List写法一样@Value("#{'${susan.test.set1:}'.empty ? null : '${susan.test.set1:}'.split(',')}")public Set<String> set1;// 指定默认值,默认值不以逗号分割时,要用第二种指定分割。分隔符和配置项保持一致。//    @Value("${susan.test.set2:e,f,g}")@Value("#{'${susan.test.set2:e;f;g}'.split(';')}")public Set<String> set2;// Map@Value("#{${susan.test.map}}")private Map<String, String> map;// susan.test.map={"name":"苏三", "age":"18"}// Map设置默认空和Set有点不同@Value("#{'${susan.test.map:}'.empty ? null : '${susan.test.map:}'}")private Map<String, String> map1;

1.6.1.6关于EL玩法

注入Bean

Bean除了用@Autowired 和@Resource注入,也可以用@Value注入。

@Servicepublic class RoleService {  public String getRoleName() {    return "管理员";  }}@Servicepublic class UserService {  // 常用的Bean注入方式  /*@Autowired  private RoleService roleService;*/  // 也可以使用@Value注入  @Value("#{roleService}")  private RoleService roleService;  public String test() {    System.out.println(roleService.getRoleName());    return null;  }}

Bean变量和方法

@Servicepublic class RoleService {  public static final int DEFAULT_AGE = 18;  public int id = 1000;  public String getRoleName() {    return "管理员";  }  public static int getParentId() {    return 2000;  }}// 使用方式@Servicepublic class UserService {  @Value("#{roleService.DEFAULT_AGE}")  private int myAge;  @Value("#{roleService.id}")  private int id;  @Value("#{roleService.getRoleName()}")  private String myRoleName;  @Value("#{roleService.getParentId()}")  private String myParentId;  public String test() {    System.out.println(myAge);    System.out.println(id);    System.out.println(myRoleName);    System.out.println(myParentId);    return null;  }}

静态类

// 注入系统的路径分隔符到path中@Value("#{T(java.io.File).separator}")private String path;// 注入一个随机数到randomValue中@Value("#{T(java.lang.Math).random()}")private double randomValue;

逻辑运算

// 拼接字符串@Value("#{roleService.roleName + '' + roleService.DEFAULT_AGE}")private String value;// 逻辑判断@Value("#{roleService.DEFAULT_AGE > 16 and roleService.roleName.equals('苏三')}")private String operation;// 三目运算@Value("#{roleService.DEFAULT_AGE > 16 ? roleService.roleName: '苏三' }")private String realRoleName;

1.6.1.7关于${}和#{}

${}主要用于获取配置文件中的系统属性值。#{}主要用于通过spring的EL表达式,获取bean的属性,或者调用bean的某个方法。还有调用类的静态常量和静态方法。

1.6.2@Resource和@Autowired区别

Spring 内置的 @Autowired 以及 JDK 内置的 @Resource 和 @Inject 都可以用于注入 Bean。 前两者都可用于属性或setter注入。当一个接口存在多个实现类的情况下,@Autowired 和@Resource都需要通过名称才能正确匹配到对应的 Bean。Autowired 可以通过 @Qualifier 注解来显示指定名称,@Resource可以通过 name 属性来显示指定名称。

Annotaion

Package

Source

属性

作用范围

@Autowired

org.springframework.bean.factory

Spring 2.5+

required

属性、setter、构造

@Resource

javax.annotation

Java JSR-250

name、type等7种

属性、setter

@Inject

javax.inject

Java JSR-330



支持参数:

依赖注入:@Autowired支持以下三种;@Resource不支持构造注入;

// 属性注入@RestControllerpublic class UserController {  @Autowired  private UserService userService;  @RequestMapping("/add")  public UserInfo add(String username, String password) {    return userService.add(username, password);  }}// Setter 注入@RestControllerpublic class UserController {  private UserService userService;  @Autowired  public void setUserService(UserService userService) {    this.userService = userService;  }  @RequestMapping("/add")  public UserInfo add(String username, String password) {    return userService.add(username, password);  }}// 构造方法注入@RestControllerpublic class UserController {  private UserService userService;  @Autowired  public UserController(UserService userService) {    this.userService = userService;  }  @RequestMapping("/add")  public UserInfo add(String username, String password) {    return userService.add(username, password);  }}

查找顺序

@Autowired 是先根据类型(byType)查找,如果存在多个 Bean 再根据名称(byName)进行查找,它的具体查找流程如下:

Spring 源码
org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor#postProcessPropertyValues实现分析得出,源码执行流程如下图所示:

总结:@Autowired默认按byType装配,默认情况下要求依赖对象必须存在,如果要允许null值,可以设置它的required属性为false。当一个接口存在多个实现类的话,byType这种方式就无法正确注入了,此时注入方式会变为byName。通常和@Qualifier或@Primary注解配合使用效果和@Resource一样。

@Resource 是先根据名称查找,如果(根据名称)查找不到,再根据类型进行查找,它的具体流程如下图所示:

Spring 源码的


org.springframework.context.annotation.CommonAnnotationBeanPostProcessor#postProcessPropertyValues 中分析得出。虽然 @Resource 是 JSR-250 定义的,但是由 Spring 提供了具体实现,它的源码实现如下:

总结:@Resource如果同时指定了name和type,则从上下文找唯一匹配的bean进行装配,找不到则抛出异常。如果指定了name,则从上下文找名称匹配的bean进行装配,找不到则抛出异常。如果指定了type,则从上下文找类型匹配的唯一bean进行装配,找不到或找到多个都会抛出异常。如果即没指定了name,又没指定type,则默认按byName装配;如果没匹配,按照byType装配。

1.6.3@Bean和@Component区别

  • 作用对象不同:@Component 注解作用于类,而 @Bean 注解作用于方法。
  • @Component 通常是通过路径扫描来自动侦测以及自动装配到 Spring 容器中(我们可以使用 @ComponentScan 注解定义要扫描的路径从中找出标识了需要装配的类自动装配到 Spring 的 bean 容器中)。@Bean 注解通常是我们在标有该注解的方法中定义产生这个 bean,@Bean 告诉了 Spring 这是某个类的实例,当我们需要用它的时候还给我。
  • @Bean 注解比 @Component 注解的自定义性更强,而且很多地方我们只能通过 @Bean 注解来注册 bean。比如当我们引用第三方库中的类需要装配到 Spring 容器时,只能通过 @Bean 来实现。

@Bean 与 @Component 用在同一个类上,会怎么样?

  • 新建springboot项目,增加配置项及两个类:
// user.name="测试"// UserConfig类:@Configuration加@Bean会创建一个userName不为null的UserManager对象@Configurationpublic class UserConfig{  @Value("${user.name}")  private String userName;  @Bean  public UserManager userManager(){    UserManager userManager = new UserManager(userName);    return userManager;  }}// UserManager类:@Component会创建一个userName为null的UserManager对象@Componentpublic class UserManager {  private String userName;  public String getUserName(){return userName;}  public UserManager(){    System.out.println("User 无参构造 this =" +this);  }  public UserManager(String userName){    this.userName= userName;    System.out.println("User 有参构造 this =" +this);  }}// 启动类@SpringBootApplicationpublic class Application {  public static void main(String[] args) {    SpringApplication.run(Application.class, args);  }}

总结:Spring 5.0.7.RELEASE ( Spring Boot 2.0.3.RELEASE ) 支持@Configuration + @Bean 与@Component 同时作用于同一个类,启动时会给 info 级别的日志提示,同时会将@Configuration + @Bean 修饰的 BeanDefinition 覆盖掉@Component 修饰的 BeanDefinition

也许 Spring 团队意识到了上述处理不太合适,于是在 Spring 5.1.2.RELEASE ( Spring Boot 2.1.0.RELEASE )做出了优化处理,增加了配置项:
allowBeanDefinitionOverriding
,将主动权交给了开发者,由开发者自己决定是否允许覆盖。在 Spring Boot 启动过程中,会用此值覆盖掉 Spring 中的
allowBeanDefinitionOverriding 的默认值,
默认false不允许覆盖,开发者设置为true时,允许覆盖。

Spring 自始至终默认都是允许 BeanDefinition 覆盖的,变的是 Spring Boot , Spring Boot 2.1.0 之前没有覆盖 Spring 的
allowBeanDefinitionOverriding 默认值,仍是允许 BeanDefinition 覆盖的。

Spring容器中加入bean的几种方式:

  • @Configuration + @Bean
  • @Component + @ComponentScan
  • @Import 配合接口进行导入
  • 使用FactoryBean。
  • 实现BeanDefinitionRegistryPostProcessor进行后置处理。

@Configuration用来声明一个配置类,然后使用 @Bean 注解,用于声明一个bean,将其加入到Spring容器中。具体代码如下:

@Configurationpublic class MyConfiguration {  @Bean  public Person person() {    Person person = new Person();    person.setName("spring");    return person;  }}

@Componet中文译为组件,放在类名上面,然后@ComponentScan放置在我们的配置类上,然后可以指定一个路径,进行扫描带有@Componet注解的bean,然后加至容器中。具体代码如下:

@Componentpublic class Person {  private String name;   public String getName() {      return name;  }  public void setName(String name) {      this.name = name;  }  @Override  public String toString() {    return "Person{" + "name='" + name + '\'' +'}';  }} @ComponentScan(basePackages = "com.springboot.initbean.*")public class Demo1 {  public static void main(String[] args) {    AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext(Demo1.class);    Person bean = applicationContext.getBean(Person.class);    System.out.println(bean);// 结果输出:Person{name='null'}  }}

@Import注解有四种使用方式,其只能放置在类上。具体代码:

// 方式一:@Import直接导入类public class Person {  private String name;  public String getName() {    return name;  }   public void setName(String name) {    this.name = name;  }   @Override  public String toString() {    return "Person{" + "name='" + name + '\'' +'}';  }}/*** 直接使用@Import导入person类,然后尝试从applicationContext中取,成功拿到**/@Import(Person.class)public class Demo1 {  public static void main(String[] args) {    AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext(Demo1.class);    Person bean = applicationContext.getBean(Person.class);    System.out.println(bean);  }}// 方式二:@Import + ImportSelector@Import(MyImportSelector.class)public class Demo1 {  public static void main(String[] args) {    AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext(Demo1.class);    Person bean = applicationContext.getBean(Person.class);    System.out.println(bean);  }} class MyImportSelector implements ImportSelector {  @Override  public String[] selectImports(AnnotationMetadata importingClassMetadata) {    return new String[]{"com.springboot.pojo.Person"};  }}// 方式三:@Import + ImportBeanDefinitionRegistrar@Import(MyImportBeanDefinitionRegistrar.class)public class Demo1 {  public static void main(String[] args) {    AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext(Demo1.class);    Person bean = applicationContext.getBean(Person.class);    System.out.println(bean);  }} class MyImportBeanDefinitionRegistrar implements ImportBeanDefinitionRegistrar {  @Override  public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {    // 构建一个beanDefinition, 关于beanDefinition我后续会介绍,可以简单理解为bean的定义.    AbstractBeanDefinition beanDefinition = BeanDefinitionBuilder.rootBeanDefinition(Person.class).getBeanDefinition();    // 将beanDefinition注册到Ioc容器中.    registry.registerBeanDefinition("person", beanDefinition);  }}// 方式四:@Import + DeferredImportSelector// DeferredImportSelector它是ImportSelector的子接口,所以实现的方法和第二种无异。只是Spring的处理方式不同,它和Spring Boot中的自动导入配置文件 延迟导入有关,非常重要。@Import(MyDeferredImportSelector.class)public class Demo1 {  public static void main(String[] args) {    AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext(Demo1.class);    Person bean = applicationContext.getBean(Person.class);    System.out.println(bean);  }}class MyDeferredImportSelector implements DeferredImportSelector {  @Override  public String[] selectImports(AnnotationMetadata importingClassMetadata) {    // 也是直接将Person的全限定名放进去    return new String[]{Person.class.getName()};  }}

FactoryBean接口,具体代码:

// @Configuration + @Bean的方式将PersonFactoryBean加入到容器中,// 注意,我没有向容器中注入Person, 而是直接注入的PersonFactoryBean然后从容器中拿Person这个类型的bean,成功运行。@Configurationpublic class Demo1 {  @Bean  public PersonFactoryBean personFactoryBean() {    return new PersonFactoryBean();  }  public static void main(String[] args) {    AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext(Demo1.class);    Person bean = applicationContext.getBean(Person.class);    System.out.println(bean);  }} class PersonFactoryBean implements FactoryBean<Person> {  /**   *  直接new出来Person进行返回.   */  @Override  public Person getObject() throws Exception {    return new Person();  }  /**   *  指定返回bean的类型.   */  @Override  public Class<?> getObjectType() {    return Person.class;  }}


BeanDefinitionRegistryPostProcessor后置处理,具体代码:

// 手动向beanDefinitionRegistry中注册了person的BeanDefinition。// 最终成功将person加入到applicationContext中public class Demo1 {  public static void main(String[] args) {    AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext();    MyBeanDefinitionRegistryPostProcessor beanDefinitionRegistryPostProcessor = new MyBeanDefinitionRegistryPostProcessor();    applicationContext.addBeanFactoryPostProcessor(beanDefinitionRegistryPostProcessor);    applicationContext.refresh();    Person bean = applicationContext.getBean(Person.class);    System.out.println(bean);  }} class MyBeanDefinitionRegistryPostProcessor implements BeanDefinitionRegistryPostProcessor {  @Override  public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException {    AbstractBeanDefinition beanDefinition = BeanDefinitionBuilder.rootBeanDefinition(Person.class).getBeanDefinition();    registry.registerBeanDefinition("person", beanDefinition);  }  @Override  public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {  }}

1.7Spring 事务

开启Spring事务本质上就是增加了一个Advisor,但我们使用@
EnableTransactionManagement注解来开启Spring事务是,该注解代理的功能就是向Spring容器中添加了两个Bean:AutoProxyRegistrar和
ProxyTransactionManagementConfiguration。

AutoProxyRegistrar主要的作用是向Spring容器中注册了一个
InfrastructureAdvisorAutoProxyCreator的Bean。 而
InfrastructureAdvisorAutoProxyCreator继承了
AbstractAdvisorAutoProxyCreator,所以这个类的主要作用就是
开启自动代理的作用,也就是一个BeanPostProcessor,会在初始化后步骤中去寻找Advisor类型的Bean,并判断当前某个Bean是否有匹配的Advisor,是否需要利用动态代理产生一个代理对象。


ProxyTransactionManagementConfiguration
是一个配置类,它又定义了另外三个bean:


BeanFactoryTransactionAttributeSourceAdvisor:一个Advisor


AnnotationTransactionAttributeSource:相当于
BeanFactoryTransactionAttributeSourceAdvisor中的Pointcut

TransactionInterceptor:相当于
BeanFactoryTransactionAttributeSourceAdvisor中的Advice


AnnotationTransactionAttributeSource就是用来判断某个类上是否存在@Transactional注解,或者判断某个方法上是否存在@Transactional注解的。​

TransactionInterceptor就是代理逻辑,当某个类中存在@Transactional注解时,到时就产生一个代理对象作为Bean,代理对象在执行某个方法时,最终就会进入到TransactionInterceptor的invoke()方法

Spring 事务的实现方式和实现原理:Spring 事务的本质其实就是数据库对事务的支持,无数据库事务的支持,Spring 是无法提供事务功能的。真正数据库层的事务提交和回滚是通过 binlog 或者 redolog 实现的。

Spring配置事务的方法有两种:1、基于XML的事务配置。 2、基于注解方式的事务配置。

@Transactional:

@Transactional 注解中如果不配置 rollbackFor 属性,那么事务只会在遇到 RuntimeException 时才回滚加上 rollbackFor=Exception.class,可以让事务在遇到非运行时异常时也回滚

@Transactional 注解一般用在可以作用在类或者方法上。

  • 作用于类:当把 @Transactional 注解放在类上时,表示所有该类的 public 方法都配置相同的事务属性信息。
  • 作用于方法:当类配置了 @Transactional ,方法也配置了 @Transactional ,方法的事务会覆盖类的事务配置信息。

Spring 事务管理主要包括 3 个接口,Spring 事务主要由以下三个共同完成的:

1、
PlatformTransactionManager:事务管理器,主要用于平台相关事务的管理。主要包括三个方 法:①、commit:事务提交。②、rollback:事务回滚。③、getTransaction:获取事务状态。

2、TransacitonDefinition:事务定义信息,用来定义事务相关属性,给事务管理器
PlatformTransactionManager 使用这个接口有下面四个主要方法:①、getIsolationLevel:获取隔离级别。②、getPropagationBehavior:获取传播行为。③、getTimeout获取超时时间。④、 isReadOnly:是否只读(保存、更新、删除时属性变为false--可读写,查询时为true--只读)事务管理器能够根据这个返回值进行优化,这些事务的配置信息,都可以通过配置文件进行配置。

3、TransationStatus:事务具体运行状态,事务管理过程中,每个时间点事务的状态信息。例如: ①、hasSavepoint():返回这个事务内部是否包含一个保存点。②、isCompleted():返回该事务是否已完成,也就是说,是否已经提交或回滚。③、isNewTransaction():判断当前事务是否是一个新事务。

1、Spring 事务的种类:

spring 支持编程式事务管理声明式事务管理两种方式:

①编程式事务管理使用TransactionTemplate。

②声明式事务管理建立在AOP之上的。其本质是通过AOP功能,对方法前后进行拦截,将事务处理的功 能编织到拦截的方法中,也就是在目标方法开始之前加入一个事务,在执行完目标方法之后根据执行情 况提交或者回滚事务。

声明式事务最大的优点就是不需要在业务逻辑代码中掺杂事务管理的代码,只需在配置文件中做相关的 事务规则声明或通过@Transactional 注解的方式,便可以将事务规则应用到业务逻辑中。

声明式事务管理要优于编程式事务管理,这正是 spring 倡导的非侵入式的开发方式,使业务代码不受污 染,只要加上注解就可以获得完全的事务支持。唯一不足地方是,最细粒度只能作用到方法级别,无法 做到像编程式事务那样可以作用到代码块级别。

2、Spring 的事务传播行为:

spring 事务的传播行为说的是,当多个事务同时存在的时候,spring 如何处理这些事务的行为。

① PROPAGATION_REQUIRED:如果当前没有事务,就创建一个新事务,如果当前存在事务,就加入该 事务,该设置是最常用的设置。

② PROPAGATION_SUPPORTS:支持当前事务,如果当前存在事务,就加入该事务,如果当前不存在事务,就以非事务执行。

③ PROPAGATION_MANDATORY:支持当前事务,如果当前存在事务,就加入该事务,如果当前不存在事务,就抛出异常。

④ PROPAGATION_REQUIRES_NEW:创建新事务,无论当前存不存在事务,都创建新事务。

⑤ PROPAGATION_NOT_SUPPORTED:以非事务方式执行操作,若当前存在事务,就把当前事务挂起。

⑥ PROPAGATION_NEVER:以非事务方式执行,如果当前存在事务,则抛出异常。

⑦ PROPAGATION_NESTED:如果当前存在事务,则在嵌套事务内执行。如果当前没有事务,则按 REQUIRED属性执行。

3、Spring 中的隔离级别:

① ISOLATION_DEFAULT:这是个
PlatfromTransactionManager默认的隔离级别,使用数据库默认的事务隔离级别。


ISOLATION_READ_UNCOMMITTED:读未提交,允许另外一个事务可看到这个事务未提交的数据。

③ ISOLATION_READ_COMMITTED:读已提交,保证一个事务修改的数据提交后才能被另一事务读 取,而且能看到该事务对已有记录的更新。

④ ISOLATION_REPEATABLE_READ:可重复读,保证一个事务修改的数据提交后才能被另一事务读 取,但是不能看到该事务对已有记录的更新。

⑤ ISOLATION_SERIALIZABLE:一个事务在执行的过程中完全看不到其他事务对数据库所做的更新。

4、事务失效的场景:

①访问权限问题:事务方法(即目标方法)的访问权限不是public,而是 private、default 或 protected 的话,spring 则不支持事务。(原因:spring事务的实现
AbstractFallbackTransactionAttributeSource类的
computeTransactionAttribute方法中有个判断,如果目标方法不是public,则TransactionAttribute返回null,即不支持事务。)

②方法用final修饰:spring 事务底层使用了 aop,即通过 jdk 动态代理或者 cglib,帮我们生成了代理类,在代理类中实现的事务功能。但如果某个方法用 final 修饰了,那么在它的代理类中,就无法重写该方法而添加事务功能。(静态方法是属于类的,而不是属于对象的,无法重写静态方法所以也就不可能实现事务。)

③方法内部调用:底层使用了aop,这样直接调用了this对象,所以不生成事务。示例:

@Servicepublic class DemoService {      @Transactional      public void query(Demo demo) {save(demo);}          @Transactional      public void save(Demo demo) { }    }

解决方法:

1、增加一个service,把一个事务的方法移到新增加的service方法里面,然后进行注入再调用

@Servicepublic class DemoTwoService {          @Transactional      public void save(Demo demo) {    }}@Servicepublic class DemoService {      @Autowired      DemoTwoService demoTwoService;      @Transactional      public void query(Demo demo) {demoTwoService.save(demo);}}

2、在自己类中注入自己

@Servicepublic class DemoService {      @Autowired      DemoService demoService;      @Transactional      public void query(Demo demo) {demoService.save(demo);}      @Transactional      public void save(Demo demo) {  }}

由于这种写法基于spring的三级缓存不会导致,循环依赖的问题出现。

3、通过AopContentent

@Servicepublic class DemoService {          @Transactional      public void query(Demo demo) {     DemoService demoService = (DemoService)AopContext.currentProxy();     demoService.save(demo);      }      @Transactional      public void save(Demo demo) {    }}

④未被Spring管理:使用 spring 事务的前提是:对象要通过 @Controller、@Service、@Component、@Repository 等注解被 spring 管理,需要创建 bean 实例。

⑤多线程调用:

@Servicepublic class DemoService {      @Autowired      DemoTwoService demoTwoService;      @Transactional      public void query(Demo demo) {            new Thread(()->{                  demoTwoService.save(demo);           }).start();      }}

⑥表不支持事务:mysql 5之前默认的数据库引擎是MyISAM,不支持事务。

⑦错误的事务传播:目前支持事务的三种传播特性为:REQUIRED,REQUIRES_NEW,NESTED。

⑧自己捕获异常:想要spring事务的正常回滚,必须抛出它能处理的异常(throw new RuntimeException ("xxxxxx");),如果没有抛出异常,spring会认为程序没有问题。

⑨手动抛出别的异常:spring事务,默认情况下不会回滚Exception(非运行时的异常),只会回滚RuntimeException(运行时异常)和Error(错误)。

⑩自定义回滚异常:

spring也支持自定义回滚异常。可以通过设置rollbackFor参数,来完成这个功能。

@Transactional(rollbackFor = BusinessException.class)    public void query(Demo demo) throws Exception{ save(demo);}

我们自定义的业务异常,如果在执行上面这段代码,保存和更新数据时,程序报错了,抛了SqlException、DuplicateKeyException等异常。而BusinessException是我们自定义的异常,报错的异常不属于BusinessException,所以事务也不会回滚。即使rollbackFor有默认值,但阿里巴巴开发者规范中,还是要求开发者重新指定该参数。因为如果使用默认值,一旦程序抛出了Exception,事务不会回滚,这会出现很大的bug。所以,建议一般情况下,将该参数设置成:Exception或Throwable

⑪嵌套事务回滚过头:

@Servicepublic class DemoService {      @Autowired      private DemoTwoService demoTwoService;      @Autowired      private DemoDao demoDao;      @Transactional(rollbackFor = Exception.class)      public void query(Demo demo) throws Exception{            demoDao.save(demo);            demoTwoService.save(demo);      }}

原本只是希望回滚demoServise.save(),不回滚demoDao.save(demo),但是这种情况两个都会被回滚,因为demoTwoService.save(demo)没有捕获异常,往上抛出,导致query进行回滚,所以同时回滚了两个。

解决方法:

@Servicepublic class DemoService {      @Autowired      private DemoTwoService demoTwoService;      @Autowired      private DemoDao demoDao;      @Transactional(rollbackFor = Exception.class)      public void query(Demo demo) throws Exception{            demoDao.save(demo);            try {                  demoTwoService.save(demo);            } finally {  }      }}

可以将内部嵌套事务放在try/catch中,并且不继续往上抛异常。这样就能保证,如果内部嵌套事务中出现异常,只回滚内部事务,而不影响外部事务。

⑫基于@Transactional注解的事务叫声明式事务。spring还提供了另外一种创建事务的方式,即通过手动编写代码实现的事务,我们把这种事务叫做:编程式事务。spring专门提供了TransactionTemplate支持编程时事务,在它的execute方法中,就实现了事务的功能。使用TransactionTemplate的编程式事务可以

  • 避免由于spring aop问题,导致事务失效的问题。
  • 能够更小粒度的控制事务的范围。
@Servicepublic class DemoService {          @Autowired      private TransactionTemplate transactionTemplate;      @Transactional(rollbackFor = Exception.class)      public void query(Demo demo) throws Exception{           transactionTemplate.execute((status -> {                 save(demo);                 return Boolean.TRUE;           }));      }      @Transactional      public void save(Demo demo) {  }}

1.8Service 多个实现类的注入方式

方法一:Controller 中注入 service 的时候使用@Autowired 和@Qualifier("beanId")来指定注入哪一个。

方法二:Controller 中注入 service 的时候使用@Resource(type = 类名.class)来指定注入哪一个。

方法三:

  • 每个 service 的 impl 都可以指定名称(使用@Service("名称"))
  • Controller 中注入 service 的时候使用名称来指定注入哪一个(使用@Resource(name="名称"))。

@Service 注解,其实做了两件事情:

1、声明 TeacherServiceImpl.java 是一个 bean。因为 TeacherServiceImpl .java 是一个 bean,其他的类才可以使用@Autowired 将 TeacherServiceImpl 作为一个成员变量自动注入。

2、TeacherServiceImpl.java 在 bean 中的 id 是"teacherServiceImpl",即类名且首字母小写。

1.9Spring 一、二、三级缓存


org.springframework.beans.factory.support.DefaultSingletonBeanRegistry--Bean创建先放三级缓存

一级缓存里存的是对外暴露的对象(成品对象),实例化和初始化都完成了,我们的应用中使用的对象就是一级缓存中的

二级缓存中存的是半成品对象或者半成品对象的代理对象,用来解决对象创建过程中的循环依赖问题

三级缓存中存的是 ObjectFactory<?> 类型的 lambda 表达式,用于处理存在 AOP 时的循环依赖问题

Spring的三种注入方式:

接口注入的方式:太灵活,易用性比较差,所以并未广泛应用起来。

构造方法注入的方式:将实例化与初始化并在一起完成,能够快速创建一个可直接使用的对象,但它没法处理循环依赖的问题。

setter 方法注入的方式:是在对象实例化完成之后,再通过反射调用对象的 setter 方法完成属性的赋值,能够处理循环依赖的问题。

Spring的循环依赖有两种场景:一、构造器的循环依赖;2、属性的循环依赖(三级缓存解决--3Map)。

构造器的循环依赖,可在构造器中使用@Lazy注解延迟加载。在注入依赖时,先注入代理对象,当首次使用时在创建对象完成注入。

1.10 常见问题

1.10.1Spring线程并发问题

一般情况下,只有无状态的Bean才可在多线程环境下共享,Spring中,绝大部分Bean都可声明为singleton作用域,因为Spring对一些Bean中非线程安全状态采用ThreadLocal进行处理,解决线程安全问题。

ThreadLocal和线程同步机制都是为解决多线程中相同变量的访问冲突问题。同步机制采用了“时间换空间”的方式,仅提供一份变量,不同的线程在访问前需要获取锁,没获得锁的线程则需要排队。而ThreadLocal采用了“空间换时间”的方式。ThreadLocal会为每一个线程提供一个独立的变量副本,从而隔离了多个线程对数据的访问冲突。因为每一个线程都拥有自己的变量副本,也就没必要对该变量进行同步了。ThreadLocal提供了线程安全的共享对象,在编写多线程代码时,可以把不安全的变量封装进ThreadLocal。

1.10.2SpringMVC 原理

Spring 的 MVC 框架是围绕 DispatcherServlet 来设计的。它用来处理所有的 HTTP 请求和响应。

1、用户发送出请求到前端控制器 DispatcherServlet。

2、DispatcherServlet 收到请求调用 HandlerMapping(处理器映射器)。

3、HandlerMapping 找到具体的处理器(可查找 xml 配置或注解配置),生成处理器对象及处理器拦截器 (如果有),再一起返回给 DispatcherServlet。

4、DispatcherServlet 调用 HandlerAdapter(处理器适配器)。

5、HandlerAdapter 经过适配调用具体的处理器(Handler/Controller)。

6、Controller 执行完成返回 ModelAndView 对象。

7、HandlerAdapter 将 Controller 执行结果 ModelAndView 返回给 DispatcherServlet。

8、DispatcherServlet 将 ModelAndView 传给 ViewReslover(视图解析器)。

9、ViewReslover 解析后返回具体 View(视图)。

10、DispatcherServlet 根据 View 进行渲染视图(即将模型数据填充至视图中)。

11、DispatcherServlet 响应用户。

1.11 Spring Event

观察者模式是软件设计模式的一种。在此种模式中,一个目标对象管理所有相依于它的观察者对象,并且在它本身的状态改变时主动发出通知。这通常透过呼叫观察者所提供的方法来实现。此种模式通常被用来实时事件处理系统。

日常业务开发中,观察者模式对我们很大的一个作用,在于实现业务的解耦 。例如:

很多时候,我们会把观察者模式和发布订阅模式放在一起对比。简单来说,发布订阅模式属于广义上的观察者模式,在观察者模式的 Subject 和 Observer 的基础上,引入 Event Channel 这个中介 ,进一步解耦。

1.11.1Spring事件机制

Spring 基于观察者模式,实现了自身的事件机制,由三部分组成:

  • 事件 ApplicationEvent:通过继承它,实现自定义事件。另外,通过它的 source 属性可以获取事件源,timestamp 属性可以获得发生的时间。
  • 事件发布者 ApplicationEventPublisher:通过它,可以进行事件的发布。
  • 事件监听器 ApplicationListener:通过实现它,进行指定类型的事件的监听。

JDK 也内置了事件机制的实现,考虑到通用性,Spring 的事件机制是基于它之上进行拓展。因此,ApplicationEvent 继承自 java.util.EventObject,ApplicationListener 继承自 java.util.EventListener。

1.11.2入门示例

①创建 UserRegisterEvent 事件类,继承 ApplicationEvent 类,用户注册事件。

②创建 UserService 类,用户 Service。实现
ApplicationEventPublisherAware 接口,从而将 ApplicationEventPublisher 注入到其中。在执行完注册逻辑后,调用 ApplicationEventPublisher 的 #publishEvent(ApplicationEvent event) 方法,发布「UserRegisterEvent」事件。

③创建 EmailService 类,邮箱 Service。实现 ApplicationListener 接口,通过 E 泛型设置感兴趣的事件。实现 #onApplicationEvent(E event) 方法,针对监听的 UserRegisterEvent 事件,进行自定义处理。设置 @Async 注解,声明异步执行。毕竟实际场景下,发送邮件可能比较慢,又是非关键逻辑。

④创建 CouponService 类,优惠劵 Service。在其方法上,添加 @EventListener 注解,并设置监听的事件为 UserRegisterEvent。这是另一种使用方式!

⑤创建 DemoController 类,提供 /demo/register 注册接口。代码如下:

@RestController@RequestMapping("/demo")public class DemoController {      @Autowired      private UserService userService;  // http://127.0.0.1:8080/demo/register?username=yudaoyuanma 接口,进行注册。  @GetMapping("/register")      public String register(String username) {            userService.register(username);            return "success";     }}

⑥创建 DemoApplication 类,应用启动类。代码如下:

@SpringBootApplication@EnableAsync // 开启 Spring 异步的功能public class DemoApplication {      public static void main(String[] args) {            SpringApplication.run(DemoApplication.class, args);      }}//# UserService 发布 UserRegisterEvent 事件2020-04-06 13:09:39.145  INFO 18615 --- [nio-8080-exec-1] c.i.s.l.eventdemo.service.UserService    : [register][执行用户(yudaoyuanma) 的注册逻辑]//# CouponService 监听处理该事件2020-04-06 13:09:39.147  INFO 18615 --- [nio-8080-exec-1] c.i.s.l.eventdemo.service.CouponService  : [addCoupon][给用户(yudaoyuanma) 发放优惠劵]//# EmailService 监听处理该事件2020-04-06 13:09:39.154  INFO 18615 --- [         task-1] c.i.s.l.eventdemo.service.EmailService   : [onApplicationEvent][给用户(yudaoyuanma) 发送邮件]

1.11.3Spring内置事件

ApplicationContextEvent 是 Spring Context 相关的事件基类。

  • ContextStartedEvent:Spring Context 启动完成事件。
  • ContextStoppedEvent:Spring Context 停止完成事件。
  • ContextClosedEvent:Spring Context 停止开始事件。
  • ContextRefreshedEvent:Spring Context 初始化或刷新完成事件。

也就是说,在 Spring Context 的整个生命周期中,会发布相应的 ApplicationContextEvent 事件。

SpringApplicationEvent 是 Spring Boot Application(应用)相关的事件基类。

  • ApplicationStartingEvent:Application 启动开始事件。
  • ApplicationEnvironmentPreparedEvent:Spring Environment 准备完成的事件。
  • ApplicationContextInitializedEvent:Spring Context 准备完成,但是 Bean Definition 未加载时的事件。
  • ApplicationPreparedEvent:Spring Context 准备完成,但是未刷新时的事件。
  • ApplicationReadyEvent:Application 启动成功事件。
  • ApplicationFailedEvent:Application 启动失败事件。

也就是说,在 Application 的整个生命周期中,会发布相应的 SpringApplicationEvent 事件。

通过 ApplicationContextEvent 和 SpringApplicationEvent 事件,实现了 Spring Boot + Nginx 的优雅上下线。

RefreshRoutesEvent:Spring Cloud Gateway 通过监听 RefreshRoutesEvent 事件,结合 Nacos 作为配置中心,实现网关路由动态刷新的功能。Spring Cloud Zuul 也是通过监听 RoutesRefreshedEvent 事件,实现网关路由动态刷新的功能。


RefreshRemoteApplicationEvent
:Spring Cloud Config Client 通过监听


RefreshRemoteApplicationEvent 事件,结合 RabbitMQ 作为 Spring Cloud Bus 消息总线,实现本地配置刷新的功能。