十、Spring
(1)什么是spring框架
是一个容器,可以整合其他框架的框架
核心是IOC(控制反转)和AOP(面向切面编程)
它由20多个模块构成,在很多领域都提供优秀的解决方案
优点:
面向接口编程,可扩展性高,可维护性高
- 类中的成员变量设计为接口
- 方法的参数设计为接口
- 方法的返回值设计为接口
- 调用的时候,接口指向实现类
代码解耦
轻量级,核心包也就3M左右
(2)Spring体系结构
Spring中有主要模块
- Spring AOP面向切面编程
- Spring ORM 如Hibernate、mybaits、JDO
- Spring Core 提供bean工厂、IOC
- Spring Dao 提供JDBC支持
- Spring Context 提供关于UI支持,邮件支持等
- Spring Web 提供web的一些工具类
- Spring MVC 提供了web mvc、webviews、jsp、pdf、export
(3)Spring IOC
Spring IOC是一个核心组件,它是Spring框架的一个重要部分。它全称为**“控制反转”**,是一种设计模式,旨在降低代码的耦合度和提高代码的可复用性。简单来说,IOC就是将对象的创建、依赖注入、生命周期等交给Spring容器来管理,而不是由程序员来手动管理对象的创建和依赖关系。通过IOC,Spring容器会自动在需要的时候创建对象并将依赖关系注入到这些对象中,从而实现了代码的松耦合和可重用性。
Spring IOC的核心是一个容器,它通过配置文件或注解来描述对象及其依赖关系,从而在程序运行时自动创建和注入这些对象。Spring IOC容器可以管理多个Bean(对象),这些Bean之间可以存在依赖关系,从而形成一个Bean的依赖关系图。当一个Bean被需要时,Spring IOC容器会根据依赖关系图自动创建这个Bean,并自动注入其依赖的其他Bean。
在Spring框架中,IOC容器有两种实现方式:BeanFactory和ApplicationContext。BeanFactory是Spring的最基础的IOC容器,它提供了基本的IOC功能。ApplicationContext是BeanFactory的子类,它提供了更多的功能,如AOP、事务管理等,是更为常用的IOC容器实现方式。
BeanFactory实现
public class School {
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
public class User {
private String name;
private int age;
private School school;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public School getSchool() {
return school;
}
public void setSchool(School school) {
this.school = school;
}
}
<?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
http://www.springframework.org/schema/beans/spring-beans-3.2.xsd">
<bean class="com.redistest.bean.User" id="user">
<property name="name" value="张三"></property>
<property name="age" value="12"></property>
<property name="school" value="xx"></property>
</bean>
</beans>
public class MyApplication {
public static void main(String[] args) {
// 获取配置文件
ClassPathResource resource = new ClassPathResource("beans.xml");
// 创建BeanFactory容器
BeanFactory factory = new XmlBeanFactory(resource);
// 从容器中获取Bean
User user = (User) factory.getBean("user");
// 使用Bean
user.getAge();
}
}
ApplicationContext实现
public class MyApplication {
public static void main(String[] args) {
// 创建ApplicationContext容器
ApplicationContext context = new ClassPathXmlApplicationContext("beans.xml");
// 从容器中获取Bean
User user = (User) context.getBean("user");
// 使用Bean
user.getAge();
}
}
Spring IOC的加载过程
首先,通过BeanDefinitionReader 读取指定的配置文件生成bean的定义信息,然后到完整的bean定义信息(BeanDefinition对象),注意这里只是存储bean的定义信息,还没有实例化bean对象;就像工厂里面一样,原材料已经准备好了,但是还没有进行生产,原材料就是beanDefinition,生产就是实例化
在 BeanDefinition 和 完整BeanDefinition 中间通过一个后置增强器,可以对bean的定义信息进行统一修改,只需要实现 BeanFactoryPostProcessor 接口即可,这个后置增强器是可以有多个的,你只要在不同的类实现多个 BeanFactoryPostProcessor 接口就会执行多次,就像这样:
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.BeanFactoryPostProcessor;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.stereotype.Component;
/**
* 扩展方法--后置增强器(可修改bean的定义信息)
*/
@Component
public class ExtBeanFactoryPostProcessor implements BeanFactoryPostProcessor {
@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
// BeanDefinition studentService = beanFactory.getBeanDefinition("studentService");
System.out.println("扩展方法--可进行修改beanDefinition的定义信息");
}
}
- 得到完整
BeanDefinition
之后就可以进行创建对象了,这整个过程被称为 bean 的生命周期,也就是从实例化到销毁的过程;
(4)依赖注入
Spring框架的核心是依赖注入(Dependency Injection),也称为控制反转(Inversion of Control)(控制反转是目标,依赖注入是我们实现控制反转的一种手段),这是Spring成功的关键之一。依赖注入是一种设计模式,它通过将对象的依赖关系转移到外部容器中来减少对象之间的耦合性。
Spring的依赖注入可以通过多种方式实现,包括构造函数注入、Setter方法注入、接口注入等。不同的注入方式实现方式略有不同,但底层原理都是相似的。
依赖注入的底层原理可以简单地描述为:
- 在运行时,Spring会扫描应用程序中的所有类和对象,并根据配置文件或注解等方式将这些对象和类之间的依赖关系注入到它们之中。
- 在注入时,Spring会使用反射技术获取对象的构造方法、属性和方法等信息,然后创建对象,并将需要注入的依赖对象注入到被依赖的对象中。
具体来说,Spring使用反射技术来实现以下功能:
- 获取对象的构造方法:Spring使用反射技术获取类的构造方法,然后使用该构造方法创建对象。构造方法的参数列表表示对象的依赖关系,Spring会自动注入依赖对象到构造方法中。
- 获取对象的属性:Spring使用反射技术获取类的属性,然后使用该属性创建对象。属性可以使用注解或XML配置文件等方式指定需要注入的依赖对象。
- 获取对象的方法:Spring使用反射技术获取类的方法,然后使用该方法创建对象。方法可以使用注解或XML配置文件等方式指定需要注入的依赖对象。
在实现过程中,Spring使用了反射、动态代理等技术来实现依赖注入。在注入时,Spring会根据对象之间的依赖关系自动创建对象,并将它们注入到需要依赖的对象中。这样就可以减少对象之间的耦合性,提高了应用程序的可扩展性和可维护性。
总之,Spring的依赖注入是通过容器来管理对象之间的依赖关系,这种方式可以将对象之间的关系从代码中分离出来,提高了代码的可读性和可维护性,也使得应用程序更加灵活和可扩展。
基于注解的IOC
@component:可以创建任意对象,创建的对象的默认名称是类名的驼峰命名,也可以指定名称:@component(“名称”)
@controller:专门用来创建控制器的对象(servlet),这种对象可以接收用户的请求,可以返回处理结果给客户端
@service:专门用来创建业务逻辑层的对象,负责向下访问数据访问层,处理完毕后的结果返回给界面层
@repository:专门用来创建数据访问层的对象,负责数据库中的增删改查所有操作
依赖注入的注解
值类型的注入
- @value:用来给简单类型注入值
引用类型的注入
@Autowired:使用类型注入值,从整个Bean工厂中搜索同源类型的对象进行注入。
@Autowired @Qualifier(“名称”):使用名称注入值,从整个Bean工厂中搜索相同名称的对象进行注入。
同源类型
被注入的类型(student中的school)与注入的类型时完全相同的类型
被注入的类型(student中的school父)与注入的类型(子)是父子类
被注入的类型(student中的school接口)与注入的类型(实现类)是接口和实现类的类型
(5)Spring AOP
AOP(Aspect Oriented Programming)是一种编程范式,通过在程序执行过程中注入特定的代码,实现对程序行为的增强,从而解决程序中的横切关注点问题。
Spring框架提供了AOP的支持,其底层实现基于动态代理和字节码操作。具体来说,Spring AOP主要基于切点和通知这两个核心概念实现,当然还需要了解一下AOP常用术语:
- 切面(Aspect):切面是一个横跨多个核心关注点的模块化功能,例如日志、安全和事务处理等。它是由切点和通知组成的,通常是一个类。
- 切点(Join Point):切点是指在应用程序中横跨多个对象的一个点,例如方法的调用、异常的抛出等。
- 通知(Advice):通知是切面的具体实现,它定义了在切点执行之前、之后或者环绕切点执行的逻辑。
- 连接点(Joinpoint):连接点是在应用程序中能够使用切面的所有点,例如方法调用、字段访问等。
- 织入(Weaving):织入是将切面应用到目标对象并创建新的代理对象的过程。它可以在编译时、类加载时、运行时进行,Spring AOP使用运行时织入。
- 引入(Introduction):引入是将额外的方法或属性添加到现有的类中,例如为一个已有的类动态添加一个接口的实现。
- 切面优先级(Aspect precedence):当多个切面共同作用于一个连接点时,需要根据优先级顺序确定切面的执行顺序。
- 目标对象(Target Object):目标对象是切面所要增强的原始对象。
- 代理对象(Proxy Object):代理对象是AOP框架创建的对象,它在目标对象的基础上添加了切面的功能。
Spring AOP实现的具体步骤(声明式)
- 配置切面:在Spring配置文件中,通过aop:config元素配置切面,定义切点和通知的关系。例如:
<aop:config>
<aop:aspect ref="myAspect">
<aop:pointcut expression="execution(* com.example.service.*.*(..))" id="servicePointcut"/>
<aop:before method="beforeService" pointcut-ref="servicePointcut"/>
<aop:after-returning method="afterService" pointcut-ref="servicePointcut"/>
</aop:aspect>
</aop:config>
- 创建代理对象:Spring会自动扫描被增强的类,生成代理对象。代理对象会重写目标对象的方法,在执行方法时调用通知的逻辑。代理对象可以基于JDK动态代理或者CGLIB动态代理生成。
- 执行增强逻辑:当目标方法被调用时,代理对象会执行通知的逻辑,例如在目标方法之前打印日志、在目标方法之后记录执行时间等。
总的来说,Spring AOP的底层实现原理基于动态代理和字节码操作,通过切点和通知实现对程序行为的增强。
Spring AOP实现的具体步骤(注解式)
基于注解式的AOP是依赖AspectJ框架实现的。
AspectJ是基于Java语言的AOP框架,可以扩展Java语言的语法,使得我们可以更方便地定义切点和通知,同时提供了更强大的AOP功能,例如静态织入、切点表达式的复杂度等。
- 导入依赖
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.3.16</version>
</dependency>
<dependency>
<groupId>commons-logging</groupId>
<artifactId>commons-logging</artifactId>
<version>1.2</version>
</dependency>
<dependency>
<groupId>aopalliance</groupId>
<artifactId>aopalliance</artifactId>
<version>1.0</version>
</dependency>
<dependency>
<groupId>cglib</groupId>
<artifactId>cglib</artifactId>
<version>3.3.0</version>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.9.7</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
<version>5.3.16</version>
</dependency>
- 在Spring配置文件中开启对注解的支持:
<aop:aspectj-autoproxy/>
- 定义一个切面类,并使用注解来标记切点和通知:
@Aspect
@Component
public class MyAspect {
@Pointcut("execution(* com.example.service.*.*(..))")
private void servicePointcut() {}
@Before("servicePointcut()")
public void beforeService() {
// 增强逻辑
}
@AfterReturning("servicePointcut()")
public void afterService() {
// 增强逻辑
}
}
在上面的例子中,使用@Aspect注解标记该类为切面类,使用@Component注解让Spring自动扫描该类。
使用@Pointcut注解定义切点,可以使用AspectJ表达式来匹配需要增强的方法。
使用@Before和@AfterReturning注解定义前置通知和返回通知,指定对应的切点。
- 在需要织入切面的类中,使用@AspectJ注解标注要织入的切面
@Service
@AspectJ
public class UserServiceImpl implements UserService {
@Override
public void addUser(User user) {
// 目标方法
}
@Override
public void deleteUser(int userId) {
// 目标方法
}
}
- 运行代码
在运行代码时,AspectJ会在运行时动态地创建代理对象,并在目标对象的方法执行前、执行后、抛出异常等时刻执行切面逻辑。
使用AspectJ可以更方便地定义切点和通知,并且提供了更强大的AOP功能,例如支持复杂的切点表达式和静态织入等。但是需要注意的是,AspectJ的语法并不是Java标准语法,需要通过编译器或者插件进行编译。
AspectJ框架中常用注解的含义和使用:
- @Aspect注解标记的类表示该类是一个切面类,里面包含了一些切点和通知方法。
- @Pointcut注解用于定义一个切点,可以用来匹配一个或多个方法。切点表达式可以使用AspectJ提供的语法来定义,例如:
// 定义了一个切点,它匹配com.example.service包下所有类的所有方法。 @Pointcut("execution(* com.example.service.*.*(..))") private void servicePointcut() {}
规范的公式:execution(访问权限 方法返回值 方法声明(参数) 异常类型)
- 简化:execution(方法返回值 方法声明(参数))
- @Before注解表示前置通知,用于在目标方法执行之前执行。
// 代码表示在切点servicePointcut()匹配的方法执行之前,执行beforeService()方法。 @Before("servicePointcut()") public void beforeService() { System.out.println("Before service method is called."); }
- @AfterReturning注解表示返回通知,用于在目标方法执行成功并返回结果之后执行。
// 代码表示在切点servicePointcut()匹配的方法执行成功并返回结果之后,执行afterService()方法。 @AfterReturning("servicePointcut()") public void afterService() { System.out.println("After service method is called."); }
- @AfterThrowing注解表示异常通知,用于在目标方法抛出异常时执行。
// 代码表示在切点servicePointcut()匹配的方法抛出异常时,执行afterThrowing()方法。 @AfterThrowing("servicePointcut()") public void afterThrowing() { System.out.println("Exception occurs."); }
- @After注解表示最终通知,无论目标方法是否执行成功或抛出异常,都会执行最终通知。
// 代码表示在切点servicePointcut()匹配的方法执行完成之后,执行after()方法。 @After("servicePointcut()") public void after() { System.out.println("Finally."); }
AspectJ框架的应用场景
Aspect框架提供的AOP可以应用于各种场景,以下是一些常见的实际应用:
- 日志记录
通过AOP实现对系统的日志记录,可以在方法执行前后记录方法名、参数、执行时间等信息,便于系统运维和问题排查。
- 安全控制
通过AOP实现对系统的安全控制,可以在方法执行前检查用户的权限、身份认证等信息,以确保系统的安全性。
- 性能监控
通过AOP实现对系统的性能监控,可以在方法执行前后记录方法的执行时间、耗时等信息,以便于优化系统的性能。
- 事务管理
通过AOP实现对系统的事务管理,可以在方法执行前开启事务,在方法执行后提交或回滚事务,以确保系统的数据完整性和一致性。
- 缓存管理
通过AOP实现对系统的缓存管理,可以在方法执行前查询缓存,如果缓存存在,则直接返回缓存数据;如果缓存不存在,则执行方法并将结果存入缓存,以加速系统的访问速度。
总的来说,AOP在各个领域都有广泛的应用,可以大大简化系统开发和维护的工作量,提高系统的可维护性、可扩展性和可重用性。
- 日志记录使用举例
首先定义一个切面类,使用@Aspect注解标记:
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@Aspect
public class LoggingAspect {
private static final Logger LOGGER = LoggerFactory.getLogger(LoggingAspect.class);
@Before("execution(* com.example.service.*.*(..))")
public void logBefore(JoinPoint joinPoint) {
LOGGER.info("Entering method {} with arguments {}.", joinPoint.getSignature().getName(), joinPoint.getArgs());
}
@After("execution(* com.example.service.*.*(..))")
public void logAfter(JoinPoint joinPoint) {
LOGGER.info("Exiting method {} with arguments {}.", joinPoint.getSignature().getName(), joinPoint.getArgs());
}
}
上述代码中,使用@Before和@After注解分别定义了前置通知和后置通知,它们的切点表达式为execution(* com.example.service..(…)),表示匹配com.example.service包下所有类的所有方法。
在@Before方法中,使用JoinPoint对象获取目标方法的名称和参数,然后使用SLF4J日志框架记录进入方法的日志。
在@After方法中,同样使用JoinPoint对象获取目标方法的名称和参数,然后使用SLF4J日志框架记录退出方法的日志。
接下来,在Spring配置文件中声明该切面类:
<bean id="loggingAspect" class="com.example.aspect.LoggingAspect"/>
<aop:config>
<aop:aspect ref="loggingAspect">
<aop:before method="logBefore" pointcut="execution(* com.example.service.*.*(..))"/>
<aop:after method="logAfter" pointcut="execution(* com.example.service.*.*(..))"/>
</aop:aspect>
</aop:config>
上述代码中,首先声明了LoggingAspect类的Bean,然后使用aop:config元素声明切面,并将loggingAspect Bean作为切面对象。
在切面对象中,使用aop:before和aop:after元素分别声明前置通知和后置通知,并指定切点表达式为execution(* com.example.service..(…))。
最后,就可以在应用程序中使用com.example.service包下的任何方法,并查看日志输出来确认切面已经生效了。
以上就是一个使用Aspect框架实现日志记录的示例代码,通过这种方式,我们可以将日志记录的逻辑与业务逻辑分离开来,提高代码的可维护性和可重用性。
(6)Spring事务
Spring 事务(transaction)是 Spring 框架中的一个重要特性,用于管理数据库操作中的事务。事务是指一组数据库操作,这些操作要么全部成功执行,要么全部失败回滚。Spring 提供了一种声明式事务的方式,通过注解或配置文件来管理事务,使得编程变得更加简单和方便。
在 Spring 中,事务是通过 AOP(面向切面编程)实现的。Spring 使用了两种事务管理方式:编程式事务管理和声明式事务管理。编程式事务管理需要在代码中手动管理事务的开启、提交或回滚,而声明式事务管理则通过注解或配置文件来实现事务管理,将事务处理与业务逻辑分离。
Spring 事务使用步骤:
- 配置数据源:在 Spring 配置文件中配置数据源(如 JDBC、Hibernate 等)的相关信息,例如数据库连接信息、用户名、密码等。
- 配置事务管理器:在 Spring 配置文件中配置事务管理器,例如使用 Spring 提供的 DataSourceTransactionManager。
- 配置事务切面:通过 AOP 配置将事务管理器织入到业务逻辑中,使得方法执行时能够自动开启、提交或回滚事务。
- 配置事务属性:在需要进行事务管理的方法上使用 @Transactional 注解或 XML 配置文件来设置事务属性,例如事务隔离级别、事务传播行为、超时时间等。
以下是一个使用 Spring 事务的简单示例:
@Service
@Transactional
public class UserServiceImpl implements UserService {
@Autowired
private UserDao userDao;
@Override
public void saveUser(User user) {
userDao.save(user);
}
@Override
public void updateUser(User user) {
userDao.update(user);
}
@Override
public void deleteUser(int userId) {
userDao.delete(userId);
}
@Override
@Transactional(readOnly = true)
public User getUser(int userId) {
return userDao.get(userId);
}
}
在上述示例中,@Transactional 注解用于声明 UserServiceImpl 类中的方法需要进行事务管理。其中,saveUser、updateUser 和 deleteUser 方法使用默认的事务属性,而 getUser 方法使用 readOnly 属性表示只读操作,不需要开启事务。
**需要注意的是,@Transactional 注解只能应用于 public 方法上,因为 Spring AOP 基于动态代理技术实现,只有 public 方法才能被代理。**另外,如果一个方法内部调用了另外一个被事务管理的方法,则默认情况下,这两个方法会共享同一个事务。如果需要分开管理事务,可以使用 @Transactional(propagation = Propagation.REQUIRES_NEW) 注解来实现。
Spring 事务的传播行为
Spring 事务的传播机制用于控制在一个事务方法中调用其他事务方法时,这些方法如何共享事务。Spring 提供了多种事务传播行为,可以通过 @Transactional 注解或 XML 配置文件来设置。
下面介绍几种常见的事务传播行为:
- PROPAGATION_REQUIRED(默认值):如果当前方法正在一个事务中运行,则使用该事务;否则,开启一个新的事务。
- PROPAGATION_SUPPORTS:如果当前方法正在一个事务中运行,则使用该事务;否则,不使用事务。
- PROPAGATION_MANDATORY:如果当前方法正在一个事务中运行,则使用该事务;否则,抛出异常。
- PROPAGATION_REQUIRES_NEW:无论当前方法是否正在一个事务中运行,都开启一个新的事务。
- PROPAGATION_NOT_SUPPORTED:如果当前方法正在一个事务中运行,则挂起该事务;否则,不使用事务。
- PROPAGATION_NEVER:如果当前方法正在一个事务中运行,则抛出异常;否则,不使用事务。
- PROPAGATION_NESTED:如果当前方法正在一个事务中运行,则在该事务中创建一个嵌套事务;否则,开启一个新的事务。嵌套事务是一个独立的事务,有自己的提交和回滚操作,但是如果外部事务回滚,嵌套事务也会被回滚。
下面是一个使用 REQUIRES_NEW 传播行为的示例:
@Service
@Transactional
public class UserServiceImpl implements UserService {
@Autowired
private UserDao userDao;
@Autowired
private AccountDao accountDao;
@Override
public void transfer(int userId1, int userId2, double amount) {
try {
accountDao.withdraw(userId1, amount); // 从 userId1 的账户中扣除金额
updateUserBalance(userId1, -amount); // 更新 userId1 的余额
userService.transfer(userId2, userId1, amount); // 调用另一个事务方法
accountDao.deposit(userId1, amount); // 向 userId1 的账户中存入金额
updateUserBalance(userId1, amount); // 更新 userId1 的余额
} catch (Exception e) {
throw new RuntimeException("转账失败:" + e.getMessage());
}
}
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void updateUserBalance(int userId, double amount) {
User user = userDao.get(userId);
double balance = user.getBalance() + amount;
user.setBalance(balance);
userDao.update(user);
}
}
在上述示例中,transfer 方法用于转账操作,调用了另一个事务方法 updateUserBalance。由于 updateUserBalance 方法需要开启新的事务,所以使用 @Transactional(propagation = Propagation.REQUIRES_NEW) 注解来设置传播行为。这样,在 updateUserBalance 方法中对 user 对象的修改不会影响外部事务的提交或回滚。
Spring的两种事务处理方式
- 注解式的事务
- 声明式事务(必须掌握),在配置文件中添加一次,整个项目遵循事务的设定。
Spring事务的五大隔离级别
第一级别:读未提交(read uncommitted)对方事务还未提交,我们事务能读取到对方未提交的数据,读取到未提交的数据表示读取到了脏数据,脏读(Dirty Read)
第二级别:读已提交(read committed),对方事务提交之后的数据我方可以读取到。读已提交存在的问题时:不可重复读(不能存在重复查询数据时,读到的数据不一样的情况)
第三级别:可重复读(repeatable read),这种隔离级别解决了,不可重复读,存在的问题是读取的数据是幻象(幻读)。事务A读取的数据是备份的数据,即使事务B修改了数据且以及commit后,事务A读取的事务还是以前的数据,这种现象就是幻象(幻读)。
第四级别:序列化读/串行化读(serializable)。解决了所有问题,但效率低,需要事务排队。
第五级别:使用数据库默认的隔离级别isolation=Isolation.DEFAULT
- oracle数据库默认的隔离级别是:读已提交。
- mysql数据库默认的隔离级别是:可重复读。
(7)Spring之Bean的生命周期步骤详解
在Spring中,Bean的生命周期可以分为以下步骤:
- 实例化Bean:Spring通过反射机制创建一个Bean实例。
- 设置对象属性:Spring通过依赖注入(Dependency Injection)或自动装配(Autowiring)机制设置Bean的属性。
- 实现BeanNameAware接口:如果Bean实现了BeanNameAware接口,Spring将Bean的ID传递给setBeanName()方法。
- 实现BeanFactoryAware接口:如果Bean实现了BeanFactoryAware接口,Spring将调用setBeanFactory()方法,将BeanFactory容器实例传入。
- 实现ApplicationContextAware接口:如果Bean实现了ApplicationContextAware接口,Spring将调用setApplicationContext()方法,将ApplicationContext容器实例传入。
- 执行BeanPostProcessor的前置处理方法:Spring框架中有很多BeanPostProcessor接口的实现类,它们在Bean的初始化前后执行一些自定义的逻辑处理。
- 调用Bean的初始化方法:如果Bean实现了InitializingBean接口,Spring将调用它的afterPropertiesSet()方法。如果在配置文件中指定了init-method属性,则Spring将自动调用指定的方法。
- 执行BeanPostProcessor的后置处理方法:Spring框架中有很多BeanPostProcessor接口的实现类,它们在Bean的初始化前后执行一些自定义的逻辑处理。
- Bean可以使用了:此时,Bean已经被完全初始化,可以使用了。
- 当容器关闭时,调用销毁方法:如果Bean实现了DisposableBean接口,Spring将在容器关闭时调用它的destroy()方法。如果在配置文件中指定了destroy-method属性,则Spring将自动调用指定的方法。
总之,Bean的生命周期包括实例化、属性赋值、初始化和销毁四个阶段。在这些阶段中,Spring提供了很多扩展点,可以通过实现相应的接口或配置相应的属性来实现自定义的逻辑处理。
以下是一个简单的示例,展示了一个Bean的完整生命周期过程:
import org.springframework.beans.factory.DisposableBean;
import org.springframework.beans.factory.InitializingBean;
public class MyBean implements InitializingBean, DisposableBean {
private String name;
public MyBean() {
System.out.println("MyBean constructor is called");
}
public void setName(String name) {
this.name = name;
System.out.println("Setting name property of MyBean to " + name);
}
@Override
public void afterPropertiesSet() throws Exception {
System.out.println("Initializing MyBean");
}
public void sayHello() {
System.out.println("Hello, " + name);
}
@Override
public void destroy() throws Exception {
System.out.println("Destroying MyBean");
}
}
上述代码定义了一个名为MyBean的Java类,实现了InitializingBean和DisposableBean接口。在构造函数中,打印了一条日志来表明Bean被实例化了。在setName()方法中,打印了一条日志来表明Bean属性被设置了。
在afterPropertiesSet()方法中,打印了一条日志来表明Bean正在初始化。在sayHello()方法中,打印了一条简单的问候语。在destroy()方法中,打印了一条日志来表明Bean正在销毁。
接下来,我们可以在Spring的配置文件中定义一个MyBean实例,并指定它的属性和初始化方法:
<bean id="myBean" class="com.example.MyBean"
p:name="John"
init-method="afterPropertiesSet"/>
在这个配置中,我们将MyBean的ID指定为“myBean”,将其类名指定为“com.example.MyBean”,并通过p:name属性将其name属性设置为“John”。我们还指定了初始化方法为afterPropertiesSet()方法。
当我们启动应用程序时,Spring将自动创建MyBean实例,并执行它的生命周期方法,最终输出以下内容:
MyBean constructor is called // 1.实例化
Setting name property of MyBean to John // 2.属性赋值
Initializing MyBean // 3.初始化
Hello, John // 手动调用方法
Destroying MyBean // 4.销毁
(8)Spring之推断构造方法底层原理详解
在 Java 8 中,引入了一个新特性:类型推断(Type Inference),这个特性可以在某些情况下省略类型声明。
在 Java 10 中,进一步增加了推断构造方法(
var
),可以使用var
关键字来推断构造方法的类型,例如:
// 等价于 ArrayList<String> list = new ArrayList<>();
var list = new ArrayList<String>();
那么,这个推断构造方法底层原理是什么呢?其实,Java 编译器在编译时,会进行一系列的操作来实现类型推断和推断构造方法。
Java 编译器会对程序进行两次扫描,第一次扫描会获取变量的类型信息,第二次扫描会根据变量的类型信息进行类型检查。当遇到
var
关键字时,编译器会从变量的初始化表达式中推断出变量的类型,然后在第二次扫描时进行类型检查。具体来说,编译器会通过初始化表达式的类型推断来确定
var
表示的类型,然后在构造方法调用中,会将这个类型传递给构造方法,并进行类型检查。如果构造方法调用中的实参类型与推断出的类型不匹配,则编译器会抛出编译错误。
**需要注意的是,推断构造方法只能用于局部变量的声明中,不能用于类的字段或方法的参数或返回值。**此外,推断构造方法也不是万能的,有些情况下需要显式指定类型,例如:
var num = 10; // 编译器可以推断出 num 的类型是 int
var d = 3.14; // 编译器可以推断出 d 的类型是 double
var f = 3.14f; // 编译器可以推断出 f 的类型是 float
var c = 'a'; // 编译器可以推断出 c 的类型是 char
var b = true; // 编译器可以推断出 b 的类型是 boolean
var s = "hello"; // 编译器可以推断出 s 的类型是 String
var arr = new int[]{1, 2, 3}; // 推断出 arr 的类型是 int[]
var list = Arrays.asList("a", "b", "c"); // 推断出 list 的类型是 List<String>
// 但是,下面的代码编译会失败,因为编译器无法推断出变量的类型
// var a;
// var foo();
// var bar(String str);
总之,推断构造方法是一项很方便的功能,可以减少代码的冗余,但也需要谨慎使用,避免代码可读性下降。
Spring 框架在其 5.0 版本中增加了对 Java 10 的新特性推断构造方法(var)的支持,可以使用推断构造方法来声明 Spring Bean。例如:
@Service
public class UserService {
private final UserRepository userRepository;
public UserService(var userRepository) {
this.userRepository = userRepository;
}
// ...
}
在上面的例子中,
UserService
类中的构造方法使用了推断构造方法,通过var
关键字声明了一个参数,编译器会根据参数的类型推断出其具体的类型,然后传递给构造方法。Spring 框架会根据参数的类型,自动在 Spring 应用程序上下文中查找对应的 Bean,并将其注入到构造方法中。需要注意的是,在使用推断构造方法来声明 Spring Bean 时,构造方法参数的类型必须是唯一的,否则编译器无法确定其具体类型,会导致编译错误。此外,Spring 仍然建议在需要可读性和可维护性的情况下,显式地声明构造方法参数的类型,而不是使用推断构造方法。
下面举一个在 Spring 相关的项目中使用推断构造方法的实际案例:
假设我们有一个简单的 Spring Boot 应用,其中有一个
UserController
类,用于处理用户相关的 HTTP 请求。UserController
需要依赖一个UserService
,而UserService
又依赖于一个UserMapper
。我们可以使用推断构造方法来声明UserController
和UserService
类。
@RestController
@RequestMapping("/users")
public class UserController {
private final UserService userService;
public UserController(var userService) {
this.userService = userService;
}
@GetMapping("/{id}")
public User getUser(@PathVariable Long id) {
return userService.getUserById(id);
}
}
@Service
public class UserService {
private final UserMapper userMapper;
public UserService(var userRepository) {
this.userRepository = userRepository;
}
public User getUserById(Long id) {
return userMapper.findById(id);
}
}
@Mapper
public interface UserMapper {
User findById(Long id);
}
在上面的例子中,
UserController
和UserService
类都使用了推断构造方法来声明它们的构造方法。Spring 框架会自动根据构造方法参数的类型来查找对应的 Bean,并将其注入到构造方法中。在本例中,Spring 会查找UserRepository
的 Bean,并注入到UserService
的构造方法中,然后再将UserService
的 Bean 注入到UserController
的构造方法中。使用推断构造方法可以简化代码,提高开发效率。但是需要注意,推断构造方法只适用于局部变量的声明中,在其他地方使用可能会降低代码的可读性。另外,推断构造方法也不是必须的,可以使用显式类型声明来替代。
(9)Spring之初始化前、初始化、初始化后详解
Spring框架中的初始化包括三个阶段:初始化前、初始化、初始化后。下面是它们的详细解释:
- 初始化前:
在这个阶段,Spring框架会执行一些预处理操作,例如扫描所有的配置文件和组件扫描器等等。在这个阶段,Spring容器还没有完全准备好,所以你不能在这个阶段使用容器中的bean实例。
- 初始化:
初始化阶段是Spring容器启动后最重要的阶段之一。在这个阶段,Spring会执行所有的BeanPostProcessors,它们可以对bean进行修改、增强或者替换。此外,Spring还会对bean进行实例化、属性注入、初始化以及Bean的生命周期回调方法的调用。这个阶段完成后,所有的bean都已经准备好并可以在应用程序中使用。
- 初始化后:
在初始化后阶段,Spring框架会执行一些后处理操作。这些后处理操作可以包括清理无用的bean、执行一些额外的逻辑、发布应用程序事件等等。在这个阶段,Spring容器已经准备好并可以被使用了。
总体来说,Spring框架的初始化是非常复杂和重要的过程。了解这三个阶段的作用和流程,可以帮助我们更好地理解Spring框架,并帮助我们写出更好的Spring应用程序。
下面是一个简单的代码示例,展示了Spring Bean的初始化前、初始化和初始化后三个阶段的执行顺序:
@Component
public class MyBean implements InitializingBean, DisposableBean {
@PostConstruct
public void postConstruct() {
System.out.println("PostConstruct: MyBean initialized.");
}
@PreDestroy
public void preDestroy() {
System.out.println("PreDestroy: MyBean destroyed.");
}
@Override
public void afterPropertiesSet() throws Exception {
System.out.println("InitializingBean: MyBean initialized.");
}
@Override
public void destroy() throws Exception {
System.out.println("DisposableBean: MyBean destroyed.");
}
public void init() {
System.out.println("Custom init method: MyBean initialized.");
}
public void destroyCustom() {
System.out.println("Custom destroy method: MyBean destroyed.");
}
}
在这个示例中,
MyBean
类实现了InitializingBean
和DisposableBean
接口,同时还定义了自定义的初始化方法和销毁方法。此外,它还使用了@PostConstruct
和@PreDestroy
注解,表示初始化前和初始化后执行的方法。
当我们启动Spring容器时,它会按照以下顺序执行
MyBean
类的初始化和销毁:
初始化前阶段:
- 扫描所有的配置文件和组件扫描器等等。
初始化阶段:
实例化
MyBean
对象。执行自定义的
init()
方法。执行
InitializingBean
接口的afterPropertiesSet()
方法。执行
@PostConstruct
注解标记的postConstruct()
方法。初始化后阶段:
执行自定义的销毁方法
destroyCustom()
。执行
DisposableBean
接口的destroy()
方法。执行
@PreDestroy
注解标记的preDestroy()
方法。
System.out.println("DisposableBean: MyBean destroyed.");
}
public void init() {
System.out.println("Custom init method: MyBean initialized.");
}
public void destroyCustom() {
System.out.println("Custom destroy method: MyBean destroyed.");
}
}
> 在这个示例中,`MyBean`类实现了`InitializingBean`和`DisposableBean`接口,同时还定义了自定义的初始化方法和销毁方法。此外,它还使用了`@PostConstruct`和`@PreDestroy`注解,表示初始化前和初始化后执行的方法。
> 当我们启动Spring容器时,它会按照以下顺序执行`MyBean`类的初始化和销毁:
>
> 1. 初始化前阶段:
> - 扫描所有的配置文件和组件扫描器等等。
>
> 2. 初始化阶段:
>
> - 实例化`MyBean`对象。
>
> - 执行自定义的`init()`方法。
>
> - 执行`InitializingBean`接口的`afterPropertiesSet()`方法。
>
> - 执行`@PostConstruct`注解标记的`postConstruct()`方法。
>
> 3. 初始化后阶段:
>
> - 执行自定义的销毁方法`destroyCustom()`。
>
> - 执行`DisposableBean`接口的`destroy()`方法。
>
> - 执行`@PreDestroy`注解标记的`preDestroy()`方法。
>
> - 执行一些后处理操作,例如清理无用的bean、执行一些额外的逻辑、发布应用程序事件等等。