Spring详解


一、为什么使用Spring(优点)

Spring是一个轻量级的控制反转(IoC)和面向切面(AOP)的容器(框架)。

  • Spring是一个开源免费的框架 , 容器 .
  • Spring是一个轻量级的框架 , 非侵入式的 .
  • 控制反转 IOC
  • 面向切面 AOP
  • 对事物的支持
  • 对框架的支持

Spring用到的设计模式

  • 工厂模式BeanFactory就是简单工厂模式的体现,用来创建对象的实例;
  • 单例模式Bean默认为单例模式。
  • 代理模式Spring的AOP功能用到了JDK的动态代理和CGLIB字节码生成技术
  • 模板方法:用来解决代码重复的问题。比如. RestTemplate, JmsTemplate, JpaTemplate
  • 观察者模式:定义对象键一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都会得到通知被制动更新,如Spring中listener的实现–ApplicationListener

二、Spring详解

Spring模块

Spring 总共大约有 20 个模块, 由 1300 多个不同的文件构成。 而这些组件被分别整合在核心容器(Core Container) 、 AOP(Aspect Oriented Programming)和设备支持(Instrmentation) 、数据访问与集成(Data Access/Integeration) 、 Web、 消息(Messaging) 、 Test等 6 个模块中。
在这里插入图片描述

  • Spring核心容器: 该层基本上是Spring的核心。它包含以下模块:Spring CoreSpring BeanSpEL(Spring Expression Language)Spring Context。​​​​​​
    • Spring核心容器-Spring Core:核心容器
      • 提供Spring框架的基本功能。
      • 核心容器的主要组件是BeanFactory,它是工厂模式的实现。
      • BeanFactory使用控制反转(IOC)模式将应用程序的配置和依赖性规范与实际的应用程序代码分开。
      • 核心类库,提供IOC服务。
    • Spring上下文-Spring Context:Spring上下文
      • Spring上下文是一个配置文件,向Spring框架提供上下文信息。
      • Spring上下文包括企业服务,例如JNDI、EJB、电子邮件、国际化、检验和调度功能。
      • 提供框架式的Bean访问方式,以及企业级功能(JNDI、定时任务等)。
  • 数据访问/集成: 该层提供与数据库交互的支持。它包含:JDBC(Java DataBase Connectivity)ORM(Object Relational Mapping)OXM(Object XML Mappers)JMS(Java Messaging Service)Transaction
    • Spring DAO
      • 为JDBC DAO抽象层提供了有意义的异常层次结构,可用该结构来管理异常处理和不同数据库供应商抛出的错误消息。
      • 异常层次结构简化了错误处理,并且极大地降低了需要编写的异常代码数量(例如打开和关闭连接)。
      • Spring DAO的面向JDBC的异常遵从通用的DAO异常层次结构。
      • 对JDBC的抽象,简化了数据访问异常的处理。
    • Spring ORM
      • Spring支持常用的HibernateMybatisjdao等框架的支持
      • Spring本身并不对ORM进行实现,仅对常见的ORM框架进行封装,并对其进行管理。
      • Spring 的ORM模块提供对常用的ORM框架的管理和辅助支持
      • 利用ORM封装包,可以混合使用所有Spring`供的特性进行对象/关系映射,如简单声明性事务管理
      • 所有这些都遵从Spring的通用事务DAO异常层次结构
      • 对现有的ORM框架的支持。
  • Web–该层提供了创建Web应用程序的支持。它包含以下模块:WebWeb–ServletWeb–SocketWeb–Portlet
    • Spring Web:Web上下文模块
      • Web上下文模块建立在应用程序上下文模块之上,为基于Web的应用程序提供了上下文,所以,Spring框架支持与Jakarta Struts的集成。
      • Web模块还简化了处理多部分请求以及将请求参数绑定到域对象的工作。
      • 提供了基本的面向Web的综合特性,例如多方文件上传。
    • Spring MVC:MVC框架是一个全功能的构建Web应用程序的MVC实现。
      • 通过策略接口,MVC框架变成为高度可配置的,MVC容纳了大量视图技术,其中包括JSP、Velocity、Tiles、iText和POI。
      • 提供面向Web应用的Model-View-Controller实现。
  • AOP-该层支持面向切面编程
    • Spring AOP
      • 通过配置管理特性,Spring AOP模块直接将面向切面的编程功能集成到了Spring框架中,可以将一些通用任务,如安全、事务、日志等集中进行管理,提高了复用性和管理的便捷性。
        Instrumentation-该层为类检测和类加载器实现提供支持。
  • Test-该层为使用JUnitTestNG进行测试提供支持。
  • 几个杂项模块:
    • Messaging–该模块为STOMP提供支持。它还支持注解编程模型,该模型用于从WebSocket客户端路由和处理STOMP消息。
    • Aspects–该模块为与AspectJ的集成提供支持。

Spring 提供了以下5种标准的事件

  • 上下文更新事件(ContextRefreshedEvent):在调用ConfigurableApplicationContext 接口中的refresh()方法时被触发。
  • 上下文开始事件(ContextStartedEvent):当容器调用ConfigurableApplicationContextStart()方法开始/重新开始容器时触发该事件。
  • 上下文停止事件(ContextStoppedEvent):当容器调用ConfigurableApplicationContextStop()方法停止容器时触发该事件。
  • 上下文关闭事件(ContextClosedEvent):当ApplicationContext被关闭时触发该事件。容器被关闭时,其管理的所有单例Bean都被销毁
  • 请求处理事件(RequestHandledEvent):在Web应用中,当一个http请求(request)结束触发该事件。如果一个bean实现了ApplicationListener接口,当一个ApplicationEvent 被发布以后,bean会自动被通知。

三、IOC(控制反转)

传统开发中,需要调用对象的时候,需要调用者手动来创建被调用者的实例,即对象是由调用者new出来的。但是在Spring框架中,创建对象的工作不再由调用者来完成,而是交给IOC容器来创建,再推送给调用者,整个流程完成反转,所以是控制反转

  • 将对象的创建权限交给了Spring,为了降低耦合度
  • IOC底层原理:(工厂设计模式+反射+XML配置文件)
  • IOC思想基于IOC容器完成,IOC容器底层就是对象工厂

1.Spring 提供IOC容器实现两种方式(两个接口)

  1. BeanFactory:IOC容器基本实现,是Spring内部使用的接口,不建议开发人员使用
    1. 加载配置文件时,不会创建对象,只有在获取对象才去创建对象
  2. ApplicationContext:是BeanFactory的子接口,提供了更多更强大的接口,推荐开发人员使用
    1. 加载配置文件时,就会创建对象 FileSystemXmlApplicationContext(实现类) 盘路径
    2. ClassPathXmlApplicationContext(实现类) 类路径

2.IOC加载流程及Bean生命周期

IOC加载流程

  • 多例Bean在容器启动时不实例化,必须要在getBean调用的时候才实例化。
  • loadBeanDefinitions 采⽤了模板模式,具体加载 BeanDefinition 的逻辑由各个⼦类完成。
  • 单例Bean的初始化以及依赖注入一般都在容器的初始化阶段进行,除非设置了懒加载 lazy-ininttrue的单例bean就是在第一次调用getBean() 方法的会后进行初始化和依赖注入。

初始化过程:BeanDefinition的资源定位、解析、注册。
IOC加载流程图

在这里插入图片描述
bean生命周期详细流程图
在这里插入图片描述

通过上图了解

  1. 通过BeanDefinitionReader 读取配置文件,获取bean的定义信息解析成BeanDefinition,并注入到BeanDefinition实例中。
  2. 后置增强器 BeanFactoryPostProcessor:实现 BeanFactoryPostProcessor 接口可以在对象创建之前修改BeanDefinition,也就是修改bean的定义信息。
    /**
     * 扩展方法--后置增强器(可修改bean的定义信息)
     */
    @Component
    public class ExtBeanFactoryPostProcessor implements BeanFactoryPostProcessor {
        @Override
        public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
    		//BeanDefinition studentService = beanFactory.getBeanDefinition("studentService");
            System.out.println("扩展方法--可进行修改beanDefinition的定义信息");
        }
    }
    
    • 此 后置增强器 可存在多个,多次实现 BeanFactoryPostProcessor 即可
  3. BeanFactory根据BeanDefinition的定义信息创建实例化和初始化Bean,进入Bean的生命周期

Bean生命周期

Bean的生命周期粗略概括主要为:实例化—>初始化—>使用—>销毁
细化后Bean`的生命周期

实例化

InstantiationAwareBeanPostProcessor 接口

@Component
public class MyInstantiationAwareBeanPostProcessor implements InstantiationAwareBeanPostProcessor {
 
    // 实例化前置
    @Override
    public Object postProcessBeforeInstantiation(Class<?> beanClass, String beanName) throws BeansException {
        
        System.out.println("postProcessBeforeInstantiation被调用了----在对象实例化之前调用-----beanName:" + beanName);
        // 默认什么都不做,返回null
        return null;
    }
 
    // 实例化后置
    @Override
    public boolean postProcessAfterInstantiation(Object bean, String beanName) throws BeansException {
        System.out.println("postProcessAfterInstantiation被调用了---------beanName:" + beanName);
        //默认返回true,什么也不做,继续下一步
        return true;
    }
    
    // 属性修改
    @Override
    public PropertyValues postProcessPropertyValues(PropertyValues pvs, PropertyDescriptor[] pds, Object bean, String beanName) throws BeansException {
        System.out.println("postProcessPropertyValues被调用了---------beanName:"+beanName);
        // 此方法可对bean中的属性值进行、添加、修改、删除操作;
        // 对属性值进行修改,如果postProcessAfterInstantiation方法返回false,该方法可能不会被调用,
        return pvs;
    }
}

1. 实例化前置

  • 方法: postProcessBeforeInstantiation(Class<?> beanClass, String beanName)
  • 对象实例化之前对bean对象的class信息进行修改或者扩展,以达到我们想要的功能
  • 底层是动态代理AOP技术实现的
  • bean生命周期中最先执行的方法
  • 返回值:
    • 返回非空:
      • 返回值是Object类型,意味着可以返回任何类型的值,由于此时目标对象还未实例化,所以该返回值可以用来代替原本该生成对象的目标对象的实例。
      • 如果返回了非空的值,需要用到这个bean的时候,拿到的就现在返回的对象,不会去走第二步去实例化对象了;
    • 返回空(null)值:
      • 默认也是返回null值的,那么就直接返回,接下来会调用doCreateBean方法来实例化对象

2、实例化对象:

  • 方法:doCreateBean()
  • 该方法创建实例,用反射技术创建
  • 注意:这个时候只是将对象实例化了,对象内的属性还未设置

3、实例化后置

  • 方法:postProcessAfterInstantiation(Object bean, String beanName)
  • 在目标对象实例化之后调用(doCreateBean()调用之后)
  • 对象已实例化但对象内的属性还未设置,都是null。
  • 此方法的返回值决定要不要调用postProcessPropertyValuesgetDependencyCheck()与此方法共同决定)
  • 返回值:
    • false此方法返回false,且不需要check,那么postProcessPropertyValues就会被忽略不执行
    • true如果返回truepostProcessPropertyValues就会被执行

4、属性修改

  • 方法 :postProcessPropertyValues(PropertyValues pvs, PropertyDescriptor[] pds, Object bean, String beanName)
  • 此方法可对属性值进行修改,修改范围包括添加、修改、删除操作
  • 实例化后置 postProcessAfterInstantiation() 方法返回false,那么该方法不会被调用
初始化

1、给用户属性赋值

  • 方法:populateBean()
  • 用户属性指的是自定义的bean对象属性。如: User、Student、Teacher 、UserService、IndexService 这类的对象都是自定义bean对象
  • 此步骤主要给这类属性进行赋值操作

2、给容器属性赋值

  • 容器属性是容器自带的属性,这些属性是spring本来就有的
  • 它们都是 Aware 接口的实现类,主要有以下实现类,按执行顺序排列:
    在这里插入图片描述
  • 每个方法的作用为如下代码示例
    @Component
    public class AllAwareInterface implements BeanNameAware, BeanClassLoaderAware,
            BeanFactoryAware, EnvironmentAware, EmbeddedValueResolverAware,
            ResourceLoaderAware, ApplicationEventPublisherAware, MessageSourceAware,
            ApplicationContextAware, ServletContextAware, LoadTimeWeaverAware, ImportAware {
    
        /**
         * 让Bean对Name有知觉
         * 简单的返回我们当前的beanName
         * 官方指导为:这个接口更多的使用在spring的框架代码中,实际开发环境应该不建议使用
         */
        @Override
        public void setBeanName(String name) {
            System.out.println("1 我是 BeanNameAware 的 setBeanName 方法  ---参数:name,内容:" + name);
        }
    
        /**
         * 获取Bean的类装载器
         */
        @Override
        public void setBeanClassLoader(ClassLoader classLoader) {
            System.out.println("2 我是 BeanClassLoaderAware 的 setBeanClassLoader 方法");
        }
    
        /**
         * 获取bean工厂
         * beanFactory可以不依赖注入方式,随意的读取IOC容器里面的对象
         * 不过beanFactory本身还是要注入的
         */
        @Override
        public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
            // 注意: 如果使用 @Configuration 注解的话,setBeanFactory方法会执行2次,
            System.out.println("3 我是 BeanFactoryAware 的 setBeanFactory 方法");
        }
    
        /**
         * 在工程启动时可以获得application.properties 、xml、yml 的配置文件配置的属性值
         */
        @Override
        public void setEnvironment(Environment environment) {
            System.out.println("4 我是 EnvironmentAware 的 setEnvironment 方法");
        }
    
        /**
         * 设置嵌入值解析器
         * 通常我们使用@Value注解来获取properties 和 yml 文件中的值,每个类中都要使用@Value,
         * 实现EmbeddedValueResolverAware接口后就方便多了。用法和@Value一致,需要用${}包裹住;
         */
        @Override
        public void setEmbeddedValueResolver(StringValueResolver stringValueResolver) {
            System.out.println(stringValueResolver.resolveStringValue("${logging.file}"));
            System.out.println("5 我是 EmbeddedValueResolverAware 的 setEmbeddedValueResolver 方法");
        }
    
    
    
    
    
    
        /**
         * Spring ResourceLoader为我们提供了一个统一的getResource()方法来通过资源路径检索外部资源。
         * 从而将资源或文件(例如文本文件、XML文件、属性文件或图像文件)加载到Spring应用程序上下文中的不同实现 ,其实说白了,就是用来加载外部资源的;
         * 参数:ResourceLoader:这个参数其实是ApplicationContext(spring 的上下文对象)。
         * 并且我们可以指定不同的前缀来创建路径以从不同位置加载资源:
         *  前缀                          示例                                  说明
         * classpath:       从类路径加载classpath:com/myapp/config.xm1        从类路径加载
         * file:            file:///data/config.xml                         从文件系统作为 URL 加载
         * http:            https ://myserver/logo.png                      从 URL 加载
         * (none)           /data/config.xml                                取决于底层的 ApplicationContext
         *
         */
        @Override
        public void setResourceLoader(ResourceLoader resourceLoader) {
    
            // 可直接强转为 ApplicationContext
            ApplicationContext context = (ApplicationContext) resourceLoader;
    
    
            System.out.println("6 我是 ResourceLoaderAware 的 setResourceLoader 方法");
        }
    
    
        /**
         * 事件发布器的接口,使用这个接口,我们自己的 Service 就拥有了发布事件的能力。
         * 用户注册后,不再是显示调用其他的业务 Service,而是发布一个用户注册事件。
         * 那么在这里是发布事件,那就肯定有监听事件的接口,
         * 这个接口叫做 ApplicationListener <E extends ApplicationEvent > ,只要实现 ApplicationListener 接口就可以接受发布的事件了,
         * 具体示例在代码块完结后
         */
        @Override
        public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) {
            System.out.println("7 我是 ApplicationEventPublisherAware 的 setApplicationEventPublisher 方法");
        }
    
        /**
         * 国际化消息通知操作
         */
        @Override
        public void setMessageSource(MessageSource messageSource) {
            System.out.println("8 我是 MessageSourceAware 的 setMessageSource 方法");
        }
    
        /**
         * 通过实现ServletContextAware接口可获取servletContext,也就是servlet的上下文;
         *
         * ServletContext :
         * WEB容器在启动时,它会为每个WEB应用程序都创建一个对应的ServletContext对象,它代表当前web应用。S
         * ervletConfig对象中维护了ServletContext对象的引用
         * 开发人员在编写servlet时,可以通过ServletConfig.getServletContext方法获得ServletContext对象。
         * 由于一个WEB应用中的所有Servlet共享同一个ServletContext对象,因此Servlet对象之间可以通过ServletContext对象来实现通讯。
         * ServletContext对象通常也被称之为context域对象。
         */
        @Override
        public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
            System.out.println("9 我是 ApplicationContextAware 的 setApplicationContext 方法");
        }
    
        @Override
        public void setServletContext(ServletContext servletContext) {
            System.out.println("10 我是 ServletContextAware 的 setServletContext 方法");
        }
    
        /**
         * 简称LTW,LTW是AOP的一种实现方式,此方法是为了获取Aop织入的对象,使用的织入方式是:类加载期织入,
         * 一般的aop都是运行期织入,就是在运行的时候才进行织入切面方法,但是LTW是在类加载前就被织入了,也就是class文件在jvm加载之前进行织入切面方法
         * 只有在使用 @EnableLoadTimeWeaving 或者存在 LoadTimeWeaver 实现的 Bean 时才会调用,顺序也很靠后;
         */
        @Override
        public void setLoadTimeWeaver(LoadTimeWeaver loadTimeWeaver) {
            //LoadTimeWeaver 简称LTW,LTW是AOP的一种实现方式,此方法是为了获取Aop织入的对象,使用的织入方式是:类加载期织入,
            // 一般的aop都是运行期织入,就是在运行的时候才进行织入切面方法,但是LTW是在类加载前就被织入了,也就是class文件在jvm加载之前进行织入切面方法
            // 只有在使用 @EnableLoadTimeWeaving 或者存在 LoadTimeWeaver 实现的 Bean 时才会调用,顺序也很靠后
            System.out.println("11 我是 LoadTimeWeaverAware 的 setLoadTimeWeaver 方法");
        }
    
        /**
         * 只有被其他配置类 @Import(XX.class) 时才会调用
         * 这个调用对 XX.class 中的所有 @Bean 来说顺序是第 1 的。
         */
        @Override
        public void setImportMetadata(AnnotationMetadata annotationMetadata) {
    
            System.out.println("12 我是 ImportAware 的 setImportMetadata 方法");
        }
    }
    
  • setApplicationEventPublisher()示例
    • 先创建一个实体类StringEvent.java,用来存储发布的事件内容
      //事件监听对象
      public class StringEvent extends ApplicationEvent {
       
          private String str;
          // 构造函数
          public StringEvent(Object source) {
              super(source);
              str = source.toString();
          }
          // 获取字符串
          public String getStr(){
              return str;
          }
      }
      
    • 创建一个发布事件的类: ExtApplicationEventPublisherAware.java ,实现 ApplicationEventPublisherAware 接口增加发布事件的功能
      /**
       * 发布事件
       */
      @Component
      public class ExtApplicationEventPublisherAware implements ApplicationEventPublisherAware {
          @Override
          public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) {
              System.out.println("发布事件,事件对象为 StringEvent ,内容为 :1234");
              StringEvent stringEvent = new StringEvent("1234");
              // 发布事件 ,发布后会在 ApplicationListener.onApplicationEvent()方法进行捕获;
              applicationEventPublisher.publishEvent(stringEvent);  // 发布事件
          }
      }
      
    • 创建一个事件监听器: EventListener.java ,用来监听所有发布的事件;
      //事件监听器
      @Component
      public class EventListener implements ApplicationListener<StringEvent> {
       
          @Override
          public void onApplicationEvent(StringEvent o) {
              System.out.println("监听到事件,内容:"+o.getStr());
          }
      }
      
    • 运行结果示例(启动Spring项目):
      发布事件,事件对象为 StringEvent ,内容为 :1234
      监听到事件,内容:1234		
      

4.IOC操作 Bean 管理(Spring创建对象)

基于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"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">

    <!--配置User对象创建-->
    <bean id="user" class="com.spring5.User"></bean>
</beans>

  • applicationContext.xml配置文件中,使用bean标签,标签中添加对应属性就可实现对象创建
    • id属性:给创建的对象的名称,不可添加特殊符号(唯一标识,不可重复) class 属性:需要创建类的全路径
    • name属性:和Id属性作用相同,可以添加特殊符号
  • 注入属性
    • set方法:在bean标签中添加如下标签即可set方法注入属性
		<property name="属性名称" value=‘属性值’/>
  • 名称空间注入
    • 在xml头部beans标签中添加一个名称空间,直接把属性写在bean标签内即可,不用再写property标签
    <?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:p="http://www.springframework.org/schema/p"
    xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
    
    <bean id="book" class="com.spring5.Book" p:bname="WeiSanJin" p:bauthor="WeiSanJin"></bean>
    
    </beans>
    
  • 有参构造
    • 在bean标签中添加如下标签即可完成有参构造
 		<constructor-arg name='属性名称' value=‘属性值’/> 
  • 注入null值:使用 null 标签
		<constructor-arg name='属性名称' >  <null/>  </constructor-arg>
  • 注入特殊符号
    • 使用转义字符代替特殊符号 如:< 小于号 转义为 &lt;
    • 使用value标签:
      		<value> ![CDATA[属性值]] </value>
      
  • 注入外部bean属性第一步先创建自己的bean,第二部创建外部bean
    • 此处必须在自己的bean中存在外部bean属性用于接收外部bean
    • 在自己的bean中注入外部bean:
      		<constructor-arg name='属性名称' ref=‘外部 bean id’/>
      
  • property 中可以嵌套bean:被嵌套的bean中还可以写入property 标签
    		<property name='属性名称'> 
    			<bean id='xxxx' class='xxxx'>
    			<property name="属性名称" value=‘属性值’/>
    			<property name="属性名称" value=‘属性值’/>
    			</bean> 
    		</property >
    
  • 集合类属性注入
     <!--1. 集合类型属性注入-->
        <bean id="stu" class="com.spring5.collectionytpe.Stu">
            <!--数组类型属性注入 -->
            <property name="courses">
                <array>
                    <value>Java课程</value>
                    <value>数据库课程</value>
                </array>
            </property>
            <!--list类型属性注入 -->
            <property name="list">
                <list>
                    <value>张三</value>
                    <value>小三</value>
                </list>
                //如果集合里的存储的是其它bean对象,就需要引用,同时也需要配置引用的bean
                <list>
                    <ref bean="course1"></ref>
                    <ref bean="course2"></ref>
                </list>
            </property>
            <!--map类型属性注入 -->
            <property name="maps">
                <map>
                    <entry key="Java" value="java"></entry>
                    <entry key="PHP" value="php"></entry>
                </map>
            </property>
            <!--set类型属性注入 -->
            <property name="sets">
                <set>
                    <value>Mysql</value>
                    <value>Redis</value>
                </set>
            </property>
        </bean>
    
    <!-- 创建多个course对象-->
        <bean id="course1" class="com.spring5.collectionytpe.Course">
            <property name="cname" value="String"></property>
        </bean>
        <bean id="course2" class="com.spring5.collectionytpe.Course">
            <property name="cname" value="String"></property>
        </bean>
    
  • util命名空间
    <?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:util="http://www.springframework.org/schema/util"
           xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
                               http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util.xsd">
    
     <!--1 提前list集合类型属性注入-->
    <util:list id="bookList">
         <value>三国演义</value>
         <value>水浒传</value>
         <value>西游记</value>
         <value>红楼梦</value>
    </util:list>
    
    <!--2 提前list集合类型属性使用-->
    <bean id="book" class="com.spring5.collectionytpe.Book">
         <property name="list" ref="bookList"></property>
    </bean>
    
    </beans>
    

基于注解方式

!!!!!!!!!!!!重点注意!!!!!!!!!!!!
使用基于注解方式注入bean时,必选完成以下两种方式其中一种!
xml配置注解支持

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:context="http://www.springframework.org/schema/context">

    <!--容器创建时要扫描的包-->
    <context:component-scan base-package="org.example.*"/>

</beans>

配置类配置注解支持

//指定当前类为配置类
@Configuration 
//容器创建时要扫描的包
@ComponentScan(basePackages = "org.example.*")
public ApplicationConfig {

}

@Configuration@Component@Controller@Service@Repository~
具体解释请查看上方SpringIoc常用注解

四、DI(依赖注入)

1.概述

Dependency Injection即为依赖注入,简称DI 。
创建对象的过程中Spring可以依据配置对象的属性进行设置,这个过程称之为依赖注入,即 DI

  • 简单来说,在Spring创建对象的同时,为其属性赋值,称之为依赖注入。
  • 形象来说,组件之间依赖关系由容器在运行期决定的,即由容器动态的将某个依赖关系注入到组件之中。
  • 依赖:
    • 应用程序依赖于IOC容器
    • 应用程序依赖于IOC容器提供的对象所需外部资源
  • 注入:
    • 容器的IOC是被注入的对象(我们项目中所需要的对象、资源、数据等等)
    • 动态的向某个对象提供它所需要的其他对象
    • 需要外部的资源既可以注入到IOC容器中,并由IOC容器来实现注入对象的控制反转

2.注入方式

2.1.Setter方法注入

  • 定义一个JavaBean并赋予其Setter方法
    //Lombok@Data注解提供了Setter方法
    public class User {
        private Integer             id;
        private String              password;
        private String              sex;
        private Integer             age;
        private Date                bornDate;
        private String[]            hobbys;
        private Set<String>         phones;
        private List<String>        names;
        private Map<String, String> countries;
        private Properties          files;
    }
    
  • 注入各种数据(Spring底层对Date日期类型做了处理,默认处理格式为“yyyy/MM/dd”)
    <bean id="User" class="com.mylifes1110.bean.User">
        <!--注入基本数据类型-->
        <property name="id" value="1"/>
        <property name="password" value="123456"/>
        <property name="sex" value="male"/>
        <property name="age" value="18"/>
        <!--注入日期类型-->
        <property name="bornDate" value="1999/09/09"/>
        <!--注入数组类型-->
        <property name="hobbys">
            <array>
                <value>Run</value>
                <value>Jump</value>
                <value>Climb</value>
            </array>
        </property>
        <!--注入List集合类型-->
        <property name="names">
            <list>
                <value>Ziph</value>
                <value>Join</value>
                <value>Marry</value>
            </list>
        </property>
        <!--注入Set集合类型-->
        <property name="phones">
            <set>
                <value>110</value>
                <value>119</value>
                <value>120</value>
            </set>
        </property>
        <!--注入Properties类型-->
        <property name="files">
            <props>
                <prop key="first">One</prop>
                <prop key="second">Two</prop>
                <prop key="third">Three</prop>
            </props>
        </property>
        <!--注入Map集合类型-->
        <property name="countries">
            <map>
                <entry key="CHINA" value="中国"/>
                <entry key="USA" value="美国"/>
                <entry key="UK" value="英国"/>
            </map>
        </property>
    </bean>
    

2.2.构造方法注入

创建对象时,Spring工厂会通过构造方法为对象的属性赋值。由于某些框架或者项目中并没有为JavaBean提供Setter方法,我们就可以利用其构造方法来注入

  • 定义一个JavaBean对象,为其提供构造方法
    public class Student {
        private Integer id;
        private String name;
        private String sex;
        private Integer age;
      
        //Constructors
      	public Student(Integer id , String name , String sex , Integer age){
          	this.id = id;
        	this.name = name;
      	    this.sex = sex;
    	    this.age = age;
        }
    }
    
  • 构造方法注入
     <!--构造注入-->
    <bean id="u3" class="com.mylifes1110.bean.Student">
        <!-- 除标签名称有变化,其他均和Set注入一致 -->
        <constructor-arg name="id" value="1234" /> 
        <constructor-arg name="name" value="tom" />
        <constructor-arg name="age" value="20" />
        <constructor-arg name="sex" value="male" />
    </bean>
    

2.3.注入自建类型数据

将Bean当做参数注入即可

2.4.自动注入

xml自动注入
  • 基于名称自动注入值
    <bean id="UserDao" class="com.mylifes1110.dao.impl.UserDaoImpl"/>
    	<!--为UserServiceImpl中的属性基于名称自动注入值-->
    	<bean id="userService" class="com.mylifes1110.service.impl.userServiceImpl" autowire="byName"/>
    </beans>
    
  • 基于类型自动注入值
    根据实现的接口来判断并自动注入值,如果实现此接口的实现类太多,它会在很多实现此接口的实现类中选择名字相同的实现类进行注入。(现根据判断,如果不成功,则根据名称注入)
    <bean id="userDao" class="com.mylifes1110.dao.UserDaoImpl" />
    	<!--为UserServiceImpl中的属性基于类型自动注入值-->
    	<bean id="userService" class="com.mylifes1110.service.impl.UserServiceImpl" autowire="byType"/>
    </beans>
    
注解自动注入
注解名称描述
@Autowired基于类型自动注入
@Resource基于名称自动注入
@Qualifier(“userDAO”)限定要自动注入的bean的id,一般和@Autowired联用
@Value注入简单类型数据 (jdk8种基本数据类型+String类型)
  • 使用基于类型自动注入,将Dao层注入到Service层
    @Service
    public class UserServiceImpl implements UserService {   
        @Autowired //注入类型为UserDao的bean
        @Qualifier("userDao") //如果有多个类型为UserDao的bean,可以用此注解从中指定一个
        private UserDao userDao;
    }
    
  • 使用基于名称自动注入,将Dao层注入到Serivce层
    @Service
    public class UserServiceImpl implements UserService {   
        @Resource("userDao") //注入id=“userDao”的bean
        private UserDao userDao;
    }
    
  • 使用注入简单类型数据注解来完成简单注入JavaBean
    	public class User{
    	    @Value("1")    //注入数字
    	    private Integer id;
    	    @Value("Ziph") //注入String
    		private String name;
    	}
    

五、Spring AOP

什么是AOP:
AOP是一种思想,一种编程方式。编写一段代码在合适的时机找到切入点然后执行。不直接修改原来的代码,而是在原代码执行的前后执行一段额外的代码。

优点:

  • 可以降低模块之间的耦合性
  • 提供代码的复用性
  • 提高代码的维护性
  • 集中管理非业务代码,便于维护
  • 业务代码不受非业务代码影响,逻辑更加清晰

Spring引入了一套AOP顶级API-- AOP联盟,用来定义和使用AOP。底层根据配置生成JDK或CGLIB动态代理对象

1.AOP联盟

spring-aop.jarorg.aopalliance.aop包和intercept包下 是 AOP联盟提供的AOP标准API
在这里插入图片描述

  • Advice.java:顶级通知类,实现可以是任何类型的通知。例如:拦截器
  • Interceptor.java:通用拦截器,包含需要植入的自定义切面内容,代理类通过连接点来调用拦截器执行增强逻辑,继承Advice
  • Invocation.java:表示调用,可以被拦截的连接点,静态连接点,继承Joinpoint,和Interceport结合使用
  • Joinpoint.java:运行时连接点,将被切面的对象和拦截器连接起来,包含了被切面对象信息和拦截链,proceed方法通过传入Interceport实现链式调用拦截器
  • MethodInterceptor.java:方法拦截器,方法被调用时拦截,用户实现invoke方法自定义切面内容,实现拦截行为,继承Interceport
  • MethodInvocation.java:方法连接点,方法被调用时携带被切面对象信息和拦截链,执行切面自定义切面内容

2.SpringAOP核心概念

  • SpringAOP在AOP联盟规则之上新增了几个类,丰富了AOP定义及使用概念,如下:

    • Advisor:包含通知(拦截器),Spring内部使用的AOP顶级接口,还需要包含一个aop适用判断的过滤器,考虑到通用性,过滤规则由其子接口定义,例如IntroductionAdvisorPointcutAdvisor,过滤器用于判断bean是否需要被代理
    • Pointcut: 切点,属于过滤器的一种实现,匹配过滤哪些类哪些方法需要被切面处理,包含一个ClassFilter和一个MethodMatcher,使用PointcutAdvisor定义时需要
    • ClassFilter:限制切入点或引入点与给定目标类集的匹配的筛选器,属于过滤器的一种实现。过滤筛选合适的类,有些类不需要被处理
    • MethodMatcher:方法匹配器,定义方法匹配规则,属于过滤器的一种实现,哪些方法需要使用AOP
  • AOP术语

    名称翻译说明
    Joinpoint连接点是在应用程序执行过程中能够插入切面的一个点,这个点可以是调用方法时,抛出异常时,甚至修改一个字段时,切面代码可以利用这些点插入到应用程序的正常流程中,并增加新的行为
    Pointcut切点需要去添加代码的“地方”。 描述了切面织入的位置。 切点的定义会匹配通知所要织入的一个或多个连接点,可以指定明确的类和方法,也可以使用正则表达式匹配符合条件的类和方法来指定这些切点。
    Target目标指代理的目标对象
    Advice通知向切点处动态添加的那部分代码。 描述了切面要做的工作。
    Aspect切面切点+通知
    Introduction引入向现有类添加新方法或属性。
    Weaving织入把增加代码应用到目标上,生成代理对象的过程
    Proxy代理指生成代理的对象
  • 通知分类

    通知翻译说明
    before前置通知通知方法在目标方法调用之前执行
    after后置通知通知方法在目标方法返回或异常后调用
    after-returning返回后通知通知方法在目标方法返回后调用
    after-throwing抛出异常通知通知方法在目标方法异常后调用
    around后置通知通知方法在目标方法封装起来
  • 织入时期

    时期说明
    编译期切面在目标类编译时被织入
    类加载期切面在目标类加载到JVM时被织入
    运行期切面在应用运行的某个时期被织入,通常在织入切面时,AOP容器会为目标对象动态创建一个代理对象(Spring AOP就是以这种方式织入切面的)

    关系图
    在这里插入图片描述

3.使用AOP的方式

准备: 创建接口,存在一个睡觉的方法,并实现该接口。

//接口
public interface Sleepable {
	public void sleep();
}

//实现类
//接口实现类,“Me”可以睡觉,“Me”就实现可以睡觉的接口


public class Me implements Sleepable{
	public void sleep() {
		System.out.println("\n睡觉!不休息哪里有力气学习!\n");
	}
}

Me关注于睡觉的逻辑,但是睡觉需要其他功能辅助,比如睡前脱衣服,起床脱衣服,这里开始就需要AOP替“Me”完成!解耦!

睡觉前要脱衣服:切入点之前执行。
起床后要穿衣服:切入点之后执行。

3.1.经典的基于代理的AOP

此方式可以利用反射机制获得该方法的一些信息,必须的参数等
创建一个类编写增强或辅助代码并实现如下接口

public class SleepHelper implements MethodBeforeAdvice, AfterReturningAdvice {
	 //MethodBeforeAdvice接口,在目标方法调用前调用
	public void before(Method arg0, Object[] arg1, Object arg2) throws Throwable {
		System.out.println("睡觉前要脱衣服!");
	}
	 //AfterReturningAdvice接口,在目标方法被调用之后调用
	public void afterReturning(Object arg0, Method arg1, Object[] arg2, Object arg3) throws Throwable {
		System.out.println("起床后要穿衣服!");
	}
}

将此类加入到SpringIoc容器管理,并配置切入点

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">

    <!-- 定义被代理者 -->
    <bean id="me" class="org.example.SpringAopTest.Me"/>

    <!-- 定义通知内容,也就是切入点执行前后需要做的事情 -->
    <bean id="sleepHelper" class="org.example.SpringAopTest.SleepHelper"/>

    <!-- 定义切入点位置 -->
    <bean id="sleepPointcut" class="org.springframework.aop.support.JdkRegexpMethodPointcut">
        <property name="pattern" value=".*sleep"/>
    </bean>

    <!-- 使切入点与通知相关联,完成切面配置 -->
    <bean id="sleepHelperAdvisor" class="org.springframework.aop.support.DefaultPointcutAdvisor">
        <property name="advice" ref="sleepHelper"/>
        <property name="pointcut" ref="sleepPointcut"/>
    </bean>

    <!-- 设置代理 -->
    <bean id="proxy" class="org.springframework.aop.framework.ProxyFactoryBean">
        <!-- 代理的对象,有睡觉能力 -->
        <property name="target" ref="me"/>
        <!-- 使用切面 -->
        <property name="interceptorNames" value="sleepHelperAdvisor"/>
        <!-- 代理接口,睡觉接口 -->
        <property name="proxyInterfaces" value="org.example.SpringAopTest.Sleepable"/>
    </bean>

</beans>

运行测试
通过AOP代理的方式执行Me的sleep()方法,会把执行前、执行后的操作执行,实现了AOP的效果

public class Test {
    public static void main(String[] args){
        @SuppressWarnings("resource")
        //如果是web项目,则使用注释的代码加载配置文件,这里是一般的Java项目,所以使用下面的方式
        //ApplicationContext appCtx = new ClassPathXmlApplicationContext("src/main/resources/application.xml");
        ApplicationContext appCtx = new FileSystemXmlApplicationContext("src/main/resources/application.xml");
        Sleepable me = (Sleepable)appCtx.getBean("proxy");
        me.sleep();
    }
}

执行结果:

睡觉前要脱衣服!

睡觉!不休息哪里有力气学习!

起床后要穿衣服!

可以通过org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator简化配置

  1. 将配置文件中设置代理的代码去掉,加上:
    <bean class="org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator"/>
  2. Test中,直接获取me对象,执行sleep方法,就可以实现同样的功能!
  3. 通过自动匹配,切面会自动匹配符合切入点的bean,会被自动代理,实现功能!

3.2…通过AspectJ提供的注解实现AOP

@Aspect
public class SleepHelper{
 
    public SleepHelper(){
        
    }
    
    @Pointcut("execution(* *.sleep())")
    public void sleeppoint(){}
    
    @Before("sleeppoint()")
    public void beforeSleep(){
        System.out.println("睡觉前要脱衣服!");
    }
    
    @AfterReturning("sleeppoint()")
    public void afterSleep(){
        System.out.println("睡醒了要穿衣服!");
    }
    
}

在方法中,可以加上JoinPoint参数以进行相关操作

//当抛出异常时被调用
    public void doThrowing(JoinPoint point, Throwable ex)
    {
        System.out.println("doThrowing::method "
                + point.getTarget().getClass().getName() + "."
                + point.getSignature().getName() + " throw exception");
        System.out.println(ex.getMessage());
    }

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:aop="http://www.springframework.org/schema/aop"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/aop https://www.springframework.org/schema/aop/spring-aop.xsd">

    <aop:aspectj-autoproxy />
    <!-- 定义通知内容,也就是切入点执行前后需要做的事情 -->
    <bean id="sleepHelper" class="org.example.SpringAopTest.SleepHelper"></bean>
    <!-- 定义被代理者 -->
    <bean id="me" class="org.example.SpringAopTest.Me"></bean>
</beans>

运行测试
通过AOP代理的方式执行Me的sleep()方法,会把执行前、执行后的操作执行,实现了AOP的效果

public class Test {
    public static void main(String[] args){
        @SuppressWarnings("resource")
        //如果是web项目,则使用注释的代码加载配置文件,这里是一般的Java项目,所以使用下面的方式
        //ApplicationContext appCtx = new ClassPathXmlApplicationContext("src/main/resources/application.xml");
        ApplicationContext appCtx = new FileSystemXmlApplicationContext("src/main/resources/application.xml");
        Sleepable me = (Sleepable)appCtx.getBean("me");
        me.sleep();
    }
}

执行结果:

睡觉前要脱衣服!

睡觉!不休息哪里有力气学习!

起床后要穿衣服!

3.3.使用Spring来定义纯粹的POJO切面(通过<aop:fonfig> 标签配置)

这种方式的优点就是在代码中不体现任何AOP相关配置,纯粹使用xml配置。

SleepHelper类

public class SleepHelper{
 
    public void beforeSleep(){
        System.out.println("睡觉前要脱衣服!");
    }
    
    public void afterSleep(){
        System.out.println("睡醒了要穿衣服!");
    }
  
}

配置文件:

<?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:aop="http://www.springframework.org/schema/aop"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/aop https://www.springframework.org/schema/aop/spring-aop.xsd">

    <!-- 定义通知内容,也就是切入点执行前后需要做的事情 -->
    <bean id="sleepHelper" class="org.example.SpringAopTest.SleepHelper"/>
    <!-- 定义被代理者 -->
    <bean id="me" class="org.example.SpringAopTest.Me"/>

    <aop:config>
        <aop:aspect ref="sleepHelper">
            <aop:before method="beforeSleep" pointcut="execution(* *.sleep(..))" />
            <aop:after method="afterSleep" pointcut="execution(* *.sleep(..))" />
        </aop:aspect>
    </aop:config>
</beans>

配置的另一种写法

<?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:aop="http://www.springframework.org/schema/aop"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/aop https://www.springframework.org/schema/aop/spring-aop.xsd">

    <!-- 定义通知内容,也就是切入点执行前后需要做的事情 -->
    <bean id="sleepHelper" class="org.example.SpringAopTest.SleepHelper"/>
    <!-- 定义被代理者 -->
    <bean id="me" class="org.example.SpringAopTest.Me"/>

    <aop:config>
        <aop:aspect ref="sleepHelper">
            <aop:pointcut id="sleepHelpers" expression="execution(* *.sleep(..))" />
            <aop:before pointcut-ref="sleepHelpers" method="beforeSleep" />
            <aop:after pointcut-ref="sleepHelpers" method="afterSleep" />
        </aop:aspect>
    </aop:config>
</beans>

六、Spring声明式事务

  • 事务在项目开发过程非常重要,涉及到数据的一致性的问题,不容马虎!
  • 事务管理是企业级应用程序开发中必备技术,用来确保数据的完整性和一致性。

编程式事务管理

  • 将事务管理代码嵌到业务方法中来控制事务的提交和回滚
  • 缺点:必须在每个事务操作业务逻辑中包含额外的事务管理代码

声明式事务管理

  • 将事务管理代码从业务方法中分离出来,以声明的方式来实现事务管理。

  • 将事务管理作为横切关注点,通过aop方法模块化。Spring中通过Spring AOP框架支持声明式事务管理。

事务四个属性ACID

  • 原子性(atomicity):事务是原子性操作,由一系列动作组成,事务的原子性确保动作要么全部完成,要么完全不起作用

  • 一致性(consistency):一旦所有事务动作完成,事务就要被提交。数据和资源处于一种满足业务规则的一致性状态中

  • 隔离性(isolation):可能多个事务会同时处理相同的数据,因此每个事务都应该与其他事务隔离开来,防止数据损坏

  • 持久性(durability):事务一旦完成,无论系统发生什么错误,结果都不会受到影响。通常情况下,事务的结果被写到持久化存储器中

Spirng配置事务

xml配置方式

xml文件配置
注意头文件的约束导入 : tx

头文件配置

xmlns:tx="http://www.springframework.org/schema/tx"
 
http://www.springframework.org/schema/tx
http://www.springframework.org/schema/tx/spring-tx.xsd">

配置事务管理器

事务管理器:

  • 无论使用Spring的哪种事务管理策略(编程式或者声明式)事务管理器都是必须的。

  • 就是 Spring的核心事务管理抽象,管理封装了一组独立于技术的方法。

向IOC容器注册事务管理器

<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <property name="dataSource" ref="dataSource" />
 </bean>

配置事务的通知

<!--配置事务通知-->
<tx:advice id="txAdvice" transaction-manager="transactionManager">
    <tx:attributes>
        <!--配置哪些方法使用什么样的事务,配置事务的传播特性-->
        <tx:method name="add" propagation="REQUIRED"/>
        <tx:method name="delete" propagation="REQUIRED"/>
        <tx:method name="update" propagation="REQUIRED"/>
        <tx:method name="search*" propagation="REQUIRED"/>
        <tx:method name="get" read-only="true"/>
        <tx:method name="*" propagation="REQUIRED"/>
    </tx:attributes>
</tx:advice>

配置AOP织入事务
导入AOP头文件

xmlns:aop="http://www.springframework.org/schema/aop"

向IOC注册代理

<!--配置aop织入事务-->
<aop:config>
    <aop:pointcut id="txPointcut" expression="execution(* com.kuang.dao.*.*(..))"/>
    <aop:advisor advice-ref="txAdvice" pointcut-ref="txPointcut"/>
</aop:config>

基于注解的声明式事务

使用 @EnableTransactionManagement 注解开启事务

	@Configuration
	@EnableTransactionManagement//开启基于注解的声明式事务
	public class SpringTxConfig {
	}

spring的事务传播机制

  • PROPAGATION_REQUIRED:(默认传播行为)如果当前没有事务,就创建一个新事务;如果当前存在事务,就加入该事务。
  • PROPAGATION_REQUIRES_NEW:无论当前存不存在事务,都创建新事务进行执行。
  • PROPAGATION_SUPPORTS:如果当前存在事务,就加入该事务;如果当前不存在事务,就以非事务执行。‘
  • PROPAGATION_NOT_SUPPORTED:以非事务方式执行操作,如果当前存在事务,就把当前事务挂起。
  • PROPAGATION_NESTED:如果当前存在事务,则在嵌套事务内执行;如果当前没有事务,则按REQUIRED属性执行。
  • PROPAGATION_MANDATORY:如果当前存在事务,就加入该事务;如果当前不存在事务,就抛出异常。
  • PROPAGATION_NEVER:以非事务方式执行,如果当前存在事务,则抛出异常。

Spring中的隔离级别

  • ISOLATION_DEFAULT:这是个 PlatfromTransactionManager 默认的隔离级别,使用数据库默认的事务隔离级别。
  • ISOLATION_READ_UNCOMMITTED:读未提交,允许事务在执行过程中,读取其他事务未提交的数据。
  • ISOLATION_READ_COMMITTED:读已提交,允许事务在执行过程中,读取其他事务已经提交的数据。
  • ISOLATION_REPEATABLE_READ:可重复读,在同一个事务内,任意时刻的查询结果都是一致的。
  • ISOLATION_SERIALIZABLE:所有事务逐个依次执行。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

我好帅啊~

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值