Spring基础总结(上)

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 注解的作用

先看如下代码



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 对象) 所以可改造代码如下



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

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

<dependency>    
    <groupId>javax.annotation</groupId>    
    <artifactId>javax.annotation-api</artifactId>    
    <version>1.3.2</version>
</dependency>

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



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 的无参构造方法(即时有多个构造方法);如下代码,我们可以验证一下


public class UserService {
    
    private 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 … …


public class UserService {
    
    private User user;
	
    /**
     * 有参构造函数1
     */
    public UserService(User user) {
        System.out.println("调用了有参构造函数1");
    }
}

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



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

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

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


public User user1() {
    return new User();
}

public User user2() {
    return new User();
}

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 的构造函数中,如下


public class UserService {
    private User user;

    
    public UserService(User user1) {
        System.out.println(user1);
        this.user = user1;
    }
}

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


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 实现



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

在 UserService 中添加如下代码


public class UserService {
    
    private 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@3d34d211
    context.close();
}

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

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

可以看到,代理对象继承了普通对象,这样就可以在 context.getBean(“userService”) 的时候实现强制转化(UserService userService = (UserService)context.getBean(“userService”));代理对象中的 target 属性的值就是普通对象,这样在调用 test() 方法的时候,就可以通过普通对象去调用;所以在代理对象中,user 属性 = null,但普通对象中 user 有值
Spring基础总结(上)_第1张图片

(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



("com.spring.demo")
public class AppConfig {
    
    public JdbcTemplate jdbcTemplate() {
        return new JdbcTemplate(dataSource());
    }
	
    
    public PlatformTransactionManager transactionManager() {
        DataSourceTransactionManager transactionManager = new DataSourceTransactionManager();
        transactionManager.setDataSource(dataSource());
        return transactionManager;
    }
	
    
    public DataSource dataSource() {
        DriverManagerDataSource dataSource = new DriverManagerDataSource();
        dataSource.setUrl("xxx");
        dataSource.setUsername("xxx");
        dataSource.setPassword("xxx");
        return dataSource;
    }
}
  1. 在 UserService 中,注入 JdbcTemplate 并添加 insert() 方法;代码如下

public class UserService {
    
    private JdbcTemplate jdbdTemplate;
	
    
    public 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 是同一个对象


public JdbcTemplate jdbcTemplate() {
    return new JdbcTemplate(dataSource());
}


public PlatformTransactionManager transactionManager() {
    DataSourceTransactionManager transactionManager = new DataSourceTransactionManager();
    transactionManager.setDataSource(dataSource());
    return transactionManager;
}

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


public class UserService {
    
    private JdbcTemplate jdbdTemplate;
	
    
    public void insert() {
        jdbdTemplate.execute("insert into user(id, name) values(1, 'dufu')");
	insert1();
    }
	
    // 不提交事务, 也就是说不会执行下面的插入操作
    (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() 方法被代理对象调用即可,所以可修改代码如下


public class UserService {
    
    private JdbcTemplate jdbdTemplate;
	
    // Spring 中可以注入自己
    
    private UserService userService;
	
    
    public void insert() {
        jdbdTemplate.execute("insert into user(id, name) values(1, 'dufu')");
        userService.insert1();
    }
	
    // 不提交事务, 也就是说不会执行下面的插入操作
    (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 注解的方法如下

public class AService {
    
    private BService bService;
	
    
    public 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 注解即可,如下


public class AService {
    
    
    private BService bService;
	
    
    public void print() {
        System.out.println(bService);
    }
}

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

  1. 在循环依赖的情况下通过构造方法注入同样会产生这样的问题(甚至连普通对象都无法生成),如下

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 及以上,则可省略该步骤


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

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


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

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


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

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

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

({ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER, ElementType.ANNOTATION_TYPE})
(RetentionPolicy.RUNTIME)
(${xxx})
public  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) 注解


(MyCondition.class)
public class BService {    
    private String name;
}
  1. 在 AService 中注入 BService,并打印 BService 的 name 属性的值

public class AService {
    
    private BService bService;
	
    public void print() {
        System.out.println(bService.getName());
    }
}

你可能感兴趣的:(#,Spring3基础,spring,java,mybatis)