文章详情

短信预约-IT技能 免费直播动态提醒

请输入下面的图形验证码

提交验证

短信预约提醒成功

黑马程序员Spring视频教程,全面深度讲解spring5底层原理 学习笔记

2023-09-29 05:57

关注

介绍

代码仓库地址:https://gitee.com/CandyWall/spring-source-study
跟着黑马满一航老师的spring高级49讲做的学习笔记,本笔记跟视频内容的项目名称和代码略有不同,我将49讲的代码每一讲的代码都拆成了独立的springboot项目,并且项目名称尽量做到了见名知意,都是基于我自己的考量,代码都已经过运行验证过的,仅供参考。

视频教程地址:https://www.bilibili.com/video/BV1P44y1N7QG

注:

1. 每一讲对应一个二级,每一个三级是使用子项目名称命名的,和我代码仓库的项目是一一对应的;
2. 代码里面用到了lombok插件来简化了Bean中的get()、set()方法,以及日志的记录的时候用了lombok的@Slf4j注解。

笔记中如有不正确的地方,欢迎在评论区指正,非常感谢!!!

每个子项目对应的视频链接以及一些重要内容的笔记

第一讲 BeanFactoryApplicationContext的区别与联系

spring_01_beanfactory_applicationcontext_differences_connections

p1 000-Spring高级49讲-导学

p2 001-第一讲-BeanFactory与ApplicationContext_1

测试代码:

@SpringBootApplication@Slf4jpublic class A01Application {    public static void main(String[] args) {        ConfigurableApplicationContext context = SpringApplication.run(A01Application.class, args);        // class org.springframework.boot.web.servlet.context.AnnotationConfigServletWebServerApplicationContext        System.out.println(context.getClass());    }}

到底什么是BeanFactory

p3 002-第一讲-BeanFactory功能

BeanFactory接口中的方法

image-20220323145908937

查看springboot默认的ConfigurableApplicationContext类中的BeanFactory的实际类型

ConfigurableApplicationContext context = SpringApplication.run(A01Application.class, args);// org.springframework.boot.web.servlet.context.AnnotationConfigServletWebServerApplicationContextConfigurableListableBeanFactory beanFactory = context.getBeanFactory();// 查看实际类型// class org.springframework.beans.factory.support.DefaultListableBeanFactorySystem.out.println(beanFactory.getClass());

从打印结果可以了解到实际类型为DefaultListableBeanFactory,所以这里以BeanFactory的一个实现类DefaultListableBeanFactory作为出发点,进行分析。

它的类图如下:

image-20220323150712761

这里我们暂且不细看DefaultListableBeanFactory,先看DefaultListableBeanFactory的父类DefaultSingletonBeanFactory,先选中它,然后按F12,可以跳转到对应的源码,可以看到有个私有的成员变量singletonObjects

image-20220323150929451

这里通过反射的方法来获取该成员变量,进行分析

先补充一下反射获取某个类的成员变量的步骤:

获取成员变量,步骤如下:

  1. 获取Class对象

  2. 获取构造方法

  3. 通过构造方法,创建对象

  4. 获取指定的成员变量(私有成员变量,通过setAccessible(boolean flag)方法暴力访问)

  5. 通过方法,给指定对象的指定成员变量赋值或者获取值

public void set(Object obj, Object value)

​ 在指定对象obj中,将此 Field 对象表示的成员变量设置为指定的新值

​ public Object get(Object obj)

​ 返回指定对象obj中,此 Field 对象表示的成员变量的值

代码如下:

Field singletonObjects = DefaultSingletonBeanRegistry.class.getDeclaredField("singletonObjects");// 设置私有变量可以被访问singletonObjects.setAccessible(true);ConfigurableListableBeanFactory beanFactory = context.getBeanFactory();Map<String, Object> map = (Map<String, Object>) singletonObjects.get(beanFactory);// 查看实际类型// class org.springframework.beans.factory.support.DefaultListableBeanFactorySystem.out.println(beanFactory.getClass());map.entrySet().stream().filter(entry -> entry.getKey().startsWith("component")).forEach(System.out::println);

这里singletonObjects.get(beanFactory)为什么要传一个ConfigurableListableBeanFactory的变量进去呢?打印了这个beanFactory的实际类型为DefaultListableBeanFactory,查看其类图,可以了解到该类也实现了DefaultSingletonBeanRegistry接口,所以这里反射获取某个类的成员变量的get()方法中可以作为参数传进来。

image-20220324003636506

p4 003-第一讲-ApplicationContext功能1

ApplicationContextBeanFactory 多点啥?

多实现了四个接口:

image-20220324115647260

  1. MessageSource

    resources目录下创建四个文件messages.propertesmessages_en.propertiesmessages_ja.propertiesmessages_zh.properties,然后分别在四个文件里面定义同名的key,比如在message_en.properties中定义hi=hello,在messages_ja.propertes中定义hi=こんにちは,在messages_zh中定义hi=你好,这样在代码中就可以根据这个**key hi和不同的语言类型**获取不同的value了。

    System.out.println(context.getMessage("hi", null, Locale.CHINA));System.out.println(context.getMessage("hi", null, Locale.ENGLISH));System.out.println(context.getMessage("hi", null, Locale.JAPANESE));

    运行结果如下:

    image-20220324181409040

p5 004-第一讲-ApplicationContext功能2,3

  1. ResourcePatternResolver

    例1:获取类路径下的messages开头的配置文件

    Resource[] resources = context.getResources("classpath:messages*.properties");for (Resource resource : resources) {    System.out.println(resource);}

    image-20220324182456169

    例2:获取spring相关jar包中的spring.factories配置文件

    resources = context.getResources("classpath*:META-INF/spring.factories");for (Resource resource : resources) {    System.out.println(resource);}

    image-20220324183236048

  2. EnvironmentCapable

    获取系统环境变量中的java_home和项目的application.yml中的server.port属性

    System.out.println(context.getEnvironment().getProperty("java_home"));System.out.println(context.getEnvironment().getProperty("server.port"));

    image-20220324191740825

p6 005-第一讲-ApplicationContext功能4

  1. ApplicationEventPublisher

    定义一个用户注册事件类,继承自ApplicationEvent

    public class UserRegisteredEvent extends ApplicationEvent {    public UserRegisteredEvent(Object source) {        super(source);    }}

    再定义一个监听器类,用于监听用户注册事件,类头上需要加@Component注解,将该类交给spring管理,定义一个处理事件的方法,参数类型为用户注册事件类的对象,方法头上需要加上@EventListener注解

    @Component@Slf4jpublic class UserRegisteredListener {    @EventListener    public void userRegist(UserRegisteredEvent event) {        System.out.println("UserRegisteredEvent...");        log.debug("{}", event);    }}

    接着再定义一个用户服务类,里面有个register(String username, String password)方法可以完成用户的注册,注册完毕后发布一下用户注册完毕事件

    @Component@Slf4jpublic class UserService {    @Autowired    private ApplicationEventPublisher context;    public void register(String username, String password) {        log.debug("新用户注册,账号:" + username + ",密码:" + password);        context.publishEvent(new UserRegisteredEvent(this));    }}

    最后在Springboot启动类中调用一下UserService里面的register()方法注册一个新用户,UserRegisteredListener中就能处理这个用户注册完毕的事件,实现了UserService类和UserRegisteredListener类的解耦。

    UserService userService = context.getBean(UserService.class);userService.register("张三", "123456");

    image-20220324210306704

p7 006-第一讲-小结

第二讲 BeanFactoryApplicationContext 类的重要实现类

spring_02_01_beanfactory_impl

p8 007-第二讲-BeanFactory实现

p9 008-第二讲-BeanFactory实现

p10 009-第二讲-BeanFactory实现-后处理器排序

DefaultListableBeanFactory

接着第一讲中的内容,执行以下代码,可以了解到

ConfigurableApplicationContext context = SpringApplication.run(Application.class, args);// org.springframework.boot.web.servlet.context.AnnotationConfigServletWebServerApplicationContextConfigurableListableBeanFactory beanFactory = context.getBeanFactory();// 查看实际类型// class org.springframework.beans.factory.support.DefaultListableBeanFactorySystem.out.println(beanFactory.getClass());

ConfigurableApplicationContext类内部组合的BeanFactory实际类型为DefaultListableBeanFactoryspring底层创建实体类就是依赖于这个类,所以它是BeanFactory接口最重要的一个实现类,下面使用这个类,模拟一下spring使用DefaultListableBeanFactory类创建其他实体类对象的过程。

测试代码如下:

package top.jacktgq.spring_02_beanfactory_impl;import lombok.extern.slf4j.Slf4j;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.beans.factory.config.BeanFactoryPostProcessor;import org.springframework.beans.factory.config.BeanPostProcessor;import org.springframework.beans.factory.support.AbstractBeanDefinition;import org.springframework.beans.factory.support.BeanDefinitionBuilder;import org.springframework.beans.factory.support.DefaultListableBeanFactory;import org.springframework.context.annotation.AnnotationConfigUtils;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;import javax.annotation.Resource;import java.util.ArrayList;import java.util.stream.Collectors;public class TestBeanFactory {    public static void main(String[] args) {        DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory();        // bean 的定义(即bean的一些描述信息,包含class:bean是哪个类,scope:单例还是多例,初始化、销毁方法等)        AbstractBeanDefinition beanDefinition = BeanDefinitionBuilder.genericBeanDefinition(Config.class).setScope("singleton").getBeanDefinition();        beanFactory.registerBeanDefinition("config", beanDefinition);        // 给 BeanFactory添加一些常用的后处理器,让它具备解析@Configuration、@Bean等注解的能力        AnnotationConfigUtils.registerAnnotationConfigProcessors(beanFactory);        // 从bean工厂中取出BeanFactory的后处理器,并且执行这些后处理器        // BeanFactory 后处理器主要功能,补充了一些 bean 的定义        beanFactory.getBeansOfType(BeanFactoryPostProcessor.class).values().forEach(beanFactoryPostProcessor -> {            System.out.println(beanFactoryPostProcessor);            beanFactoryPostProcessor.postProcessBeanFactory(beanFactory);        });        // 打印BeanFactory中Bean        for (String name : beanFactory.getBeanDefinitionNames()) {            System.out.println(name);        }        // 从BeanFactory中取出Bean1,然后再从Bean1中取出它依赖的Bean2        // 可以看到结果为null,所以@Autowired注解并没有被解析        // Bean1 bean1 = beanFactory.getBean(Bean1.class);        // System.out.println(bean1.getBean2());        // 要想@Autowired、@Resource等注解被解析,还要添加Bean的后处理器,可以针对Bean的生命周期的各个阶段提供扩展        // 从bean工厂中取出Bean的后处理器,并且执行这些后处理器        // BeanFactory 后处理器主要功能,补充了一些 bean 的定义        // beanFactory.getBeansOfType(BeanPostProcessor.class).values().forEach(beanFactory::addBeanPostProcessor);        // beanFactory.addBeanPostProcessors(beanFactory.getBeansOfType(BeanPostProcessor.class).values());        // 改变Bean后处理器加入BeanFactory的顺序        // 写法1:        // ArrayList list = new ArrayList<>(beanFactory.getBeansOfType(BeanPostProcessor.class).values());        // Collections.reverse(list);        // beanFactory.addBeanPostProcessors(list);        // 写法2:        beanFactory.addBeanPostProcessors(beanFactory.getBeansOfType(BeanPostProcessor.class).values().stream().sorted(beanFactory.getDependencyComparator()).collect(Collectors.toCollection(ArrayList::new)));        // 准备好所有单例,get()前就把对象初始化好        beanFactory.preInstantiateSingletons();        System.out.println(">>>>>>>>>>>>>>>>>>>>>>>>>>>>");        Bean1 bean1 = beanFactory.getBean(Bean1.class);        System.out.println(bean1.getBean2());                System.out.println(bean1.getInter());    }    @Configuration    static class Config {        @Bean        public Bean1 bean1() {            return new Bean1();        }        @Bean        public Bean2 bean2() {            return new Bean2();        }        @Bean        public Bean3 bean3() {            return new Bean3();        }        @Bean        public Bean4 bean4() {            return new Bean4();        }    }    @Slf4j    static class Bean1 {        @Autowired        private Bean2 bean2;        public Bean2 getBean2() {            return bean2;        }        @Autowired        @Resource(name = "bean4")        private Inter bean3;        public Inter getInter() {            return bean3;        }        public Bean1() {            log.debug("构造 Bean1()");        }    }    @Slf4j    static class Bean2 {        public Bean2() {            log.debug("构造 Bean2()");        }    }    interface Inter {    }    @Slf4j    static class Bean3 implements Inter {        public Bean3() {            log.debug("构造 Bean3()");        }    }    @Slf4j    static class Bean4 implements Inter {        public Bean4() {            log.debug("构造 Bean4()");        }    }}

总结:

spring_02_02_applicationcontext_impl

p11 010-第二讲-ApplicationContext实现1,2

p12 011-第二讲-ApplicationContext实现3

p13 012-第二讲-ApplicationContext实现4

四个重要的ApplicationContext接口的实现类

image-20220325174005606

相关测试代码如下:

@Slf4jpublic class TestApplicationContext {    @Test    // ⬇️1.最为经典的容器,基于classpath 下 xml 格式的配置文件来创建    public void testClassPathXmlApplicationContext() {        ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("spring_bean.xml");        for (String name : context.getBeanDefinitionNames()) {            System.out.println(name);        }        System.out.println(context.getBean(Bean2.class).getBean1());    }    @Test    // ⬇️2.基于磁盘路径下 xml 格式的配置文件来创建    public void testFileSystemXmlApplicationContext() {        // 可以用绝对路径或者相对路径        // FileSystemXmlApplicationContext context = new FileSystemXmlApplicationContext("D:\\ideacode\\spring-source-study\\spring_02_02_applicationcontext_impl\\src\\main\\resources\\spring_bean.xml");        FileSystemXmlApplicationContext context = new FileSystemXmlApplicationContext("src\\main\\resources\\spring_bean.xml");        for (String name : context.getBeanDefinitionNames()) {            System.out.println(name);        }        System.out.println(context.getBean(Bean2.class).getBean1());    }    @Test    // ⬇️模拟一下ClassPathXmlApplicationContext和FileSystemXmlApplicationContext底层的一些操作    public void testMockClassPathAndFileSystemXmlApplicationContext() {        DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory();        System.out.println("读取之前");        for (String name : beanFactory.getBeanDefinitionNames()) {            System.out.println(name);        }        System.out.println("读取之后");        XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(beanFactory);        // reader.loadBeanDefinitions("spring_bean.xml");        // reader.loadBeanDefinitions(new ClassPathResource("spring_bean.xml"));        reader.loadBeanDefinitions(new FileSystemResource("src\\main\\resources\\spring_bean.xml"));        for (String name : beanFactory.getBeanDefinitionNames()) {            System.out.println(name);        }    }    @Test    // ⬇️3.较为经典的容器,基于java配置类来创建    public void testAnnotationConfigApplicationContext() {        // 会自动加上5个后处理器        // org.springframework.context.annotation.internalConfigurationAnnotationProcessor        // org.springframework.context.annotation.internalAutowiredAnnotationProcessor        // org.springframework.context.annotation.internalCommonAnnotationProcessor        // org.springframework.context.event.internalEventListenerProcessor        // org.springframework.context.event.internalEventListenerFactory        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(Config.class);        for (String name : context.getBeanDefinitionNames()) {            System.out.println(name);        }        System.out.println(context.getBean(Bean2.class).getBean1());    }    @Test    // ⬇️4.较为经典的容器,基于java配置类来创建,并且还可以用于web环境    // 模拟了 springboot web项目内嵌Tomcat的工作原理    public void testAnnotationConfigServletWebServerApplicationContext() throws IOException {        AnnotationConfigServletWebServerApplicationContext context = new AnnotationConfigServletWebServerApplicationContext(WebConfig.class);        // 防止程序终止        System.in.read();    }}@Configurationclass WebConfig {    @Bean    // 1. WebServer工厂    public ServletWebServerFactory servletWebServerFactory() {        return new TomcatServletWebServerFactory();    }    @Bean    // 2. web项目必备的DispatcherServlet    public DispatcherServlet dispatcherServlet() {        return new DispatcherServlet();    }    @Bean    // 3. 将DispatcherServlet注册到WebServer上    public DispatcherServletRegistrationBean dispatcherServletRegistrationBean(DispatcherServlet dispatcherServlet) {        return new DispatcherServletRegistrationBean(dispatcherServlet, "/");    }    @Bean("/hello")    public Controller controller1() {        return (request, response) -> {            response.getWriter().println("hello");            return null;        };    }}// 单元测试的过程中如果要解析一些Spring注解,比如@Configuration的时候不要把相关类定义到写单元测试类的内部类,会读取不到@Configurationclass Config {    @Bean    public Bean1 bean1() {        return new Bean1();    }    @Bean    public Bean2 bean2(Bean1 bean1) {        Bean2 bean2 = new Bean2();        bean2.setBean1(bean1);        return bean2;    }}class Bean1 {}class Bean2 {    private Bean1 bean1;    public Bean1 getBean1() {        return bean1;    }    public void setBean1(Bean1 bean1) {        this.bean1 = bean1;    }}

spring_bean.xml如下:

<?xml version="1.0" encoding="UTF-8"?><beans xmlns="http://www.springframework.org/schema/beans"       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"       xmlns:context="http://www.springframework.org/schema/context"       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd">    <!--5个后处理器加进来            等价于:AnnotationConfigUtils.registerAnnotationConfigProcessors(beanFactory);    -->    <context:annotation-config />    <bean id="bean1" class="top.jacktgq.Bean1" />    <bean id="bean2" class="top.jacktgq.Bean2">        <property name="bean1" ref="bean1"/>    </bean></beans>

第三讲 Bean的生命周期和模板方法设计模式

spring_03_bean_lifecycle

p14 013-第三讲-bean生命周期

springboot项目启动类

@SpringBootApplicationpublic class BeanLifeCycleApplication {    public static void main(String[] args) {        ConfigurableApplicationContext context = SpringApplication.run(BeanLifeCycleApplication.class, args);        context.close();    }}

定义一个LifeCycleBean,加上@Component注解,再编写一些方法,给这些方法加上Bean的生命周期过程中的注解

@Component@Slf4jpublic class LifeCycleBean {    public LifeCycleBean() {        log.debug("构造");    }    @Autowired    public void autowire(@Value("${JAVA_HOME}") String name) {        log.debug("依赖注入:{}", name);    }    @PostConstruct    public void init() {        log.debug("初始化");    }    @PreDestroy    public void destroy() {        log.debug("销毁");    }}

编写自定义Bean的后处理器,需要实现InstantiationAwareBeanPostProcessorDestructionAwareBeanPostProcessor接口,并加上@Component注解,对lifeCycleBean的生命周期过程进行扩展。

@Slf4j@Componentpublic class MyBeanPostProcessor implements InstantiationAwareBeanPostProcessor, DestructionAwareBeanPostProcessor {    @Override    // 实例化前(即调用构造方法前)执行的方法    public Object postProcessBeforeInstantiation(Class<?> beanClass, String beanName) throws BeansException {        if (beanName.equals("lifeCycleBean"))            log.debug("<<<<<<<<<<< 实例化前执行,如@PreDestroy");        // 返回null保持原有对象不变,返回不为null,会替换掉原有对象        return null;    }    @Override    // 实例化后执行的方法    public boolean postProcessAfterInstantiation(Object bean, String beanName) throws BeansException {        if (beanName.equals("lifeCycleBean")) {            log.debug("<<<<<<<<<<< 实例化后执行,这里如果返回 false 会跳过依赖注入阶段");            // return false;        }        return true;    }    @Override    // 依赖注入阶段执行的方法    public PropertyValues postProcessProperties(PropertyValues pvs, Object bean, String beanName) throws BeansException {        if (beanName.equals("lifeCycleBean"))            log.debug("<<<<<<<<<<< 依赖注入阶段执行,如@Autowired、@Value、@Resource");        return pvs;    }    @Override    // 销毁前执行的方法    public void postProcessBeforeDestruction(Object bean, String beanName) throws BeansException {        if(beanName.equals("lifeCycleBean"))            log.debug("<<<<<<<<<<<销毁之前执行");    }    @Override    // 初始化之前执行的方法    public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {        if(beanName.equals("lifeCycleBean"))            log.debug("<<<<<<<<<<< 初始化之前执行,这里返回的对象会替换掉原本的bean,如 @PostConstruct、@ConfigurationProperties");        return bean;    }    @Override    // 初始化之后执行的方法    public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {        if(beanName.equals("lifeCycleBean"))            log.debug("<<<<<<<<<<< 初始化之后执行,这里返回的对象会替换掉原本的bean,如 代理增强");        return bean;    }}

运行结果如下:

image-20220326140553213

p15 014-第三讲-模板方法

public class TestMethodTemplatePattern {    public static void main(String[] args) {        MyBeanFactory beanFactory = new MyBeanFactory();        beanFactory.addBeanPostProcessor(bean -> System.out.println("解析 @Autowired"));        beanFactory.addBeanPostProcessor(bean -> System.out.println("解析 @Resource"));        beanFactory.getBean();    }    static class MyBeanFactory {        public Object getBean() {            Object bean = new Object();            System.out.println("构造:" + bean);            System.out.println("依赖注入:" + bean);            for (BeanPostProcessor processor : processors) {                processor.inject(bean);            }            System.out.println("初始化:" + bean);            return bean;        }        private List<BeanPostProcessor> processors = new ArrayList<>();        public void addBeanPostProcessor(BeanPostProcessor beanPostProcessor) {            processors.add(beanPostProcessor);        }    }    interface BeanPostProcessor {        void inject(Object bean);    }}

第四讲 常见Bean后处理器以及@Autowired注解被解析的详细过程

spring_04_beanpostprocessor

p16 015-第四讲-常见bean后处理器1,2

p17 016-第四讲-常见bean后处理器3

定义三个Bean,名称分别为Bean1Bean2Bean3,其中Bean1中依赖了Bean2Bean3Bean2通过@Autowired的注解注入,Bean3通过@Resource注解注入,再通过@Value注解注入一个Java的环境变量JAVA_HOME的值。最后定义两个方法init()destroy(),分别加上@PostConstruct@PreDestroy注解。

@Slf4jpublic class Bean1 {    private Bean2 bean2;    @Autowired    public void setBean2(Bean2 bean2) {        log.debug("@Autowired 生效:{}", bean2);        this.bean2 = bean2;    }    @Autowired    public void setJava_home(@Value("${JAVA_HOME}") String java_home) {        log.debug("@Value 生效:{}", java_home);        this.java_home = java_home;    }    private Bean3 bean3;    @Resource    public void setBean3(Bean3 bean3) {        log.debug("@Resource 生效:{}", bean3);        this.bean3 = bean3;    }    private String java_home;    @PostConstruct    public void init() {        log.debug("@PostConstruct 生效:{}");    }    @PreDestroy    public void destroy() {        log.debug("@PreDestroy 生效:{}");    }    @Override    public String toString() {        return "Bean1{" +                "bean2=" + bean2 +                ", bean3=" + bean3 +                ", java_home='" + java_home + '\'' +                '}';    }}public class Bean2 {}public class Bean3 {}@ConfigurationProperties(prefix = "java")@Slf4jpublic class Bean4 {    private String home;    private String version;}

这里用GenericApplicationContext 来探究一下@Autowired@Value@Resource@PostConstruct@PreDestroy以及springboot项目中的@ConfigurationProperties这些注解分别是由哪个后处理器来解析的。

注:GenericApplicationContext 是一个【干净】的容器,默认不会添加任何后处理器,方便做测试,这里用DefaultListableBeanFactory也可以完成测试,只是会比使用GenericApplicationContext麻烦一些。

测试代码如下:

@Slf4jpublic class TestBeanPostProcessor {    @Test    public void testBeanPostProcessor() throws Exception {        // ⬇️GenericApplicationContext 是一个【干净】的容器,默认不会添加任何后处理器,方便做测试        // 这里用DefaultListableBeanFactory也可以完成测试,只是会比使用GenericApplicationContext麻烦一些        GenericApplicationContext context = new GenericApplicationContext();        // 用原始方法注册三个Bean        context.registerBean("bean1", Bean1.class);        context.registerBean("bean2", Bean2.class);        context.registerBean("bean3", Bean3.class);        context.registerBean("bean4", Bean4.class);        // 设置解析 @Value 注解的解析器        context.getDefaultListableBeanFactory().setAutowireCandidateResolver(new ContextAnnotationAutowireCandidateResolver());        // 添加解析 @Autowired 和 @Value 注解的后处理器        context.registerBean(AutowiredAnnotationBeanPostProcessor.class);        // 添加解析 @Resource、@PostConstruct、@PreDestroy 注解的后处理器        context.registerBean(CommonAnnotationBeanPostProcessor.class);        // 添加解析 @ConfigurationProperties注解的后处理器        // ConfigurationPropertiesBindingPostProcessor后处理器不能像上面几种后处理器那样用context直接注册上去        // context.registerBean(ConfigurationPropertiesBindingPostProcessor.class);        // 需要反着来注册一下        ConfigurationPropertiesBindingPostProcessor.register(context.getDefaultListableBeanFactory());        // ⬇️初始化容器        context.refresh();        System.out.println(context.getBean(Bean4.class));        // ⬇️销毁容器        context.close();    }}

运行结果如下:

image-20220327233426083

经过测试和运行结果的比对:

p18 017-第四讲-@Autowired bean后处理器执行分析

p19 018-第四讲-@Autowired bean后处理器执行分析

本案例测试代码紧接着上面,这里对Bean1中加了@Autowired注解的属性注入Bean2、方法注入Bean3以及方法注入环境变量JAVA_HOME的过程进行分析。

@Autowired注解解析用到的后处理器是AutowiredAnnotationBeanPostProcessor

全部测试代码如下:

@Slf4jpublic class TestBeanPostProcessors {    @Test    public void testAutowiredAnnotationBeanPostProcessor() throws Throwable {        DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory();        // 这里为了省事就不使用 beanFactory.registerBeanDefinition()方法去添加类的描述信息了        // 直接使用 beanFactory.registerSingleton可以直接将Bean的单例对象注入进去,        // 后面调用beanFactory.getBean()方法的时候就不会去根据Bean的定义去创建Bean的实例了,        // 也不会有懒加载和依赖注入的初始化过程了。        beanFactory.registerSingleton("bean2", new Bean2());        beanFactory.registerSingleton("bean3", new Bean3());        // 设置@Autowired注解的解析器        beanFactory.setAutowireCandidateResolver(new ContextAnnotationAutowireCandidateResolver());        // 设置解析 @Value 注解中的 ${} 表达式的解析器        beanFactory.addEmbeddedValueResolver(new StandardEnvironment()::resolvePlaceholders);        // 1. 查找哪些属性、方法加了 @Autowired,这称之为InjectionMetadata        // 创建后处理器        AutowiredAnnotationBeanPostProcessor processor = new AutowiredAnnotationBeanPostProcessor();        // 后处理器在解析@Autowired和@Value的时候需要用到其他Bean,        // 而BeanFactory提供了需要的Bean,所以需要把BeanFactory传给这个后处理器        processor.setBeanFactory(beanFactory);        // 创建Bean1        Bean1 bean1 = new Bean1();        System.out.println(bean1);        // 解析@Autowired和@Value注解,执行依赖注入        // PropertyValues pvs: 给注解的属性注入给定的值,这里不需要手动给定,传null即可        // processor.postProcessProperties(null, bean1, "bean1");        // postProcessProperties()方法底层原理探究        // 通过查看源码得知 postProcessProperties()方法中调用了一个私有的方法findAutowiringMetadata(beanName, bean.getClass(), pvs); 会返回一个InjectionMetadata的对象,然后会调用InjectionMetadata.inject(bean1, "bean1", null)进行依赖注入        // 通过反射调用一下                        // 3. 如何去Bean工厂里面按类型查找值        // 由于InjectionMetadata.inject(bean1, "bean1", null)的源码调用链过长,摘出主要调用过程进行演示        // 3.1 @Autowired加在成员变量上,InjectionMetatadata给Bean1注入Bean3的过程        // 通过InjectionMetadata把Bean1加了@Autowired注解的属性的BeanName先拿到,这里假设拿到的BeanName就是 bean3        // 通过BeanName反射获取到这个属性,        Field bean3Field = Bean1.class.getDeclaredField("bean3");        // 设置私有属性可以被访问        bean3Field.setAccessible(true);        // 将这个属性封装成一个DependencyDescriptor对象        DependencyDescriptor dd1 = new DependencyDescriptor(bean3Field, false);        // 再执行beanFactory的doResolveDependency        Bean3 bean3Value = (Bean3) beanFactory.doResolveDependency(dd1, null, null, null);        System.out.println(bean3Value);        // 给Bean1的成员bean3赋值        bean3Field.set(bean1, bean3Value);        System.out.println(bean1);        // 3.2 @Autowired加在方法上,InjectionMetatadata给Bean1注入Bean2的过程        Method setBean2 = Bean1.class.getDeclaredMethod("setBean2", Bean2.class);        DependencyDescriptor dd2 = new DependencyDescriptor(new MethodParameter(setBean2, 0), true);        Bean2 bean2Value = (Bean2) beanFactory.doResolveDependency(dd2, "bean2", null, null);        System.out.println(bean2Value);        // 给Bean1的setBean2()方法的参数赋值        setBean2.invoke(bean1, bean2Value);        System.out.println(bean1);        // 3.3 @Autowired加在方法上,方法参数为String类型,加了@Value,        // InjectionMetadata给Bean1注入环境变量JAVA_HOME属性的值        Method setJava_home = Bean1.class.getDeclaredMethod("setJava_home", String.class);        DependencyDescriptor dd3 = new DependencyDescriptor(new MethodParameter(setJava_home, 0), true);        String java_home = (String) beanFactory.doResolveDependency(dd3, null, null, null);        System.out.println(java_home);        setJava_home.invoke(bean1, java_home);        System.out.println(bean1);    }}

运行结果如下:

image-20220329115607946

第五讲 常见Bean工厂后处理器以及模拟实现组件扫描

spring_05_beanfactorypostprocessor

p20 019-第五讲-常见工厂后处理器

定义Bean1Bean2Mapper1Mapper2Config 5个类,其中Bean2上面加上@Component@ComponentScan注解,Config上加@Component注解,Config中通过@Bean注解定义Bean1Mapper1Mapper2上加@Mapper注解,类的定义参考下图,由于涉及的类比较多,具体代码可以去我的代码仓库获取。

image-20220329142407664

这里用GenericApplicationContext 来探究一下@Component@ComponentScan@Bean@MapperScan这些注解分别是由哪个后处理器来解析的。

测试代码如下:

@Slf4jpublic class TestBeanFactoryPostProcessors {    @Test    public void testBeanPostProcessors() throws IOException {        // ⬇️GenericApplicationContext 是一个【干净】的容器,默认不会添加任何后处理器,方便做测试        GenericApplicationContext context = new GenericApplicationContext();        context.registerBean("config", Config.class);        // 添加Bean工厂后处理器ConfigurationClassPostProcessor        // 解析@ComponentScan、@Bean、@Import、@ImportResource注解        context.registerBean(ConfigurationClassPostProcessor.class);        // 添加Bean工厂后处理器MapperScannerConfigurer,解析@MapperScan注解        context.registerBean(MapperScannerConfigurer.class, beanDefinition -> {            // 指定扫描的包名            beanDefinition.getPropertyValues().add("basePackage", "top.jacktgq.mapper");        });        // ⬇️初始化容器        context.refresh();        for (String name : context.getBeanDefinitionNames()) {            System.out.println(name);        }        // ⬇️销毁容器        context.close();    }}

经过测试和运行结果的比对:

p21 020-第五讲-工厂后处理器模拟实现-组件扫描

p22 021-第五讲-工厂后处理器模拟实现-组件扫描

自定义组件扫描Bean工厂后处理器CandyComponentScanPostProcessor来解析@Component注解,代码如下:

public class CandyAtComponentScanPostProcessor implements BeanDefinitionRegistryPostProcessor {    @Override    public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry beanFactory) throws BeansException {        try {            ComponentScan componentScan = AnnotationUtils.findAnnotation(Config.class, ComponentScan.class);            if (componentScan != null) {                for (String basePage : componentScan.basePackages()) {                    System.out.println(basePage);                    // top.jacktgq.component -> classpath*:com/jacktgq/component*.class                    String path = "classpath*:" + basePage.replace('.', '/') + "*.class";                    // System.out.println(path);                    CachingMetadataReaderFactory factory = new CachingMetadataReaderFactory();                    AnnotationBeanNameGenerator generator = new AnnotationBeanNameGenerator();                    for (Resource resource : new PathMatchingResourcePatternResolver().getResources(path)) {                        // System.out.println(resource);                        // 查看对应的类上是否有@Component注解                        // System.out.println("分隔符>>>>>>>>>>>>>>>>>");                        MetadataReader reader = factory.getMetadataReader(resource);                        String className = reader.getClassMetadata().getClassName();                        // System.out.println("类名:" + className);                        String name = Component.class.getName();                        // System.out.println("是否加了 @Component注解:" + reader.getAnnotationMetadata().hasAnnotation(name));                        AnnotationMetadata annotationMetadata = reader.getAnnotationMetadata();                        // System.out.println("是否加了@Component的派生注解:" + annotationMetadata.hasMetaAnnotation(name));                        // 如果直接或者间接加了@Component注解                        if (annotationMetadata.hasAnnotation(Component.class.getName()) || annotationMetadata.hasMetaAnnotation(name)) {// 创建Bean的定义AbstractBeanDefinition beanDefinition = BeanDefinitionBuilder.genericBeanDefinition(className).getBeanDefinition();String beanName = generator.generateBeanName(beanDefinition, beanFactory);// 将Bean定义加入工厂beanFactory.registerBeanDefinition(beanName, beanDefinition);                        }                    }                }            }        } catch (IOException e) {            e.printStackTrace();        }    }    @Override    // context.refresh()中会回调该方法    public void postProcessBeanFactory(ConfigurableListableBeanFactory configurableListableBeanFactory) throws BeansException {            }}

测试代码:

@Slf4jpublic class TestBeanFactoryPostProcessors {    @Test    // 模拟实现组件扫描    public void testMockComponentScan() throws Exception {        // ⬇️GenericApplicationContext 是一个【干净】的容器,默认不会添加任何后处理器,方便做测试        GenericApplicationContext context = new GenericApplicationContext();        context.registerBean("config", Config.class);        // 把自定义组件扫描Bean工厂后处理器加进来        context.registerBean(CandyComponentScanPostProcessor.class);        // ⬇️初始化容器        context.refresh();        for (String name : context.getBeanDefinitionNames()) {            System.out.println(name);        }        // ⬇️销毁容器        context.close();    }}

运行结果:

image-20220329192201153

p23 022-第五讲-工厂后处理器模拟实现-@Bean

自定义Bean工厂后处理器CandyAtBeanPostProcessor来解析@Bean注解,代码如下:

public class CandyAtBeanPostProcessor implements BeanDefinitionRegistryPostProcessor {    @Override    public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry beanFactory) throws BeansException {        try {            CachingMetadataReaderFactory factory = new CachingMetadataReaderFactory();            MetadataReader reader = factory.getMetadataReader(new ClassPathResource("top/jacktgq/Config.class"));            Set<MethodMetadata> annotatedMethods = reader.getAnnotationMetadata().getAnnotatedMethods(Bean.class.getName());            for (MethodMetadata annotatedMethod : annotatedMethods) {                System.out.println(annotatedMethod);                String initMethod = annotatedMethod.getAnnotationAttributes(Bean.class.getName()).get("initMethod").toString();                // 这里不需要指定类名了,因为最终的BeanDefinition是Config类中加了@Bean属性的方法的返回值的类型的定义。                BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition();                builder.setFactoryMethodOnBean(annotatedMethod.getMethodName(), "config");                builder.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_CONSTRUCTOR);                if (initMethod.length() > 0) {                    builder.setInitMethodName(initMethod);                }                AbstractBeanDefinition beanDefinition = builder.getBeanDefinition();                beanFactory.registerBeanDefinition(annotatedMethod.getMethodName(), beanDefinition);            }        } catch (IOException e) {            e.printStackTrace();        }    }    @Override    public void postProcessBeanFactory(ConfigurableListableBeanFactory configurableListableBeanFactory) throws BeansException {    }}

注:自定义解析 @ComponentScan 和 后面的 @Bean 注解的Bean工厂后处理器,实现BeanDefinitionRegistryPostProcessor接口而不是BeanFactoryPostProcessor接口,这么做的原因是:

测试代码:

public class TestBeanFactoryPostProcessors {    @Test    // 模拟实现@Bean注解的解析    public void testMockAtBeanAnnotation() throws Exception {        // ⬇️GenericApplicationContext 是一个【干净】的容器,默认不会添加任何后处理器,方便做测试        GenericApplicationContext context = new GenericApplicationContext();        context.registerBean("config", Config.class);        context.registerBean(CandyAtBeanPostProcessor.class);        // ⬇️初始化容器        context.refresh();        for (String name : context.getBeanDefinitionNames()) {            System.out.println(name);        }        // ⬇️销毁容器        context.close();    }}

运行结果:

image-20220329224604315

p25 024-第五讲-工厂后处理器模拟实现-Mapper

自定义Bean工厂后处理器CandyAtMapperPostProcessor来解析@Mapper注解,代码如下:

public class CandyAtMapperPostProcessor implements BeanDefinitionRegistryPostProcessor {    @Override    public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry beanFactory) throws BeansException {        try {            PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();            Resource[] resources = resolver.getResources("classpath:top/jacktgq/mapper*.class");            // Bean的名字生成器            AnnotationBeanNameGenerator generator = new AnnotationBeanNameGenerator();            CachingMetadataReaderFactory factory = new CachingMetadataReaderFactory();            for (Resource resource : resources) {                MetadataReader reader = factory.getMetadataReader(resource);                ClassMetadata classMetadata = reader.getClassMetadata();                if (classMetadata.isInterface()) {                    AbstractBeanDefinition beanDefinition = BeanDefinitionBuilder.genericBeanDefinition(MapperFactoryBean.class).addConstructorArgValue(classMetadata.getClassName()).setAutowireMode(AbstractBeanDefinition.AUTOWIRE_BY_TYPE).getBeanDefinition();                    // 这里不能使用名字生成器和MapperFactoryBean的BeanDefinition作为参数直接生成名字,                    // 这样会导致多个相同的类型的对象因为名字一样产生覆盖的问题                    // 解决办法 这里参考Spring源码的做法                    // 用@Mapper注解修饰的接口的BeanDefinition作为参数生成名字                    AbstractBeanDefinition bd = BeanDefinitionBuilder.genericBeanDefinition(classMetadata.getClassName()).getBeanDefinition();                    String beanName = generator.generateBeanName(bd, beanFactory);                    beanFactory.registerBeanDefinition(beanName, beanDefinition);                }            }        } catch (IOException e) {            e.printStackTrace();        }    }    @Override    public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {    }}

测试代码:

@Slf4jpublic class TestBeanFactoryPostProcessors {    @Test    // 模拟实现@Mapper注解的解析    public void testMockAtMapperAnnotation() throws Exception {        // ⬇️GenericApplicationContext 是一个【干净】的容器,默认不会添加任何后处理器,方便做测试        GenericApplicationContext context = new GenericApplicationContext();        context.registerBean("config", Config.class);        // 先解析@Bean注解,把SqlSessionFactory加到Bean工厂里面        context.registerBean(CandyAtBeanPostProcessor.class);        // 解析Mapper接口        context.registerBean(CandyAtMapperPostProcessor.class);        // ⬇️初始化容器        context.refresh();        for (String name : context.getBeanDefinitionNames()) {            System.out.println(name);        }        // ⬇️销毁容器        context.close();    }}

运行结果:

image-20220330012845575

第六讲 Aware和InitializingBean接口以及@Autowired注解失效分析

spring_06_aware_initializingbean

p26 025-第六讲-Aware与InitializingBean接口

Aware 接口用于注入一些与容器相关信息,例如:

​ a. BeanNameAware 注入 Bean 的名字

​ b. BeanFactoryAware 注入 BeanFactory 容器

​ c. ApplicationContextAware 注入 ApplicationContext 容器

​ d. EmbeddedValueResolverAware 注入 解析器,解析${}

定义一个MyBean类,实现BeanNameAwareApplicationContextAwareInitializingBean接口并实现其方法,再定义两个方法,其中一个加@Autowired注解,注入ApplicationContext容器,另一个加@PostConstruct注解,具体代码如下:

public class MyBean implements BeanNameAware, ApplicationContextAware, InitializingBean {    @Override    public void setBeanName(String name) {        log.debug("当前bean:" + this + ",实现 BeanNameAware 调用的方法,名字叫:" + name);    }    @Override    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {        log.debug("当前bean:" + this + ",实现 ApplicationContextAware 调用的方法,容器叫:" + applicationContext);    }    @Override    public void afterPropertiesSet() throws Exception {        log.debug("当前bean:" + this + ",实现 InitializingBean 调用的方法,初始化");    }    @Autowired    public void aaa(ApplicationContext applicationContext) {        log.debug("当前bean:" + this +",使用 @Autowired 容器是:" + applicationContext);    }    @PostConstruct    public void init() {        log.debug("当前bean:" + this + ",使用 @PostConstruct 初始化");    }}

测试代码:

public class TestAwareAndInitializingBean {    @Test    public void testAware1() throws Exception {        GenericApplicationContext context = new GenericApplicationContext();        context.registerBean("myBean", MyBean.class);        context.refresh();        context.close();    }}

运行结果:

image-20220330173209568

加了@Autowired@PostConstruct注解的方法并没有被执行,而AwareInitializingBean接口方法都被执行了。

修改测试代码,把解析@Autowired@PostConstruct注解的Bean后处理加进来,然后再运行一下

public class TestAwareAndInitializingBean {    @Test    public void testAware1() throws Exception {        GenericApplicationContext context = new GenericApplicationContext();        context.registerBean("myBean", MyBean.class);        // 解析 @Autowired 注解的Bean后处理器        context.registerBean(AutowiredAnnotationBeanPostProcessor.class);        // 解析 @PostConstruct 注解的Bean后处理器        context.registerBean(CommonAnnotationBeanPostProcessor.class);        context.refresh();        context.close();    }}

运行结果:

image-20220330173807848

可以看到这下都执行了

有人可能会问:bcd的功能用 @Autowired注解就能实现啊,为啥还要用 Aware 接口呢?
InititalizingBean 接口可以用 @PostConstruct注解实现,为啥还要用InititalizingBean呢?
简单地说:

总结:

第七讲 Bean的初始化与销毁

spring_07_init_destroy

p28 027-第七讲-初始化与销毁

定义Bean1类,实现InitializingBean接口和对应的接口方法afterPropertiesSet(),再定义init1()方法,在方法上加@PostConstruct注解,最后定义init3()

@Slf4jpublic class Bean1 implements InitializingBean {    @PostConstruct    public void init1() {        log.debug("初始化1,@PostConstruct");    }    @Override    public void afterPropertiesSet() throws Exception {        log.debug("初始化2,InitializingBean接口");    }    public void init3() {        log.debug("初始化3,@Bean的initMethod");    }}

定义Bean2类,实现DisposableBean接口和对应的接口方法destroy(),再定义destroy1()方法,在方法上加@PreDestroy注解,最后定义init3()

@Slf4jpublic class Bean2 implements DisposableBean {    @PreDestroy    public void destroy1() {        log.debug("销毁1,@PreDestory");    }    @Override    public void destroy() throws Exception {        log.debug("销毁2,DisposableBean接口");    }    public void destroy3() {        log.debug("销毁3,@Bean的destroyMethod");    }}

定义Config类,类上加@Configuration注解,类中通过@Bean注解把Bean1Bean2加到Bean工厂中,分别在@Bean注解中指定initMethod = "init3",destroyMethod = "destroy"

@Configurationpublic class Config {    @Bean(initMethod = "init3")    public Bean1 bean1() {        return new Bean1();    }    @Bean(destroyMethod = "destroy3")    public Bean2 bean2() {        return new Bean2();    }}

编写测试代码,观察三个初始化方法和三个销毁方法的执行顺序

@Slf4jpublic class TestInitAndDestroy {    @Test    public void testInitAndDestroy() throws Exception {        // ⬇️GenericApplicationContext 是一个【干净】的容器,这里只是为了看初始化步骤,就不用springboot启动类进行演示了        GenericApplicationContext context = new GenericApplicationContext();        context.registerBean("config", Config.class);        // 解析@PostConstruct注解的bean后处理器        context.registerBean(CommonAnnotationBeanPostProcessor.class);        // 解析@Configuration、@Component、@Bean注解的bean工厂后处理器        context.registerBean(ConfigurationClassPostProcessor.class);        context.refresh();        context.close();    }}

运行结果:

image-20220401013108042

可以看到,spring提供了多种初始化和销毁手段

第八讲 Scope类型、注意事项、销毁和失效分析

spring_08_scope

p29 028-第八讲-Scope

springscope类型:

测试scope类型中的requestsessionapplication

定义**BeanForRequest类,加上@Component@Scope注解,指定Scope类型为request**,在类型中定义destroy()方法,方法上加@PreDestory注解,代码如下:

@Slf4j@Scope("request")@Componentpublic class BeanForRequest {    @PreDestroy    public void destory() {        log.debug("destroy");    }}

定义**BeanForSession类,加上@Component@Scope注解,指定Scope类型为session**,在类型中定义destroy()方法,方法上加@PreDestory注解,代码如下:

@Slf4j@Scope("request")@Componentpublic class BeanForRequest {    @PreDestroy    public void destory() {        log.debug("destroy");    }}

定义**BeanForApplication类,加上@Component@Scope注解,指定Scope类型为application**,在类型中定义destroy()方法,方法上加@PreDestory注解,代码如下:

@Slf4j@Scope("request")@Componentpublic class BeanForRequest {    @PreDestroy    public void destory() {        log.debug("destroy");    }}

编写一个MyController类,加上@RestController注解,在该类中通过@Autowired注解注入BeanForRequestBeanForSessionBeanForApplication的实例,需要注意,这里还需要加@Lazy注解(至于原因后面会解释),否则会导致@Scope域失效,再定义一个方法tes(),加上@GetMapping注解,用于响应一个http请求,在test()方法中,打印beanForRequestbeanForSessionbeanForApplication,代码如下:

@RestControllerpublic class MyController {    @Lazy    @Autowired    private BeanForRequest beanForRequest;    @Lazy    @Autowired    private BeanForSession beanForSession;    @Lazy    @Autowired    private BeanForApplication beanForApplication;    @GetMapping(value = "/test", produces = "text/html")    public String test(HttpServletRequest request, HttpSession session) {        // ServletContext sc = request.getServletContext();        String result = "
    " + "
  • request scope: " + beanForRequest + "
  • "
    + "
  • session scope: " + beanForSession + "
  • "
    + "
  • application scope: " + beanForApplication + "
  • "
    + "
"
; return result; }}

springboot启动类

@Slf4j@SpringBootApplicationpublic class ScopeApplicationContext {    public static void main(String[] args) throws InterruptedException {        testRequest_Session_Application_Scope();    }    // 演示 request, session, application作用域    private static void testRequest_Session_Application_Scope() throws InterruptedException {        ConfigurableApplicationContext context = SpringApplication.run(ScopeApplicationContext.class);    }}

启动后,用谷歌浏览器访问 http://localhost:8080/test

浏览器运行结果如下:

image-20220401222141258

再刷新一下当前页,查看运行结果:

image-20220401222154488

控制台运行结果如下:

image-20220401214139467

可以看到两次刷新只有BeanForRequest对象发生了改变,这是由于scoperequest类型的对象,会在请求结束后销毁,再来一次请求就会重新创建,请求结束后又会销毁。

接下来我们换个Edge浏览器访问 http://localhost:8080/test,对比两个浏览器的显示结果:

image-20220401222256653

可以看到这回除了BeanForRequest对象不同,BeanForSession对象也不同了,这是因为开一个新的浏览器会创建一个新的会话,所以BeanForSession对象也不同了。

继续进行测试,在application.properties配置一个属性server.servlet.session.timeout=10s,这个属性的默认值为30分钟,这样10s没有操作浏览器的话就会销毁对应session,不过经过测试这个这个属性最少为1分钟,低于1分钟一律按照1分钟算。具体原理看这篇博客:https://www.jianshu.com/p/9d91cca74082,里面进行了源码级别的分析。

设置好之后,重启项目, 然后去浏览器访问,1分钟后控制台会打印session被销毁,如下图所示:

image-20220402144856449

那什么时候scopeapplication的对象BeanForApplication会销毁呢?按理说应该是在SpringBoot程序结束,也即内置的Tomcat服务器停止的时候调用,但是经过测试:无论是在控制台停止SpringBoot项目,还是调用ApplicationContextclose()方法,都没有调用BeanForApplication的销毁方法,有知道什么方法可以让它调用的,请评论区告知,谢谢!!!

p30 029-第八讲-Scope失效解决1,2

p31 030-第八讲-Scope失效解决3,4

定义1个单例类SingletonBean,指定它的Scopesingleton,定义5个多例类,PrototypeBeanPrototypeBean1PrototypeBean2PrototypeBean3PrototypeBean4,将这5个多例类的对象注入到SingletonBean5个属性中,其中PrototypeBean用来演示多例对象注入到单例对象中Scope失效的情况,其他四个类的对象用来演示四种解决Scope失效的方法。相关类的定义如下:

SingletonBean

@Scope("singleton")@Componentpublic class SingletonBean {    @Autowired    private PrototypeBean prototypeBean;    @Lazy    @Autowired    private PrototypeBean1 prototypeBean1;    @Autowired    private PrototypeBean2 prototypeBean2;    @Autowired    private ObjectFactory<PrototypeBean3> factory;    @Autowired    private ApplicationContext context;    public PrototypeBean getPrototypeBean() {        return prototypeBean;    }    public PrototypeBean1 getPrototypeBean1() {        return prototypeBean1;    }    public PrototypeBean2 getPrototypeBean2() {        return prototypeBean2;    }    public PrototypeBean3 getPrototypeBean3() {        return factory.getObject();    }    public PrototypeBean4 getPrototypeBean4() {        return context.getBean(PrototypeBean4.class);    }}

PrototypeBean

@Scope("prototype")@Componentpublic class PrototypeBean {}

PrototypeBean1

@Scope("prototype")@Componentpublic class PrototypeBean1 {}

PrototypeBean2

@Scope(value = "prototype", proxyMode = ScopedProxyMode.TARGET_CLASS)@Componentpublic class PrototypeBean2 {}

PrototypeBean3

@Scope("prototype")@Componentpublic class PrototypeBean3 {}

PrototypeBean4

@Scope("prototype")@Componentpublic class PrototypeBean4 {}

测试代码:

@Slf4j@SpringBootApplicationpublic class ScopeApplicationContext {    public static void main(String[] args) throws Exception {        testSingletonPrototypeInvalidAndSolve();    }    // 演示单例中注入多例失效的情况,以及解决失效问题的方法    private static void testSingletonPrototypeInvalidAndSolve() {        ConfigurableApplicationContext context = SpringApplication.run(ScopeApplicationContext.class);        SingletonBean singletonBean = context.getBean(SingletonBean.class);        // 单例中注入多例失效的情况        log.debug("{}", singletonBean.getPrototypeBean().getClass());        log.debug("{}", singletonBean.getPrototypeBean());        log.debug("{}", singletonBean.getPrototypeBean());        log.debug("{}", singletonBean.getPrototypeBean());        System.out.println("------------------------------------");        // 解决方法1:在SingletonBean的PrototypeBean1属性上加@Lazy注解        log.debug("{}", singletonBean.getPrototypeBean1().getClass());        log.debug("{}", singletonBean.getPrototypeBean1());        log.debug("{}", singletonBean.getPrototypeBean1());        log.debug("{}", singletonBean.getPrototypeBean1());        System.out.println("------------------------------------");        // 解决方法2:在PrototypeBean2的类上的@Scope注解多配置一个属性,如,@Scope(value = "prototype", proxyMode = ScopedProxyMode.TARGET_CLASS)        log.debug("{}", singletonBean.getPrototypeBean2().getClass());        log.debug("{}", singletonBean.getPrototypeBean2());        log.debug("{}", singletonBean.getPrototypeBean2());        log.debug("{}", singletonBean.getPrototypeBean2());        System.out.println("------------------------------------");        // 解决方法3:使用ObjectFactory工厂类,在每次调用getProtypeBean3()方法中返回factory.getObject()        log.debug("{}", singletonBean.getPrototypeBean3().getClass());        log.debug("{}", singletonBean.getPrototypeBean3());        log.debug("{}", singletonBean.getPrototypeBean3());        log.debug("{}", singletonBean.getPrototypeBean3());        System.out.println("------------------------------------");        // 解决方法4:在SingletonBean中注入一个ApplicationContext,使用context.getBean(PrototypeBean4.class)获取对应的多例        log.debug("{}", singletonBean.getPrototypeBean4().getClass());        log.debug("{}", singletonBean.getPrototypeBean4());        log.debug("{}", singletonBean.getPrototypeBean4());        log.debug("{}", singletonBean.getPrototypeBean4());        System.out.println("------------------------------------");    }}

运行结果如下:

image-20220406141812580

第九讲 aop之ajc增强

aopspring框架中非常重要的功能,其主要实现通常情况下是动态代理,但是这个说法并不全面,还有另外两种实现:

spring_09_aop_ajc

p32 031-第九讲-aop之ajc增强

先看aop的第一种实现ajc编译器代码增强,这是一种编译时的代码增强。

新建一个普通的maven项目

总结:

第十讲 aop之agent增强

spring_10_aop_agent

p33 032-第十讲-aop之agent增强

现在来看aop的另外一种实现agent增强,这是一种类加载时的代码增强。

第十一讲 aop之proxy增强-jdk和cglib

spring_11_aop_proxy_jdk_cglib

p34 033-第十一讲-aop之proxy增强-jdk

测试代码

public class AopJdkProxyTest {    @Test    public void testJdkProxy() {        // jdk的动态代理,只能针对接口代理        // 目标对象        Target target = new Target();        // 用来加载在运行期间动态生成的字节码        ClassLoader loader = AopJdkProxyTest.class.getClassLoader();        Foo fooProxy = (Foo) Proxy.newProxyInstance(loader, new Class[]{Foo.class}, (proxy, method, args) -> {            System.out.println("before...");            Object result = method.invoke(target, args);            System.out.println("after...");            return result;  // 让代理也返回目标方法执行的结果        });        fooProxy.foo();    }}interface Foo {    void foo();}@Slf4jfinal class Target implements Foo {    public void foo() {        log.debug("target foo");    }}

运行结果

image-20220411083443041

jdk动态代理总结:

  1. 代理对象和目标对象是兄弟关系,都实现了Foo接口,代理对象类型不能强转成目标对象类型;
  2. 目标类定义的时候可以加final修饰。

p35 034-第十一讲-aop之proxy增强-cglib

public class AopCglibProxyTest {    @Test    public void testCglibProxy1() {        // 目标对象        Target target = new Target();        Foo fooProxy = (Foo) Enhancer.create(Target.class, (MethodInterceptor) (obj, method, args, proxy) -> {            System.out.println("before...");            Object result = method.invoke(target, args); // 用方法反射调用目标            System.out.println("after...");            return result;        });        System.out.println(fooProxy.getClass());        fooProxy.foo();    }    @Test    public void testCglibProxy2() {        // 目标对象        Target target = new Target();        Foo fooProxy = (Foo) Enhancer.create(Target.class, (MethodInterceptor) (obj, method, args, proxy) -> {            System.out.println("before...");            // proxy 它可以避免反射调用            Object result = proxy.invoke(target, args); // 需要传目标类            System.out.println("after...");            return result;        });        System.out.println(fooProxy.getClass());        fooProxy.foo();    }    @Test    public void testCglibProxy3() {        // 目标对象        Foo fooProxy = (Foo) Enhancer.create(Target.class, (MethodInterceptor) (obj, method, args, proxy) -> {            System.out.println("before...");            // proxy 它可以避免反射调用            Object result = proxy.invokeSuper(obj, args); // 不需要目标类,需要代理自己            System.out.println("after...");            return result;        });        System.out.println(fooProxy.getClass());        fooProxy.foo();    }}interface Foo {    void foo();}@Slf4jclass Target implements Foo {    public void foo() {        log.debug("target foo");    }}

运行结果

image-20220411085629883

cglib动态代理总结:

  1. MethodInterceptorintercept()方法的第2个参数是method,可以通过反射对目标方法进行调用

    Object result = method.invoke(target, args); // 用方法反射调用目标
  2. 第4个参数proxy,可以不用反射就能对目标方法进行调用;

    Object result = proxy.invoke(target, args); // 需要传目标类 (spring用的是这种)// 或者Object result = proxy.invokeSuper(obj, args); // 不需要目标类,需要代理自己
  3. 代理类不需要实现接口;

  4. 代理对象和目标对象是父子关系,代理类继承于目标类;

  5. 目标类定义的时候不能加final修饰,否则代理类就无法继承目标类了,会报java.lang.IllegalArgumentException: Cannot subclass final class top.jacktgq.proxy.cglib.Target异常;

  6. 目标类方法定义的时候不能加final修饰,否则代理类继承目标类以后就不能重写目标类的方法了。

第十二讲 jdk代理原理

spring_12_aop_proxy_jdk_cglib_principle

p36 035-第十二讲-jdk代理原理

p37 036-第十二讲-jdk代理原理

p38 037-第十二讲-jdk代理源码

为了更好地探究jdk动态代理原理,先用代码显式地模拟一下这个过程。

先定义一个Foo接口,里面有一个foo()方法,再定义一个Target类来实现这个接口,代码如下所示:

public interface Foo {    void foo();}@Slf4jpublic final class Target implements Foo {    public void foo() {        log.debug("target foo");    }}public class $Proxy0 implements Foo {    @Override    public void foo() {        // 1. 功能增强        System.out.println("before...");        // 2. 调用目标        new Target().foo();    }}public class Main {    public static void main(String[] args) {        Foo proxy = new $Proxy0();        proxy.foo();    }}

接下来对Target类中的foo()方法进行增强

  1. 首先想到的是,再定义一个类也同样地实现一下Foo接口,然后在foo()方法中编写增强代码,接着再new一个Target对象,调用它的foo()方法,代码如下所示:

    public class $Proxy0 implements Foo {    @Override    public void foo() {        // 1. 功能增强        System.out.println("before...");        // 2. 调用目标        new Target().foo();    }}// 测试运行public class Main {    public static void main(String[] args) {        Foo proxy = new $Proxy0();        proxy.foo();    }}
  2. 上面的代码把功能增强的代码和调用目标的代码都固定在了代理类的内部,不太灵活。因此可以通过定义一个InvocationHandler接口的方式来将这部分代码解耦出来,代码如下:

    public interface InvocationHandler {    void invoke();}public interface Foo {    void foo();}@Slf4jpublic final class Target implements Foo {    public void foo() {        System.out.println("target foo");    }}public class $Proxy0 implements Foo {    private InvocationHandler h;    public $Proxy0(InvocationHandler h) {        this.h = h;    }    @Override    public void foo() {        h.invoke();    }}public class Main {    public static void main(String[] args) {        Foo proxy = new $Proxy0(new InvocationHandler() {            @Override            public void invoke() {                // 1. 功能增强                System.out.println("before...");                // 2. 调用目标                new Target().foo();            }        });        proxy.foo();    }}
  3. 第2个版本的代码虽然将功能增强的代码和调用目标的代码通过接口的方式独立出来了,但还是有问题,如果此时接口中新增了一个方法bar()Target类和$Proxy0类中都要实现bar()方法,那么调用proxyfoo()bar()方法都将间接调用目标对象的foo()方法,因为在InvocationHandlerinvoke()方法中调用的是target.foo()方法,代码如下:

    public interface InvocationHandler {    void invoke();}public interface Foo {    void foo();    void bar();}@Slf4jpublic final class Target implements Foo {    public void foo() {        System.out.println("target foo");    }    @Override    public void bar() {        log.debug("target bar");    }}public class $Proxy0 implements Foo {    private InvocationHandler h;    public $Proxy0(InvocationHandler h) {        this.h = h;    }    @Override    public void foo() {        h.invoke();    }    @Override    public void bar() {        h.invoke();    }}public class Main {    public static void main(String[] args) {        Foo proxy = new $Proxy0(new InvocationHandler() {            @Override            public void invoke() {                // 1. 功能增强                System.out.println("before...");                // 2. 调用目标                new Target().foo();            }        });        proxy.foo();        proxy.bar();    }}

    改进方法是,代理类中调用方法的时候,通过反射把接口中对应的方法Method对象作为参数传给InvocationHandler,代码如下:

    public interface InvocationHandler {    void invoke(Method method, Object[] args) throws Throwable;}public interface Foo {    void foo();    void bar();}public final class Target implements Foo {    public void foo() {        System.out.println("target foo");    }    @Override    public void bar() {        System.out.println("target bar");    }}public class $Proxy0 implements Foo {    private InvocationHandler h;    public $Proxy0(InvocationHandler h) {        this.h = h;    }    @Override    public void foo() {        try {            Method foo = Foo.class.getDeclaredMethod("foo");            h.invoke(foo, new Object[0]);        } catch (Throwable e) {            e.printStackTrace();        }    }    @Override    public void bar() {        try {            Method bar = Foo.class.getDeclaredMethod("bar");            h.invoke(bar, new Object[0]);        } catch (Throwable e) {            e.printStackTrace();        }    }}public class Main {    public static void main(String[] args) {        Foo proxy = new $Proxy0(new InvocationHandler() {            @Override            public void invoke(Method method, Object[] args) throws InvocationTargetException, IllegalAccessException {                // 1. 功能增强                System.out.println("before...");                // 2. 调用目标                method.invoke(new Target(), args);            }        });        proxy.foo();        proxy.bar();    }}
  4. 第3个版本的代码其实已经离jdk动态代理生成的代码很相近了,为了更好地学习底层,更近一步,修改Foo接口的中bar()方法,使其具有int类型的返回值,因此InvocationHandlerinvoke()方法也得有返回值,同时将代理对象本身作为第一个参数,具体代码如下:

    public interface InvocationHandler {    Object invoke(Object proxy, Method method, Object[] args) throws Throwable;}public interface Foo {    void foo();    int bar();}public final class Target implements Foo {    public void foo() {        System.out.println("target foo");    }    @Override    public int bar() {        System.out.println("target bar");        return 1;    }}public class $Proxy0 implements Foo {    static Method foo;    static Method bar;    static {        try {            foo = Foo.class.getDeclaredMethod("foo");            bar = Foo.class.getDeclaredMethod("bar");        } catch (NoSuchMethodException e) {            throw new NoSuchMethodError(e.getMessage());        }    }    private InvocationHandler h;    public $Proxy0(InvocationHandler h) {        this.h = h;    }    @Override    public void foo() {        try {            h.invoke(this, foo, new Object[0]);        } catch (RuntimeException | Error e) {            throw e;        } catch (Throwable e) {            throw new UndeclaredThrowableException(e);        }    }    @Override    public int bar() {        try {            return (int) h.invoke(this, bar, new Object[0]);        } catch (RuntimeException | Error e) {            throw e;        } catch (Throwable e) {            throw new UndeclaredThrowableException(e);        }    }}public class Main {    public static void main(String[] args) {        Foo proxy = new $Proxy0(new InvocationHandler() {            @Override            public Object invoke(Object proxy, Method method, Object[] args) throws InvocationTargetException, IllegalAccessException {                // 1. 功能增强                System.out.println("before...");                // 2. 调用目标                return method.invoke(new Target(), args);            }        });        proxy.foo();        System.out.println("bar()方法返回值:" + proxy.bar());    }}
  5. 到这里跟jdk的动态代理只有些微差距了,jdk的动态代码会让代理类再继承一个Proxy类,里面定义了一个InvocationHandler接口的对象,代理类中会通过super(h)调用父类Proxy的构造,这里建议结合视频教程理解。

p39 038-第十二讲-jdk代理字节码生成

p40 039-第十二讲-jdk反射优化

p41 040-第十三讲-cglib代理原理

p42 041-第十三讲-cglib代理原理-MethodProxy

p43 042-第十四讲-MethodProxy原理

p44 043-第十四讲-MethodProxy原理

p45 044-第十五讲-Spring选择代理

p46 045-第十五讲-Spring选择代理

p47 046-第十五讲-Spring选择代理

p48 047-第十六讲-切点匹配

p49 048-第十六讲-切点匹配

p50 049-第十七讲-Advisor与@Aspect

p51 050-第十七讲-findEligibleAdvisors

p52 051-第十七讲-wrapIfNecessary

p53 052-第十七讲-代理创建时机

p54 053-第十七讲-吐槽@Order

p55 054-第十七讲-高级切面转低级切面

p56 055-第十八讲-统一转换为环绕通知

p57 056-第十八讲-统一转换为环绕通知

p58 057-第十八讲-适配器模式

p59 058-第十八讲-调用链执行

p60 059-第十八讲-模拟实现调用链

p61 060-第十八讲-模拟实现调用链-责任链模式

p62 061-第十九讲-动态通知调用

p63 062-第十九讲-动态通知调用

p64 063-第廿讲-DispatcherServlet初始化时机

p65 064-第廿讲-DispatcherServlet初始化时机

p66 065-第廿讲-DispatcherServlet初始化执行的操作

p67 066-第廿讲-RequestMappingHandlerMapping

p68 067-第廿讲-RequestMappingHandlerAdapter

p69 068-第廿讲-RequestMappingHandlerAdapter-参数和返回值解析器

p70 069-第廿讲-RequestMappingHandlerAdapter-自定义参数解析器

p71 070-第廿讲-RequestMappingHandlerAdapter-自定义返回值解析器

p72 071-第廿一讲-参数解析器-准备

p73 072-第廿一讲-参数解析器-准备

p74 073-第廿一讲-参数解析器-@RequestParam 0-4

p75 074-第廿一讲-参数解析器-组合模式

p76 075-第廿一讲-参数解析器 5-9

p77 076-第廿一讲-参数解析器 10-12

p78 077-第廿二讲-获取参数名

p79 078-第廿二讲-获取参数名

p80 079-第廿三讲-两套底层转换接口

p81 080-第廿三讲-一套高层转换接口

p82 081-第廿三讲-类型转换与数据绑定演示

p83 082-第廿三讲-web环境下数据绑定演示

p84 083-第廿三讲-绑定器工厂

p85 084-第廿三讲-绑定器工厂-@InitBinder扩展

p86 085-第廿三讲-绑定器工厂-ConversionService扩展

p87 086-第廿三讲-绑定器工厂-默认ConversionService

p88 087-第廿三讲-加餐-如何获取泛型参数

p89 088-第廿四讲-@ControllerAdvice-@InitBinder

p90 089-第廿四讲-@ControllerAdvice-@InitBinder

p91 090-第廿五讲-控制器方法执行流程

p92 091-第廿五讲-控制器方法执行流程

p93 092-第廿五讲-控制器方法执行流程-代码

p94 093-第廿六讲-@ControllerAdvice-@ModelAttribute

p95 094-第廿七讲-返回值处理器

p96 095-第廿七讲-返回值处理器-1

p97 096-第廿七讲-返回值处理器-2-4

p98 097-第廿七讲-返回值处理器-5-7

p99 098-第廿八讲-MessageConverter

p100 099-第廿八讲-MessageConverter

p101 100-第廿九讲-@ControllerAdvice-ResponseBodyAdvice

p102 101-第廿九讲-@ControllerAdvice-ResponseBodyAdvice

p103 102-第卅讲-异常处理

p104 103-第卅讲-异常处理

p105 104-第卅一讲-@ControllerAdvice-@ExceptionHandler

p106 105-第卅二讲-tomcat异常处理

p107 106-第卅二讲-tomcat异常处理-自定义错误地址

p108 107-第卅二讲-tomcat异常处理-BasicErrorController

p109 108-第卅二讲-tomcat异常处理-BasicErrorController

p110 109-第卅三讲-HandlerMapping与HandlerAdapter-1

p111 110-第卅三讲-HandlerMapping与HandlerAdapter-自定义

p112 111-第卅四讲-HandlerMapping与HandlerAdapter-2

p113 112-第卅五讲-HandlerMapping与HandlerAdapter-3

p114 113-第卅五讲-HandlerMapping与HandlerAdapter-3-优化

p115 114-第卅五讲-HandlerMapping与HandlerAdapter-3-优化

p116 115-第卅五讲-HandlerMapping与HandlerAdapter-4-欢迎页

p117 116-第卅五讲-HandlerMapping与HandlerAdapter-总结

p118 117-第卅六讲-MVC执行流程

p119 118-第卅六讲-MVC执行流程

p120 119-第卅七讲-构建boot骨架项目

p121 120-第卅八讲-构建boot war项目

p122 121-第卅八讲-构建boot war项目-用外置tomcat测试

p123 122-第卅八讲-构建boot war项目-用内嵌tomcat测试

p124 123-第卅九讲-boot执行流程-构造

p125 124-第卅九讲-boot执行流程-构造-1

p126 125-第卅九讲-boot执行流程-构造-2

p127 126-第卅九讲-boot执行流程-构造-3

p128 127-第卅九讲-boot执行流程-构造-4-5

p129 128-第卅九讲-boot执行流程-run-1

p130 129-第卅九讲-boot执行流程-run-1

p131 130-第卅九讲-boot执行流程-run-8-11

p132 131-第卅九讲-boot执行流程-run-2,12

p133 132-第卅九讲-boot执行流程-run-3

p134 133-第卅九讲-boot执行流程-run-4

p135 134-第卅九讲-boot执行流程-run-5

p136 135-第卅九讲-boot执行流程-run-5

p137 136-第卅九讲-boot执行流程-run-6

p138 137-第卅九讲-boot执行流程-run-7

p139 138-第卅九讲-boot执行流程-小结

p140 139-第卌讲-Tomcat重要组件

p141 140-第卌讲-内嵌Tomcat

p142 141-第卌讲-内嵌Tomcat与Spring整合

p143 142-第卌一讲-自动配置类原理

p144 143-第卌一讲-自动配置类原理

p145 144-第卌一讲-AopAutoConfiguration

p146 145-第卌一讲-AopAutoConfiguration

p147 146-第卌一讲-自动配置类2-4概述

p148 147-第卌一讲-自动配置类2-DataSource

p149 148-第卌一讲-自动配置类3-MyBatis

p150 149-第卌一讲-自动配置类3-mapper扫描

p151 150-第卌一讲-自动配置类4-事务

p152 151-第卌一讲-自动配置类5-MVC

p153 152-第卌一讲-自定义自动配置类

p154 153-第卌二讲-条件装配底层1

p155 154-第卌二讲-条件装配底层2

p156 155-第卌三讲-FactoryBean

p157 156-第卌四讲-@Indexed

p158 157-第卌五讲-Spring代理的特点

p159 158-第卌五讲-Spring代理的特点

p160 159-第卌六讲-@Value注入底层1

p161 160-第卌六讲-@Value注入底层2

p162 161-第卌七讲-@Autowired注入底层-doResolveDependency外1

p163 162-第卌七讲-@Autowired注入底层-doResolveDependency外2

p164 163-第卌七讲-@Autowired注入底层-doResolveDependency内1

p165 164-第卌七讲-@Autowired注入底层-doResolveDependency内2

p166 165-第卌七讲-@Autowired注入底层-doResolveDependency内3

p167 166-第卌七讲-@Autowired注入底层-doResolveDependency内4

p168 167-第卌八讲-事件监听器1

p169 168-第卌八讲-事件监听器2

p170 169-第卌八讲-事件监听器3

p171 170-第卌八讲-事件监听器4

p172 171-第卌八讲-事件监听器5

p173 172-第卌九讲-事件发布器1

p174 173-第卌九讲-事件发布器2

来源地址:https://blog.csdn.net/qq_38505969/article/details/123739542

阅读原文内容投诉

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

软考中级精品资料免费领

  • 历年真题答案解析
  • 备考技巧名师总结
  • 高频考点精准押题
  • 2024年上半年信息系统项目管理师第二批次真题及答案解析(完整版)

    难度     813人已做
    查看
  • 【考后总结】2024年5月26日信息系统项目管理师第2批次考情分析

    难度     354人已做
    查看
  • 【考后总结】2024年5月25日信息系统项目管理师第1批次考情分析

    难度     318人已做
    查看
  • 2024年上半年软考高项第一、二批次真题考点汇总(完整版)

    难度     435人已做
    查看
  • 2024年上半年系统架构设计师考试综合知识真题

    难度     224人已做
    查看

相关文章

发现更多好内容

猜你喜欢

AI推送时光机
位置:首页-资讯-后端开发
咦!没有更多了?去看看其它编程学习网 内容吧
首页课程
资料下载
问答资讯