Spring基础总结(上)
创始人
2024-05-26 02:47:36
0

Spring基础总结(上)

1. Spring 如何创建一个 Bean 对象

通过调用对象的无参构造方法创建一个新对象,然后通过依赖注入得到bean对象(默认单例)依赖注入这一步对新对象中添加了 @Autowired 或者@Resource 等注解的属性赋值,得到 Bean 对象,如下是一段伪代码

//1.通过无参构造函数创建一个新对象 userService
UserService userService = new UserService();//2.遍历对象的属性,给添加了 @Autowired 注解的属性赋值
for(Field field : userService.getClass().getDeclaredFields()) {if(field.isAnnotationPresent(Autowired.class)) {field.set(userService, ??)}
}
... ...

2. 什么是单例池?作用是什么?先看如下代码

AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);UserService userService1 = (UserService)context.getBean("userService");
UserService userService2 = (UserService)context.getBean("userService");
System.out.println(userService1.equals(userService2)); // true

我们可以发现获取到的两个 UserService 对象是相同的,这说明 spring 有一个类似 Map 的集合来保存单例 Bean,key 值为 bean 的 name 属性,value 的值为 bean 本身,这个保存 bean 的集合就是单例池,它的作用就是用来存储单例 Bean 对象;如果是多例 Bean,就不会被放入到单例池

3. @PostConstruct 注解的作用

先看如下代码

@Component
@Data
public class UserService {    private User user;
}

我们在 UserService 中定义了一个 User(id, name) 属性,假如有这样的需求:我们需要在使用 userService 这个 Bean 的时候,获取 user 属性的值;
可能很多人觉得,那可以将 user 也定义成一个 Bean,然后注入到 UserService 不就好了么;但是如果这个 user 的信息需要从数据库查询出来并赋值呢?
这样就比较麻烦了;如果在 UserService 初始化成 Bean 的时候,让 User 对象也初始化属性就好了,这个解决办法就是定义一个初始化 User 属性的方法,并添加 @PostConstruct 让 spring 在初始化前去初始化 User 的属性,如下调用对象的无参构造方法 --> UserService 对象 --> 依赖注入 --> 初始化前(调用初始化方法 initUser()) --> 初始化 --> 初始化后 --> 放入单例池(单例 Bean 对象) 所以可改造代码如下

@Component
@Data
public class UserService {private User user;@PostConstructprivate void initUser() {user = new User();user.setId(1);user.setName("dufu");}
}

注意:这个注解在某些 JDK(高版本,例如:1.8)版本没有,这实际上是 JDK 自己的注解!如果找不到可以添加如下依赖

    javax.annotation    javax.annotation-api    1.3.2

除此之外,spring 也提供了一个解决方案,就是实现 InitializingBean 接口并实现 afterPropertiesSet() 方法,所以也可以改造代码如下

@Component
@Data
public class UserService implements InitializingBean {private User user;public void afterPropertiesSet() throws Exception {user = new User();user.setId(1);user.setName("dufu");}
}

但要注意 afterPropertiesSet() 方法是在初始化的时候执行,而 @PostConstruct 注解下的方法是在初始化前执行;调用对象的无参构造方法 --> UserService 对象 --> 依赖注入 --> 初始化前(执行 initUser()) --> 初始化(执行 afterPropertiesSet()) --> 初始化后 --> 放入单例池(单例 Bean 对象)

4. Bean 的实例化和 Bean 的初始化的区别

Bean 的实例化:调用对象的无参构造方法 --> 对象

Bean 的初始化:调用对象指定的方法初始化对象,也就是 spring 的 initializeBean() 方法做的事情

5. 推断构造方法

  1. 在实例化 Bean 的时候,默认会调用 Bean 的无参构造方法(即时有多个构造方法);如下代码,我们可以验证一下
@Component
@Data
public class UserService {@Autowiredprivate User user;/*** 无参构造函数*/public UserService() {System.out.println("调用了无参构造方法");}/*** 有参构造函数1*/public UserService(User user) {System.out.println("调用了有参构造函数1");}
}
// 调用
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
UserService userService = (UserService)context.getBean("userService");
context.close();
  1. 如果定义了多个构造方法但没有无参构造方法会报错,如下去掉代码中的无参构造方法,则会抛出异常:No default constructor found … …
@Component
@Data
public class UserService {@Autowiredprivate User user;/*** 有参构造函数1*/public UserService(User user) {System.out.println("调用了有参构造函数1");}
}

3)如果要指定实例化 Bean 的时候定义了多个构造方法中的某一个,需要在该构造方法上添加 @Autowired 注解(此时有没有其他构造方法都没有影响)

@Component
@Data
public class UserService {private User user;/*** 有参构造函数1*/public UserService() {System.out.println("调用了无参构造函数");}/*** 有参构造函数1,添加 @Autowired 注解, 指定调用该构造方法*/@Autowiredpublic UserService(User user) {System.out.println("调用了有参构造函数1");}
}

6. 从单例池中获取 Bean 的步骤

先根据 type 获取,然后再根据 name 获取在单例池中,可能会存在 bean 的 name 不一样,但 bean 的类型相同的情况,如下代码

@Bean
public User user1() {return new User();
}
@Bean
public User user2() {return new User();
}
@Bean
public User user3() {return new User();
}
// 调用
System.out.println(context.getBean("user1"));
System.out.println(context.getBean("user2"));
System.out.println(context.getBean("user3"));

可以看到这是 3 个类型一样但又不相同的 Bean;那么在初始化 Bean 的构造函数中,如下

@Component
public class UserService {private User user;@Autowiredpublic UserService(User user1) {System.out.println(user1);this.user = user1;}
}

在初始化 UserService 并调用 UserService 的构造方法的时候,先根据类型找出 User 类型的 Bean(3个),然后再根据 name(user1) 找出到底使用哪一个进行初始化操作;所以我们将构造函数改为

@Autowired
public UserService(User user4) {System.out.println(user4);this.user = user4;
}

就会报错,No qualifying bean of type [com.spring.demo.entity.User] is defined: expected single matching bean but found 3: user1,user2,user3
因为单例池中虽然有 User 类型的 Bean,但没有 name = user4 的 Bean;但是如果单例池中只有一个 User 类型的 Bean,因为 Spring 根据类型已经能找到 User 类型的 Bean,就不会根据名字再找,那么上面写成 user4 就不会报错

7. AOP实现原理

在 Bean 初始化后,如果有 AOP,则通过 AOP 得到一个代理对象,然后把代理对象放入到单例池中(注意不是普通对象),否则放入的是 Bean 实例化后的普通对象,流程如下

调用对象的无参构造方法 --> UserService 对象 --> 依赖注入 --> 初始化前(执行 initUser()) --> 初始化(执行 afterPropertiesSet()) --> 初始化后(AOP) --> 代理对象 --> 放入单例池(单例 Bean 对象)

如下,我们添加了一个 AOP 实现

@Component
@Aspect
public class SystemAop {private final String pointCut = "execution(* com.spring.demo.service.*.*(..))";@Before(pointCut)public void before(JoinPoint jp) {System.out.println("Aop Before ... ...");}@Around(pointCut)public Object around(ProceedingJoinPoint pjp) throws Throwable {Object result = pjp.proceed();System.out.println("Aop Around ... ...");return result;}@After(pointCut)public void after() {System.out.println("Aop After ... ...");}
}

在 UserService 中添加如下代码

@Component
public class UserService {@Autowiredprivate User user;public void print() {System.out.println(user);}
}

在调用如下代码的时候

public static void main(String[] args) {AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);UserService userService = (UserService)context.getBean("userService");userService.print(); // 输出:com.spring.demo.entity.User@3d34d211context.close();
}

在 UserService 中注入了 User 对象;如果在 userService.print(); 这句代码打断点,可以看到,其 user 属性是 Null;这是因为代理对象中没有 user 属性;但最终却能打印出 user 属性的值,这是为什么呢?Spring 的 AOP 是通过 CGLIB 实现的,其实现是基于继承的方式,伪代码如下

public UserServiceProxy extends UserService {// 实例池中的普通对象UserService target;// 重载父类中的 test() 方法public void test() {切面逻辑 @Beforetarget.test(); // 此处的 target 就是实例池中的普通对象切面逻辑 @After}
}

可以看到,代理对象继承了普通对象,这样就可以在 context.getBean(“userService”) 的时候实现强制转化(UserService userService = (UserService)context.getBean(“userService”));代理对象中的 target 属性的值就是普通对象,这样在调用 test() 方法的时候,就可以通过普通对象去调用;所以在代理对象中,user 属性 = null,但普通对象中 user 有值
在这里插入图片描述

@Before(pointCut)
public void before(JoinPoint jp) {System.out.println("Aop Before ... ...");System.out.println(jp.getThis().getClass().getName()); // 代理对象System.out.println(jp.getTarget().getClass().getName()); // 被代理的对象
}

8. Spring 事务实现原理

如下我们实现了一个基本的 Spring 的事务示例

  1. 在 AppConfig 配置上添加 @EnableTransactionManagement 注解,打开事务;并添加数据源和 JdbcTemplate
@Configuration
@EnableAspectJAutoProxy
@EnableTransactionManagement
@ComponentScan("com.spring.demo")
public class AppConfig {@Beanpublic JdbcTemplate jdbcTemplate() {return new JdbcTemplate(dataSource());}@Beanpublic PlatformTransactionManager transactionManager() {DataSourceTransactionManager transactionManager = new DataSourceTransactionManager();transactionManager.setDataSource(dataSource());return transactionManager;}@Beanpublic DataSource dataSource() {DriverManagerDataSource dataSource = new DriverManagerDataSource();dataSource.setUrl("xxx");dataSource.setUsername("xxx");dataSource.setPassword("xxx");return dataSource;}
}
  1. 在 UserService 中,注入 JdbcTemplate 并添加 insert() 方法;代码如下
@Component
public class UserService {@Autowiredprivate JdbcTemplate jdbdTemplate;@Transactionalpublic void insert() {jdbdTemplate.execute("insert into user(id, name) values(1, 'dufu')");throw throw new RuntimeException();}
}

注意在 insert() 方法上我们添加了 @Transactional 注解,表示支持事务,在方法中执行了插入操作后,再抛出异常;如果事务生效的话,那么这条记录将不会被插入到数据库中,事实也确实如此;如下是一段 Spring 中实现事务的伪代码

public UserServiceProxy extends UserService {UserService target;// 重载父类中的 insert() 方法public void insert() {... ...// 如果方法上面添加了 @Transactional 注解,就处理事务if(method.isAnnotationPresent(Transactional.class)) {// 开启事务try {// 1.通过事务管理器新建一个 connection;Connection conn = xxx.getConnection();// 2.关闭 autocommit 属性conn.autocommit = false;// 3.普通对象执行数据操作target.insert();... ...} catch(Exception e) {// 如果执行异常, 就回滚操作conn.rollback();}}}
}

注意:如果我们去掉了 AppConfig 配置类上的 @Configuration 注解的话,事务就会失效!这是为什么呢?这是因为在事务管理器中创建 Connection 的时候,是通过 ThreadLocal>为了保证可以使用多数据源,这里使用 Map 来存储 Connection,必须要保证事务中的 dataSource 和 jdbcTemplate 中的 dataSource 是同一个才能保证事务有效!如果没有添加 @Configuration 注解,事务和 jdbcTemplate 拿到的
dataSource 是两个 new 出来的对象(看下面的代码,两个 Bean 中都有:new DataSource()),不是同一个;添加了 @Configuration 注解,AppConfig 会被动态动态代理,在代理对象中,拿其他的 Bean 都是从 Spring 容器的单例池中拿的,就能保证拿到的 dataSource 是同一个对象

@Bean
public JdbcTemplate jdbcTemplate() {return new JdbcTemplate(dataSource());
}@Bean
public PlatformTransactionManager transactionManager() {DataSourceTransactionManager transactionManager = new DataSourceTransactionManager();transactionManager.setDataSource(dataSource());return transactionManager;
}

了解了 Spring 的事务实现原理后,再看下面这段代码

@Component
public class UserService {@Autowiredprivate JdbcTemplate jdbdTemplate;@Transactionalpublic void insert() {jdbdTemplate.execute("insert into user(id, name) values(1, 'dufu')");insert1();}// 不提交事务, 也就是说不会执行下面的插入操作@Transactional(propagation = Propagation.NEVER)public void insert() {jdbdTemplate.execute("insert into user(id, name) values(1, 'dufu')");}
}

按照正常逻辑,只会插入 insert() 方法中的数据,insert1() 方法中的数据是不会插入的,但是最终发现,两条数据都被插入了,也就是说 insert1() 方法的事务失效了,这是什么原因呢?这是因为,在上面伪代码中,target.insert(); 这句代码是普通对象执行的,insert() 方法下的 insert1() 方法也是普通对象执行的,这样的话就 insert1() 方法根本没有进入代理对象执行 insert() 方法的逻辑;而 target.insert() 方法可以,是因为外层包裹了代理对象重载的insert() 方法!

那如何让 insert() 方法下的 insert1() 方法能被事务托管呢?其解决办法就是让 insert1() 方法被代理对象调用即可,所以可修改代码如下

@Component
public class UserService {@Autowiredprivate JdbcTemplate jdbdTemplate;// Spring 中可以注入自己@Autowiredprivate UserService userService;@Transactionalpublic void insert() {jdbdTemplate.execute("insert into user(id, name) values(1, 'dufu')");userService.insert1();}// 不提交事务, 也就是说不会执行下面的插入操作@Transactional(propagation = Propagation.NEVER)public void insert() {jdbdTemplate.execute("insert into user(id, name) values(2, 'dufu')");}
}

9. Spring 为什么使用三级缓存解决循环依赖

循环依赖就是相互依赖,A 注入 B,B 注入 A 这种情况就是循环依赖的一个场景;在 Spring 中,通过三级缓存解决了循环依赖的问题;三级缓存分别为

单例池:singletonObjects 保存的是经过完整的生命周期后的Bean
第二级缓存:earlySingletonObjects 保存没有经过完整生命周期的单例对象
第三级缓存:singletonFactories 保存根据 Bean 的信息生成的 Lambda 表达式,用于循环依赖时候判断是否需要提前 AOP

A 的创建生命周期如下

  1. 创建一个 A 普通对象(同时记录到 creatingSet(“a”)) --> 存储到 singletonFactories
  2. 填充 B 属性 --> 去单例池中找 B 对象 --> 找不到就创建 B 对象
  3. 填充其他属性4. 其他操作5. 初始化后(AOP)6. 放入到单例池

B 的创建生命周期如下

  1. 创建一个 B 普通对象 --> 存储到 singletonFactories
  2. 填充 A 属性 --> 去单例池中找 A 对象 --> 通过 creatingSet 确定 A 正在创建中(即出现了循环依赖的情况) --> 到 earlySingletonObjects 中寻找 Bean 的信息 lambda 表达式(如果找到了,返回并赋值,没有的话下一步) --> 到 singletonFactories 找,提前 AOP(如果需要 AOP) 得到 A 的代理对象(如果不需要 AOP 得到普通对象) --> 放到 earlySingletonObjects
  3. 填充其他属性
  4. 其他操作
  5. 初始化后(AOP)
  6. 放入到单例池

只要出现了循环依赖的情况,都会去二级缓存中去找 Bean 对象,三级缓存的目的就是为了怎么生成对应的 Bean 对象(普通对象还是代理对象) 并放入到二级缓存中

10. 在循环依赖的情况下,添加了 @Async 会报错

  1. 例如,AService 和 BService 相互依赖注入,并在 AService 下定义了一个添加了 @Async 注解的方法如下
@Component
public class AService {@Resourceprivate BService bService;@Asyncpublic void print() {System.out.println(bService);}
}

在调用 AService 下的 print 方法的时候会报错

public static void main(String[] args) {AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);AService aService = (AService)context.getBean("AService");aService.print();context.close();
}

报错信息:Error creating bean with name ‘AService’: Bean with name ‘AService’ has been injected into other beans [BService] in its raw version as part of a circular reference, but has eventually been wrapped. This means that said other beans do not use the final version of the bean. This is often the result of over-eager type matching - consider using ‘getBeanNamesOfType’ with the ‘allowEagerInit’ flag turned off, for example.

这是因为,添加了 @Async 注解后,Spring 会生成 AService 的代理对象,而在循环依赖的情况下,Spring 也创建了一个 AService 的代理对象,这两个代理对象是不一样的,所以会报错;在开发中,应尽量避免这种既有循环依赖又有 @Async 的情况,如果实在没有办法避免,可以在注入的 BService 对象上添加一个 @Lazy 注解即可,如下

@Component
public class AService {@Resource@Lazyprivate BService bService;@Asyncpublic void print() {System.out.println(bService);}
}

这是因为,添加了 @Lazy 注解后,注入的 BService 是一个通过 @Lazy 注解逻辑直接生成的代理对象(不是从单例池中获取的),在 print() 方法中使用的时候,才会从单例池中去取出真正要使用的 BService 对象

  1. 在循环依赖的情况下通过构造方法注入同样会产生这样的问题(甚至连普通对象都无法生成),如下
@Component
public class AService {private BService bService;/*** 构造方法注入 BService*/public AService(BService bService) {this.bService = bService;}public void print() {System.out.println(bService);}
}

解决办法同样是在构造方法上添加 @Lazy 注解

3)在循环依赖的情况下,如果 AService 和 BService 是多例 Bean 的话,也会报错

11. @Value 的使用

在 AppConfig 配置类中,添加一个 Bean,指定读取的 properties 文件地址;如果 Spring 的版本在 5 及以上,则可省略该步骤

@Bean
public PreferencesPlaceholderConfigurer configurer() {PreferencesPlaceholderConfigurer configurer = new PreferencesPlaceholderConfigurer();configurer.setLocation(new ClassPathResource("/application.properties"));return configurer;
}

在其他类中,就可以通过 @Value(${xxx}) 的方式获取到 properties 配置文件中的值

@Component
public class AService {@Value("${appName}")private String appName;public void print() {System.out.println(appName);}
}

如果使用 @Value(#{Bean的name}) 这种方式,可以实现像 @Resource 注解的功能;例如:

@Component
public class AService {@Value("#{BService}")private BService bService;public void test() {System.out.println(bService);}
}

假如在 properties 配置文件中有一个很多地方都在使用的配置,如果还是在各个使用的地方添加 @Value(“${keyName}”) 这种方式,假如修改了 keyName 的话,很容易因为漏改而出现问题;有两个简单的解决办法

新建一个类,把常用的值注入到这个类的属性中
新建一个注解,在该注解上添加 @Value(“${keyName}”) 注解,在使用到这个值的地方,使用我们新建的这个注解因为新建的这个注解不需要添加属性,所以不存在修改的问题;如下

@Target({ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Value(${xxx})
public @interface MyValue {}

12. @Conditional 注解

该注解可用于在注册 Bean 的时候,根据自定义条件让 Spring 容器注册 Bean;或者对 Bean 做一些操作

  1. 首先新建 MyCondition 类并实现 Condition 接口,重新其 matches 方法,这里我们给 BService 这个 Bean 设置 name 属性的值
public class MyCondition implements Condition {public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {try {// 读取本地配置文件Resource resource = context.getResourceLoader().getResource("/application.properties");Properties properties = new Properties();properties.load(resource.getInputStream());		BeanFactory beanFactory = context.getBeanFactory();if(beanFactory.containsBean("BService")) {// 给 BService 的  name 属性赋值BService bService = (BService) beanFactory.getBean(BService.class);bService.setName((String)properties.getProperty("name"));}} catch (IOException e) {return false;}return true;}
}
  1. BService 类的代码如下,注意我们添加了 @Conditional(MyCondition.class) 注解
@Component
@Data
@Conditional(MyCondition.class)
public class BService {    private String name;
}
  1. 在 AService 中注入 BService,并打印 BService 的 name 属性的值
@Component
public class AService {@Resourceprivate BService bService;public void print() {System.out.println(bService.getName());}
}

相关内容

热门资讯

监控摄像头接入GB28181平... 流程简介将监控摄像头的视频在网站和APP中直播,要解决的几个问题是:1&...
Windows10添加群晖磁盘... 在使用群晖NAS时,我们需要通过本地映射的方式把NAS映射成本地的一块磁盘使用。 通过...
protocol buffer... 目录 目录 什么是protocol buffer 1.protobuf 1.1安装  1.2使用...
在Word、WPS中插入AxM... 引言 我最近需要写一些文章,在排版时发现AxMath插入的公式竟然会导致行间距异常&#...
【PdgCntEditor】解... 一、问题背景 大部分的图书对应的PDF,目录中的页码并非PDF中直接索引的页码...
修复 爱普生 EPSON L4... L4151 L4153 L4156 L4158 L4163 L4165 L4166 L4168 L4...
Fluent中创建监测点 1 概述某些仿真问题,需要创建监测点,用于获取空间定点的数据࿰...
educoder数据结构与算法...                                                   ...
MySQL下载和安装(Wind... 前言:刚换了一台电脑,里面所有东西都需要重新配置,习惯了所...
MFC文件操作  MFC提供了一个文件操作的基类CFile,这个类提供了一个没有缓存的二进制格式的磁盘...