Spring学习笔记

1、Spring基础

1.1简介

  1. 由一个叫Rod Johnson的程序员在 2002 年最早提出(interface 21框架)并随后创建,是为了解决企业级编程开发中的复杂性,实现敏捷开发的应用型框架 。
  2. Spring理念:使现有技术更加容易使用,本身是一个大杂烩,整合现有框架。
  3. 官网:https://spring.io/projects/spring-framework#overview
  4. 下载地址:https://repo.spring.io/ui/native/release/org/springframework/spring/

1.2 核心部分

  • IOC:控制反转,把创建对象过程交给 Spring 进行管理 。
  • Aop:面向切面,不修改源代码进行功能增强。

1.3优点

  1. Spring是一个开源免费框架(容器)。
  2. Spring是一个轻量级的,非入侵的框架。
  3. 控制反转(IOC),面向切面编程(AOP)
  4. 支持事物的处理,对框架整合的支持

总结:Spring是一个轻量级的控制反转(IOC)和面向切面编程(AOP)的框架!

1.4拓展

  • Spring Boot

    • 一个快速开发的脚手架
    • 基于SpringBoot可以快速地开发单个微服务
    • 约定大于配置
  • Spring Cloud

    • SPringCloud是基于SpringBoot实现地

    很多公司都在使用SpringBoot进行快速开发,学习SpringBoot的前提是完全掌握Spring以及SpringMVC!

2、IOC容器

2.1 什么是IOC

  1. 控制反转,把对象创建和对象之间的调用过程交给Spring进行管理。

  2. 使用IOC的目的:为了降低耦合。

  3. IOC的底层原理:xml解析、工厂模式、反射。

  1. IOC 思想基于 IOC 容器完成,IOC 容器底层就是对象工厂

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

    1. BeanFactory:IOC 容器基本实现,是 Spring 内部使用 接口,不提供开发人员进行使用 。加载配置文件时候不会创建对象,在获取对象(使用)才去创建对象。
    2. ApplicationContext:BeanFactory 接口的子接口,提供更多更强大的功能,一般由开发人 员进行使用。加载配置文件时候就会把在配置文件对象进行创建。
      1. ApplicationContext 接口有实现类有FileSystemXmlApplicationContextClassPathXmlApplicationContext

2.2 IOC 操作(Bean管理)

  1. 什么是 Bean 管理
    1. Spring 创建对象
    2. Spirng 注入属性
  2. Bean 管理操作有两种方式
    1. 基于 xml 配置文件方式实现
    2. 基于注解方式实现

2.3 Bean 管理——基于 xml 方式

xml配置

alias:用于设置别名

<!--别名的设置,不常用,可以用name代替-->
<alias name="user" alias="uu"/>

import:用于将多个配置文件导入合并为一个,多用于团队开发,applicationContext.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
        https://www.springframework.org/schema/beans/spring-beans.xsd">

    <import resource="beans.xml"/>
    <import resource="beans2.xml"/>
</beans>

创建对象 使用bean标签

<bean id="helloSpring" class="com.th.pojo.Hello" name="u1 u2,u3;u4">
    <property name="str" value="HelloSpring"/>
</bean>

在 spring 配置文件中,使用 bean 标签,并对应属性,实现对象创建。

在 bean 标签有很多属性,介绍常用的属性 :

  • id : bean的唯一标识 。
  • class :类全路径(包类路径) 。
  • name: 早期版本使用,可以用于别名设置,可以取多个别名,用逗号或者分号分开即可。
  • scope:prototype、request、session、singleton

创建对象时候,默认使用无参构造方法完成对象创建。所以存在有参构造时最好把无参构造显示的写出来

注入属性

DI:依赖注入,就是注入属性。

方式:

  • 使用setter方法
  • 使用使用有参数构造进行注入。

Spring可以根据这两种方式分别使用propertyconstructor-arg标签进行配置,从而注入属性。

  1. property注入

    <bean id="helloSpring" class="com.th.pojo.Hello">
        <property name="str" value="HelloSpring"/>
    </bean>
    

    name: 类里面属性名称

    value:向属性注入的值

    ref:标识引用Spring容器中已经创建好的的对象

    各种数据类型的注入

    https://docs.spring.io/spring-framework/docs/current/reference/html/core.html#beans-factory-properties-detailed

    <bean id="student" class="com.th.pojo.Student">
        <!--第一种:普通值注入,用value-->
        <property name="name" value="ThreePure"/>
        
        <!--第二种:Bean注入,用ref-->
        <property name="address" ref="address"/>
    
        <!--第三种:数组-->
        <property name="books">
            <array>
                <value>红楼梦</value>
                <value>水浒传</value>
                <value>西游记</value>
                <value>三国演义</value>
            </array>
        </property>
    
        <!--第四种:List-->
        <property name="hobbys">
            <list>
                <value>篮球</value>
                <value>书法</value>
                <value>跑步</value>
            </list>
        </property>
    
        <!--第五种:Map-->
        <property name="card">
            <map>
                <entry key="身份证" value="13512626346"/>
                <entry key="银行卡" value="13512624327236346"/>
            </map>
        </property>
    
        <!--第六种:Set-->
        <property name="games">
            <set>
                <value>LOL</value>
                <value>COC</value>
                <value>BOB</value>
            </set>
        </property>
    
        <!--第七种:空指针-->
        <property name="wife">
            <null/>
        </property>
    
        <!--第八种:Properties-->
        <property name="info">
            <props>
                <prop key="学号">13891</prop>
                <prop key="性别">男</prop>
                <prop key="年龄">21</prop>
            </props>
        </property>
    </bean>
    

    引入外部属性文件 以druid连接数据库为例

    1. 创建外部文件,properties 格式文件,写数据库信息

      prop.driverClass=com.mysql.jdbc.Driver
      prop.url=jdbc:mysql://localhost:3306/mybatis
      prop.userName=root
      prop.password=mysqlpw
      
    2. 把外部 properties 属性文件引入到 spring 配置文件中

      <!--引入外部属性文件-->
      <context:property-placeholder location="druid.properties"/>
      

      注意:使用这个标签时必须在配置文件中导入context约束。

    3. 获取外部文件的值 (使用 ${} 获取properties的值)

      <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
          <property name="driverClassName" value="${prop.driverClass}"/>
          <property name="url" value="${prop.url}"/>
          <property name="username" value="${prop.userName}"/>
          <property name="password" value="${prop.password}"/>
      </bean>
      
    4. 获取使用DruidDataSource对象

      public void testOne(){
          ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
          DruidDataSource dataSource = context.getBean("dataSource", DruidDataSource.class);
          //jdbc:mysql://localhost:3306/mybatis
          System.out.println(dataSource.getUrl());
      }
      

    p 名称空间注入(了解):如果对于一个属性很多的实体类,那么property使用过于繁琐,p名称空间是一种简化的方法。

    第一步:添加p名称空间到配置文件xmlns:p="http://www.springframework.org/schema/p"

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

    第二步:进行属性注入,在 bean 标签里面进行操作

    <bean id="user1" class="com.th.pojo.User" p:age="20" p:name="TH"/>
    

    注意:p命名空间依赖于getter和setter方法注入,所以需要设置getter和setter方法。

  2. constructor-arg注入

    <!--方式一:使用下标的方式-->
    <bean id="user" class="com.th.pojo.User">
        <constructor-arg index="0" value="狂神"/>
    </bean>
    
    <!--方式二:通过参数类型-->
    <bean id="user" class="com.th.pojo.User">
        <constructor-arg type="java.lang.String" value="kunagshen"/>
    </bean>
    
    <!--方式三:通过属性名-->
        <!--name:可以设置别名,可以同时设置多个别名,空格、逗号,分号分隔-->
    <bean id="user" class="com.th.pojo.User" name="u1 u2,u3;u4">
        <constructor-arg name="name" value="KS"/>
    </bean>
    

    name: 类里面属性名称。

    value:向属性注入的值。

    type: 类属性的类型。

    index:类属性的下标(0表示第一个属性)。

    ref:标识引用Spring容器中已经创建好的的对象。

    • 通过参数类型注入属性只能对于一些简单的不含相同属性实体类,局限性较大。
    • 最推荐通过属性名进行属性注入。

    c 名称空间注入(了解)

    第一步:添加c名称空间到配置文件xmlns:c="http://www.springframework.org/schema/c""

    <beans xmlns="http://www.springframework.org/schema/beans"
           xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
           xmlns:c="http://www.springframework.org/schema/c"
           xsi:schemaLocation="http://www.springframework.org/schema/beans
            https://www.springframework.org/schema/beans/spring-beans.xsd">
    
    </beans>
    

    第二步:进行属性注入,在 bean 标签里面进行操作

    <bean id="user2" class="com.th.pojo.User" c:age="18" c:name="三淳" scope="prototype"/>
    

    注意:c命名空间依赖于构造器注入,所以需要设置有参构造方法。

  3. Bean的作用域

    https://docs.spring.io/spring-framework/docs/current/reference/html/core.html#beans-factory-scopes

    singleton:单例模式,Spring默认使用单例模式。加载 spring 配置文件时候就会创建单实例对象

    <bean id="accountService" class="com.something.DefaultAccountService" scope="singleton"/>
    

    prototype:原型模式,每次从容器中get的时候,都会产生新的对象。

    <bean id="accountService" class="com.something.DefaultAccountService" scope="prototype"/>
    

    requestsessionapplicationwebsocket在Web开发种使用。 在调用 getBean 方法时候创建多实例对象。

  4. Bean的自动装配

    Spring种有3种装配方式:

    1. 在xml中显示配置
    2. 在java中显示配置
    3. 隐式的自动装配bean : Spring在上下文种自动寻找,并自动给bean装配属性

    byName自动装配。 会自动在容器上下文中查找和自己对象set方法后面的值对应的bean的id!

    <!--这个id必须小写,否则报异常-->
    <bean id="cat" class="com.th.pojo.Cat"/>
    <bean id="dog" class="com.th.pojo.Dog"/>
    
    <bean id="man" class="com.th.pojo.People" autowire="byName">
    	<property name="name" value="ThreePure1"/>
        <!--其他对象属性在配置后可以自动装配-->
    </bean>
    

    注意: 使用byName需要保证所有的bean的id唯一,并且这个bean需要跟自动注入的类属性名称一样!

    byType自动装配。 会自动在容器上下文中查找,和自己对象属性类型相同的bean。

    <!--因为是按照类型装配,所以id可以省略-->
    <bean class="com.th.pojo.Cat"/>
    <bean id="dog" class="com.th.pojo.Dog"/>
    
    <bean id="man" class="com.th.pojo.People" autowire="byType">
        <property name="name" value="ThreePure2"/>
    </bean>
    

    注意: 使用byType需要保证所有的bean的class唯一,并且这个bean需要跟自动注入的属性的类型一致!

    使用注解实现自动装配 (见下一节)

2.4 Bean 管理——基于注解方式

  1. 使用注解须知

    在Spring4之后,使用注解开发必须要导入AOP包。导入spring-webmvc后包含了AOP的包。

    1. 导入约束

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

      http://www.springframework.org/schema/context

      https://www.springframework.org/schema/context/spring-context.xsd

    2. 配置注解的支持 <context:annotation-config/>

      <?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
              https://www.springframework.org/schema/beans/spring-beans.xsd
              http://www.springframework.org/schema/context
              https://www.springframework.org/schema/context/spring-context.xsd">
      
          <context:annotation-config/>
          <!--扫描指定的包,这个包下所有使用的注解就会生效-->
          <context:component-scan base-package="com.th"/>
      </beans>
      
    3. 开启组件扫描,配置扫描指定的包,使注解完全生效 ,多个包使用逗号隔开。

      <context:component-scan base-package="com.th"/>.

      当然也可以对扫描进行更加细致的配置:

      use-default-filters="false" :不使用默认的过滤器,可以自定义配置。

      context:include-filter标签: 设置扫描哪些内容,可以定义类型以及表达式。

      <!--扫描com.th包下的Controller注解-->
      <context:component-scan base-package="com.th" use-default-filters="false">
          <context:include-filter type="annotation" expression="org.springframework.stereotype.Controller"/>
      </context:component-scan>
      

      context:exclude-filter标签: 设置哪些内容不进行扫描,可以定义类型以及表达式。

      <!--扫描com.th包下的除Controller以外的注解-->
      <context:component-scan base-package="com.th">
          <context:exclude-filter type="annotation" expression="org.springframework.stereotype.Controller"/>
      </context:component-scan>
      
    4. 使用注解

  2. 创建对象

    @Component: 组件、放于类上,表示创建这个类的对象,相当于<bean id="user" class="com.th.pojo.User" />

    import org.springframework.beans.factory.annotation.Value;
    import org.springframework.context.annotation.Scope;
    import org.springframework.stereotype.Component;
    
    @Component
    public class User {
        //相当于:<property name="name" value="TH"/>   也可以放在setter方法前
        //@Value("TH")
        public String name;
    
        @Value("sanchun")
        public void setName(String name) {
            this.name = name;
        }
    }
    

    在注解里面@componentvalue 属性值可以省略不写,默认值是类名称,首字母小写。比如User类的user

  3. 注入属性

    public class User {
        //相当于:<property name="name" value="TH"/>   也可以放在setter方法前
        @Value("TH")
        public String name;
    }
    
    public String name;
    
    @Value("sanchun")
    public void setName(String name) {
        this.name = name;
    }
    

    对于复杂的数据注入到属性中,建议使用xml方式进行注入。

  4. Component的衍生的注解

    • dao层 @Repository
    • service层 @Service
    • controller @Controller

    这四个注解的功能一致,都是代表将某个类注册到Spring中,装配bean,只是后三者在Web开发中按照MVC架构分层特定使用。

  5. 注解实现自动装配 (@Autowired、@Qualifier、Resource) 一般时对象注入或者引用注入,而注入普通类型属性使用@Value

    在导入注解约束以及对注解支持的情况下使用@Autowired

    作用范围:属性前、setter方法前

    Autowired 我们可以不用编写Setter方法了,前提是你这个自动装配的属性在IOC(Spring)容器中存在,且符合byType类型。

    public @interface Autowired {
        boolean required() default true;
    }
    

    根绝以上源码,可以知道@Autowired注解可以传递参数,required用于设定装配属性是否允许为空。如果定义了@Autowiredrequired属性为false,说明这个属性可以为空,否则不能为空。

    @Nullable :有相似作用,字段标记了这个注解,说明这个字段可以为null。

    @Qualifier:当一个Spring容器中含有多个相同类的对象时,自动装配的环境较为复杂,可以结合使用@Qualifier注解来实现选取具体的某一个对象。其value值为确定的其中beanid值。典型案例就是一个接口含有多个实现类。

    Resource:java原生对自动装配的支持。默认使用byName实现,其次才是对byType的支持,若二则都不存在则会报错。Resourcename属性可以实现与@Qualifier类似的功能。

    <bean id="cat11" class="com.th.pojo.Cat"/>
    <!--    <bean id="cat22" class="com.th.pojo.Cat"/>-->
    <bean id="dog11" class="com.th.pojo.Dog"/>
    <bean id="dog22" class="com.th.pojo.Dog"/>
    
    <bean id="man" class="com.th.pojo.People">
    	<property name="name" value="ThreePure2"/>
    </bean>
    
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.beans.factory.annotation.Qualifier;
    import org.springframework.lang.Nullable;
    import javax.annotation.Resource;
    
    public class People {
    
        @Nullable
        private String name;
        @Resource(name = "cat11")
        private Cat cat;
        @Autowired
        @Qualifier(value = "dog22")
        private Dog dog;
    
        public String getName() {
            return name;
        }
    
        public void setName(String name) {
            this.name = name;
        }
    
        public Cat getCat() {
            return cat;
        }
    
    
        public Dog getDog() {
            return dog;
        }
    }
    
  6. 作用域 ( @Scope

    使用@Scope注解实现对作用域范围的控制,括号内直接输入具体的作用域并用""包起来。

    @Component
    @Scope("prototype")
    public class User {
    	……
    }    
    

2.5 使用JavaConfig实现配置

以上即便是使用注解方式也并没有完全脱离xml文件,而JavaConfig就是完全使用注解来实现。

  • 实体类中同样可以通过@Component注解来进行创建对象,通过@Value实现属性注入。

  • 需要一个配置类来替代applicationContext.xml的功能

    • @Configuration注解标识的类表示一个配置类
    • @Configuration注解标识的类也会被Spring容器托管,注册到容器中,因为他本来就是一个@Component
  • 需要配置类中的方法代替applicationContext.xmlbean标签进行bean的注册。

    • 这个方法的名字,就相当于bean标签的id属性
    • 这个方法的返间值,就相当bean标签中的class属性,也就是要注入到容器的对象!
  • 通过Annotationconfig 上下文来获收容器,通过配置类的class对象加载!AnnotationConfigApplicationContext

    ApplicationContext context = new AnnotationConfigApplicationContext(UserConfig.class);
    
  • 其他注解

    • @ComponentScan : 用于自动扫描包
    • @Import : 用于导入其他配置类

实体类

import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

@Component
public class User {
    private String name;

    public String getName() {
        return name;
    }

    @Value("THREEPURE")
    public void setName(String name) {
        this.name = name;
    }

    @Override
    public String toString() {
        return "User{" +
                "name='" + name + '\'' +
                '}';
    }
}

配置类

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.stereotype.Component;

//这个也会被Spring容器托管,注册到容器中,因为他本来就是一个@Component
//@Configuration代表这是一个配置类,相当于beans.xml/ applicationContext.xml
@Configuration
@ComponentScan("com.th.pojo")
@Import(UserConfig2.class)
public class UserConfig {

    //注册一个bean,就相当于我们之前写的一个bean标签
    //这个方法的名字,就相当于bean标签的id属性
    // 这个方法的返间值,就相当bean标签中的class属性
    @Bean
    public User getUser(){
        //就是返回要注入到bean的对象!
        return new User();
    }
}

测试类

public class MyTest {
    public static void main(String[] args) {
        //如果完全使用了配置类方式去做,我们就只能通过Annotationconfig 上下文来获收容器,通过配置类的class对象加载!
        ApplicationContext context = new AnnotationConfigApplicationContext(UserConfig.class);
        User getUser = context.getBean("getUser", User.class);
        System.out.println(getUser);
    }
}

2.6 Bean的生命周期

  1. 通过构造器创建 bean 实例(无参数构造)
  2. 为 bean 的属性设置值和对其他 bean 引用(调用 set 方法)
  3. 调用 bean 的初始化的方法(需要进行配置初始化的方法)
  4. bean 可以使用了(对象获取到了)
  5. 当容器关闭时候,调用 bean 的销毁的方法(需要进行配置销毁的方法)

测试

public class Orders {
    private String name;

    /**无参构造方法*/
    public Orders() {
        System.out.println("第一步 执行无参数构造创建 bean 实例");
    }

    public void setName(String name) {
        this.name = name;
        System.out.println("第二步 调用 set 方法设置属性值");
    }

    /**创建执行的初始化方法*/
    public void initMethod(){
        System.out.println("第三步,执行初始化方法");
    }

    /**创建执行的销毁的方法*/
    public void destroyMethod(){
        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"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
       http://www.springframework.org/schema/beans/spring-beans.xsd">

    <bean id="orders" class="com.th.pojo.Orders" init-method="initMethod" destroy-method="destroyMethod" >
        <property name="name" value="Phone"></property>
    </bean>

</beans>
import com.th.pojo.Orders;
import org.junit.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class TestOne {
    @Test
    public void test(){
        //ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
        ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
        Orders orders = context.getBean("orders", Orders.class);
        System.out.println(orders);
        System.out.println("第四步 获取创建 bean 实例对象");
        System.out.println(orders);
        //手动让 bean 实例销毁 (ClassPathXmlApplicationContext对象才能实现close操作)
        context.close();
    }
}

结果:

第一步 执行无参数构造创建 bean 实例
第二步 调用 set 方法设置属性值
第三步,执行初始化方法
com.th.pojo.Orders@1060b431
第四步 获取创建 bean 实例对象
第五步 执行销毁的方法

注意:需要使用ApplicationContext的实现类来实现手动销毁操作。

另外,Bean的完整生命周期还有包括两项在初始化之前的状态。实现这两个状态需要为Spring配置后置处理器。实现后置处理器需要创建一个类并实现BeanPostProcessor接口并重写postProcessBeforeInitializationpostProcessAfterInitialization两个方法,这两个方法对应这额外的两种状态。

public class MyBeanPostProcessor implements BeanPostProcessor {
    @Override
    public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
        System.out.println("在初始化方法之前执行的方法");
        return BeanPostProcessor.super.postProcessBeforeInitialization(bean, beanName);
    }

    @Override
    public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
        System.out.println("在初始化方法之后执行的方法");
        return BeanPostProcessor.super.postProcessAfterInitialization(bean, beanName);
    }
}

有了后置处理器后需要将其配置到Spring中。在Spring中,只需要将这个后置处理器像配置bean一样在applicationContext.xml中配置那么这个Spring容器中的全部bean都会加上后置处理器。

<bean id="myBeabPostProcessor" class="com.th.pojo.MyBeanPostProcessor" ></bean>

输出结果为:

第一步 执行无参数构造创建 bean 实例
第二步 调用 set 方法设置属性值
在初始化方法之前执行的方法
第三步,执行初始化方法
在初始化方法之后执行的方法
com.th.pojo.Orders@e720b71
第四步 获取创建 bean 实例对象
第五步 执行销毁的方法

3、AOP

3.1 什么是AOP

  • 面向切面编程(方面),利用 AOP 可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率。
  • 通俗地讲就是不修改原有代码实现新功能的添加
  • 案例:原有的数据增删改查前后添加日志功能。

Spring 框架一般都是基于 AspectJ 实现 AOP 操作。AspectJ 不是 Spring 组成部分,独立 AOP 框架,一般把 AspectJ 和 Spirng 框架一起使用,AOP 操作可以基于 xml 配置文件实现 或者基于注解方式实现(使用)。

3.2 AOP原理

AOP 底层使用动态代理,动态代理有两种,分别为JDK动态代理CGLIB动态代理

JDK动态代理 面向有接口的情况

  1. 原理:创建接口实现类代理对象,增强类的方法。

    使用:

    • java.lang.Object
      • java.lang.reflect.Proxy
        • newProxyInstance(ClassLoader loader, 类<?>[] interfaces, InvocationHandler h)
          • 第一参数,类加载器 。
          • 第二参数,增强方法所在的类,这个类实现的接口,支持多个接口 。
          • 第三参数,实现这个接口 InvocationHandler,创建代理对象,写增强的部分。
  2. 实例

    使用动态代理前:

    public interface UserDao {
        public int add(int a, int b);
        public String update(String id);
    }
    
    public class UserDaoImpl implements UserDao{
        @Override
        public int add(int a, int b) {
            System.out.println("add() executed");
            return a+b;
        }
    
        @Override
        public String update(String id) {
            System.out.println("update()executed");
            return id;
        }
    }
    

    使用动态代理增加功能:

    1. 第一步:创建代理对象类、 实现InvocationHandler接口。

      class UserDaoProxy implements InvocationHandler {
      
          private Object obj;
      
          /**把创建的是谁的代理对象,就把谁传递过来,通过有参构造传递。比如这里产生的是UserdaoImpl的代理对象。*/
          /**为例让这个代理类使用更加广泛,直接传递Object对象,*/
          public UserDaoProxy(Object obj) {
              this.obj = obj;
          }
      
          /**增强的逻辑代码*/
          @Override
          public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
              // 被增强方法之前
              System.out.println("It was executed before the "+method.getName()+",and the args :"+ Arrays.toString(args));
      
              // 被增强方法执行
              Object res = method.invoke(obj, args);
      
              // 被增强方法之后
              System.out.println("It was executed before ... :"+obj.toString());
      
              return res;
          }
      }
      
      1. 在创建代理对象代码时,需要代理哪个对象就传递哪个对象。通过代理类的有参构造实现代理对象的传递。

      2. 通过重写invoke方法,在其内部添加扩展功能。其中参数:

        1. proxy : 调用该方法的代理实例,
        2. method:执行的方法(需要增强功能的方法)
        3. orgs: 为这个方法传递的参数。
      3. 通过method.invoke(obj, args)方法执行原来UserDao中的方法,method就是传递过来的方法。可以在其之前或者之后对功能进行扩展,

    2. 第二步:使用Proxy类创建接口代理对象(获取代理实例执行方法)

      public class JDKProxy {
          public static void main(String[] args) {
              
              Class[] interfaces = {UserDao.class};
              UserDaoImpl userDao = new UserDaoImpl();
              
              //创建接口实现类的接口对象
              UserDao proxyDao = (UserDao) Proxy.newProxyInstance(JDKProxy.class.getClassLoader(), interfaces, new UserDaoProxy(userDao));
              //使用代理对象执行方法
              int addResult = proxyDao.add(1, 2);
              System.out.println("add method result:"+addResult);
          }
      }
      

      newProxyInstance()方法的三个参数:

      1. loader:类加载器定义代理类
      2. interfaces:被代理类实现的接口列表
      3. h:调度方法调用的调用处理函数(代理对象)

CGLIB动态代理 面向没有接口的情况

  1. 原理:创建子类的代理对象,增强类的方法。

    image-20220309211016410

3.3 术语

  1. 连接点:类中可以增强功能的方法。
  2. 切入点:最终实际进行了功能增强的方法。
  3. 通知(增强):增加的那部分功能。含有多种类型:
    1. 前置通知
    2. 后置通知
    3. 环绕通知
    4. 异常通知
    5. 最终通知
  4. 切面:是一个动作,把一个通知应用到切入点的过程。

3.4 切入点表达式

  • 作用:知道对哪个类里面的哪个方法进行增强
  • 语法结构: execution([权限修饰符] [返回类型] [类全路径].[方法名称]([参数列表]))
  • 权限修饰符可以用*代表全部权限,返回类型一般省略。

举例:

  1. 对 com.atguigu.dao.BookDao 类里面的 add 方法进行增强 。

    execution(* com.atguigu.dao.BookDao.add(..))

  2. 对 com.atguigu.dao.BookDao 类里面的所有的方法进行增强

    execution(* com.atguigu.dao.BookDao.* (..))

  3. 对 com.atguigu.dao 包里面所有类,类里面所有方法进行增强

    execution(* com.atguigu.dao.*.* (..))

3.5 AOP 操作

AspectJ 注解方式实现AOP

  1. 创建类(被增强功能的类),在类里面定义方法。

    public class User {
        public void add(){
            System.out.println("add method...");
        }
    }
    
  2. 创建增强类(编写增强逻辑)。

    public class UserProxy {
        public void before(){
            System.out.println("before...");
        }
    }
    
  3. 进行通知的配置。

    1. 在 spring 配置文件中,开启注解扫描。

      <!--开启注解扫描-->
      <context:component-scan base-package="com.th" />
      
    2. 使用注解创建 User 和 UserProxy 对象。@Component

      @Component
      public class User {
      }
      
      @Component
      public class UserProxy {
      }
      
    3. 在增强类上面添加注解 @Aspect。 @Aspect 用于生成代理对象

      @Component
      @Aspect
      public class UserProxy {
      }
      
    4. 在 spring 配置文件中开启生成代理对象。

      proxy-target-class默认状态为falseproxy-target-class="false"表示使用JDK动态代理proxy-target-class="true"表示使用cglib动态代理,

      <!--- 开启 Aspect 生成代理对象-->
      <aop:aspectj-autoproxy/>
      
  4. 配置不同类型的通知。

    在增强类里的通知方法上面添加通知类型注解,使用切入点表达式配置。

    @Component
    @Aspect
    public class UserProxy {
        /**前置通知 @Before 注解表示作为前置通知*/
        @Before("execution(* com.th.atguigu.aopanno.User.add(..))")
        public void before(){
            System.out.println("before...");
        }
    
        /**后置通知(返回通知)*/
        @AfterReturning(value = "execution(* com.th.atguigu.aopanno.User.add(..))")
        public void afterReturning() {
            System.out.println("afterReturning.........");
        }
    
        /**最终通知*/
        @After(value = "execution(* com.th.atguigu.aopanno.User.add(..))")
        public void after() {
            System.out.println("after.........");
        }
    
        /**异常通知*/
        @AfterThrowing(value = "execution(* com.th.atguigu.aopanno.User.add(..))")
        public void afterThrowing() {
            System.out.println("afterThrowing.........");
        }
    
        /**环绕通知*/
        @Around(value = "execution(* com.th.atguigu.aopanno.User.add(..))")
        public void around(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
            System.out.println("环绕之前.........");
            //被增强的方法执行
            proceedingJoinPoint.proceed();
            System.out.println("环绕之后.........");
        }
    }
    

    结果:

    环绕之前.........
    before...
    add method...
    afterReturning.........
    after.........
    环绕之后.........
        
    //如果发生异常:
    环绕之前.........
    before...
    afterThrowing.........
    after.........
    

    异常通知:只有在被增强方法内部发生异常时调用。

    最终通知:无论是否发生异常都会被执行。而后置通知如果发生异常就不会被执行。

  5. 相同的切入点抽取

    如上所示,有多种不同类型的通知,在每个通知上都需要相同的注解表达式会比较麻烦,那么可以抽取公共部分简化操作。在通知的注解上使用配置了的方法名即可。

    @Pointcut("execution(* com.th.atguigu.aopanno.User.add(..))")
    public void pointDemo(){
    
    }
    
    /**前置通知 @Before 注解表示作为前置通知*/
    @Before("pointDemo()")
    public void before(){
        System.out.println("before...");
    }
    
  6. 设置增强类优先级

    当有多个增强类对同一个方法进行增强时,可以设置增强类的优先级。

    在增强类上面添加注解 @Order(数字类型值)数字类型值越小优先级越高

    @Component
    @Aspect
    @Order(0)
    public class PersonProxy {
        @Before("execution(* com.th.atguigu.aopanno.User.add(..))")
        public void beforePerson(){
            System.out.println("before of PersonProxy");
        }
    }
    
    before of PersonProxy
    环绕之前.........
    before...
    add method...
    afterReturning.........
    after.........
    环绕之后.........
    
  7. 完全使用注解开发 (创建配置类,不需要创建 xml 配置文件)

    @Configuration
    @ComponentScan(basePackages = {"com.atguigu"})
    @EnableAspectJAutoProxy(proxyTargetClass = true)
    public class ConfigAop {
    }
    

    @EnableAspectJAutoProxy(proxyTargetClass = true)相当于<aop:aspectj-autoproxy/>

AspectJ XML配置文件实现AOP

  1. 创建两个类,增强类和被增强类,创建方法 。

  2. 在 spring 配置文件中(使用bean)创建两个类对象。

    <!--创建增强类和被增强类对象-->
    <bean id="book" class="com.th.atguigu.aopxml.Book"/>
    <bean id="bookProxy" class="com.th.atguigu.aopxml.BookProxy"/>
    
  3. 在 spring 配置文件中配置切入点。

    <!--配置 aop 增强-->
    <aop:config>
        <!--切入点-->
        <aop:pointcut id="p" expression="execution(* com.th.atguigu.aopxml.Book.buy(..))"/>
        <!--配置切面-->
        <aop:aspect ref="bookProxy">
            <!--增强作用在具体的方法上-->
            <aop:before method="before" pointcut-ref="p"/>
        </aop:aspect>
    </aop:config>
    
  4. 测试

    public void testTwo(){
        ApplicationContext context = new ClassPathXmlApplicationContext("beanXml.xml");
        Book book = context.getBean("book", Book.class);
        book.buy();
    }
    

4、整合数据库

4.1 整合mybatis

Mybatis使用步骤:

  1. 导入依赖(mybatismysql-connector-java
  2. 配置mybatis(mybatis-config.xml)
  3. 构建工具类,获取SqlSessionFactory和SqlSession
  4. 编写实体类(可使用lombok
  5. Dao接口
  6. 接口实现类Impl【XXXMapper.xml】
  7. 在Mybatis核心配置文件中注册每一个Mapper.xml
  8. 测试

整合

  1. 导包

    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <version>5.1.47</version>
    </dependency>
    
    <dependency>
        <groupId>org.mybatis</groupId>
        <artifactId>mybatis</artifactId>
        <version>3.5.3</version>
    </dependency>
    
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-webmvc</artifactId>
        <version>5.2.9.RELEASE</version>
    </dependency>
    
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-jdbc</artifactId>
        <version>5.1.9.RELEASE</version>
    </dependency>
    
    <dependency>
        <groupId>org.aspectj</groupId>
        <artifactId>aspectjweaver</artifactId>
        <version>1.9.4</version>
    </dependency>
    
    <!-- https://mvnrepository.com/artifact/org.mybatis/mybatis-spring -->
    <dependency>
        <groupId>org.mybatis</groupId>
        <artifactId>mybatis-spring</artifactId>
        <version>2.0.3</version>
    </dependency>
    

    mybatis-spring:将 MyBatis 代码无缝地整合到 Spring 中,允许 MyBatis 参与到 Spring 的事务管理之中,创建映射器 mapper 和 SqlSession 并注入到 bean 中,以及将 Mybatis 的异常转换为 Spring 的 DataAccessException。 最终,可以做到应用代码不依赖于 MyBatis,Spring 或 MyBatis-Spring。

    http://mybatis.org/spring/zh/index.html

  2. 配置mybatis数据源

    数据源:用Spring的数据源代替Mybatis的配置 ——————org.springframework.jdbc.datasource

    <bean id="DataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
        <property name="driverClassName" value="com.mysql.jdbc.Driver"/>
        <property name="url" value="jdbc:mysql://localhost:3306/mybatis?useSSL=true&amp;useUnicode=true&amp;characterEncoding=UTF-8"/>
        <property name="username" value="root"/>
        <property name="password" value="mysqlpw"/>
    </bean>
    

    数据源的形式是多样的,也可以使用其他形式对数据源进行配置。

  3. 获取SqlSessionFactory

    <!--sqlSessionFactory-->
    <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
        <property name="dataSource" ref="DataSource" />
        <!--绑定mybatis的配置文件-->
        <property name="configLocation" value="classpath:mybatis-config.xml"/>
        <property name="mapperLocations" value="classpath:com/th/mapper/*.xml"/>
    </bean>
    

    可以在这个SqlSessionFactoryBean配置Mybatis的一些其他配置(别名等)。最主要的是可以直接导入Mybatis的原有配置文件mybatis-config.xml,两种配置可以配合使用。

    mapperLocations获取Mapper的路径,相当于在Mybatis对Mappper的注册。

  4. 获取和SqlSessionSqlSessionTemplate:就是mybatis中的sqlSession。

    <bean id="sqlSession" class="org.mybatis.spring.SqlSessionTemplate">
        <!--只能通过构造器注入sqlSessionFactory,因为它没有set方法-->
        <constructor-arg index="0" ref="sqlSessionFactory"/>
    </bean>
    
  5. 编写实体类

  6. Dao接口

  7. 接口实现类XXXMapper.xmlImpl实现类,

    Impl实现类是必须的,而在Mybatis中实现类不需要,在Spring中Impl实现类是必须的,用于获取sqlSessionTemplate

    public class UserMapperImpl implements UserMapper{
    
        //我们的所有操作,都用sqlSession来执行,在原来,现在使用SqlSessionTemplate;
        private SqlSessionTemplate sqlSessionTemplate;
    
        public void setSqlSession(SqlSessionTemplate sqlSession) {
            this.sqlSessionTemplate = sqlSession;
        }
    
        @Override
        public List<User> selectUserList() {
            UserMapper mapper = sqlSessionTemplate.getMapper(UserMapper.class);
            return mapper.selectUserList();
        }
    }
    

    当然Impl实现类也可以通过继承SqlSessionDaoSupport类,通过调用 getSqlSession() 方法得到一个 SqlSessionTemplate来执行 SQL 方法。

    public class UserMapperImpl2 extends SqlSessionDaoSupport implements UserMapper{
        @Override
        public List<User> selectUserList() {
            return getSqlSession().getMapper(UserMapper.class).selectUserList();
        }
    }
    
  8. Spring配置文件中创建实现类对象。

    <bean id="userMapperImpl" class="com.th.mapper.UserMapperImpl">
        <property name="sqlSession" ref="sqlSessionTemplate"/>
    </bean>
    

    通过继承SqlSessionDaoSupport类的Impl实现类那么在创建其对象时需要传递sqlSessionFactory

    <bean id="userMapperImpl2" class="com.th.mapper.UserMapperImpl2">
        <property name="sqlSessionFactory" ref="sqlSessionFactory" />
    </bean>
    
  9. 通过实现类对象进行业务操作

    public void testSelectUserList() throws IOException {
        ApplicationContext context = new ClassPathXmlApplicationContext("spring-dao.xml");
        UserMapper userMapper = context.getBean("userMapperImpl2", UserMapper.class);
        for (User user : userMapper.selectUserList()) {
            System.out.println(user);
        }
    }
    

5、声明式事务管理

5.1 什么事务

事务是数据库操作最基本单元,逻辑上一组操作,要么都成功,如果有一个失败所有操作都失败。

事务分类:

  • 声明式事物:通过AOP实现。
  • 编程式事物:在代码中进行事务管理。

Spring均支持这两种事物,但是推荐使用声明式事物。

5.2、事务四个特性(ACID)

  1. 原子性
  2. 一致性
  3. 隔离性
  4. 持久性

5.3 Spring中的事务管理

Spring提供一个接口PlatformTransactionManager,代表事务管理器,这个接口针对不同的框架提供不同的实现类,DataSourceTransactionManagermybatisJdbcTemplate提供使用。

1、基于注解方式

  1. 在 spring 配置文件配置事务管理器

    要开启 Spring 的事务处理功能,在 Spring 的配置文件中创建一个 DataSourceTransactionManager 对象:

    <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <!--注入数据源-->
        <property name="dataSource" ref="dataSource"/>
    </bean>
    
  2. 在 spring 配置文件,开启事务注解

    1. 在 spring 配置文件引入名称空间 tx

    2. 开启事务注解

      <tx:annotation-driven transaction-manager="transactionManager"></tx:annotation-driven>
      
  3. 在 service 类上面(或者 service 类里面方法上面)添加事务注解@Transactional

    @Transactional:这个注解作用在类上,也可以作用在方法上。

    @Service
    @Transactional()
    public class AccountService {
    }
    

    声明式事务管理参数配置,@Transactional()属性参数的设置。

    • propagation:事务传播行为

      多事务方法(对数据库表数据进行变换的操作)直接进行调用,这个过程中事务是如何进行管理的。如一个事物方法调用另一个事务方法,这个被调用的事务方法是否进行事务管理。Spring有7种事务传播行为。

      @Transactional(propagation = Propagation.REQUIRED)
      
    • ioslation:事务隔离级别

      事务具有隔离性,不考虑隔离性可能会产生脏读不可重复读虚(幻)读问题。

      脏读:一个未提交事务读取到另一个未提交事务的数据。

      不可重复读:一个未提交事务读取到另一提交事务修改数据。

      虚读:一个未提交事务读取到另一提交事务添加数据。

      Spring通过设置事务隔离级别,解决读问题:

      脏读 不可重复读 幻读
      READ_UNCOMMITTED
      READ_COMMITTED
      REPEATABLE_READ
      SERIALIZABLE
      @Transactional(isolation = Isolation.SERIALIZABLE)
      
    • timeout:超时时间

      • 事务需要在一定时间内进行提交,如果不提交则进行回滚 。

      • 默认值是 -1(不超时) ,设置时间以秒单位进行计算。

        @Transactional(timeout = -1)
        
    • readOnly:是否只读

      • 读:查询操作,写:添加修改删除操作

      • readOnly 默认值 false,表示可以查询,可以添加修改删除操作

      • 设置 readOnly 值是 true,设置成 true 之后,只能查询.

        @Transactional(readOnly = true)
        
    • rollbackFor:回滚

      • 设置出现哪些异常进行事务回滚
    • noRollbackFor:不回滚

      • 设置出现哪些异常不进行事务回滚

2、基于 xml 配置文件方式

  1. 在 spring 配置文件配置事务管理器

    <!--开启 Spring 的事务处理功能-->
    <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <constructor-arg ref="dataSource" />
    </bean>
    
  2. 配置事物通知,结合AOP实现事物的织入

    <tx:advice id="txAdvice" transaction-manager="transactionManager">
        <!--给哪些方法配置事物-->
        <!--new 配置事物的传播特性:propagation-->
        <tx:attributes>
            <tx:method name="add" propagation="REQUIRED"/>
            <tx:method name="delete" propagation="REQUIRED"/>
            <tx:method name="update" propagation="REQUIRED"/>
            <tx:method name="query" read-only="true"/>
            <tx:method name="*" propagation="REQUIRED"/>
        </tx:attributes>
    </tx:advice>
    
  3. 配置事物切入

    <aop:config>
        <aop:pointcut id="txPointCut" expression="execution(* com.th.kuang.mapper.*.*(..))"/>
        <aop:advisor advice-ref="txAdvice" pointcut-ref="txPointCut" />
    </aop:config>
    

3、完全注解开发

@Configuration //配置类
@ComponentScan(basePackages = "com.th.atguigu") //组件扫描
@EnableTransactionManagement //开启事务
public class TxConfig {
    //创建数据库连接池
    @Bean
    public DriverManagerDataSource getDruidDataSource() {
        DriverManagerDataSource dataSource = new DriverManagerDataSource();
        dataSource.setDriverClassName("com.mysql.jdbc.Driver");
        dataSource.setUrl("jdbc:mysql://localhost:3306/mybatis?useSSL=true&amp;useUnicode=true&amp;characterEncoding=UTF-8");
        dataSource.setUsername("root");
        dataSource.setPassword("mysqlpw");
        return dataSource;
    }
    //创建 JdbcTemplate 对象
    @Bean
    public JdbcTemplate getJdbcTemplate(DriverManagerDataSource dataSource) {
        //到 ioc 容器中根据类型找到 dataSource
        JdbcTemplate jdbcTemplate = new JdbcTemplate();
        //注入 dataSource
        jdbcTemplate.setDataSource(dataSource);
        return jdbcTemplate;
    }
    //创建事务管理器
    @Bean
    public DataSourceTransactionManager
    getDataSourceTransactionManager(DriverManagerDataSource dataSource) {
        DataSourceTransactionManager transactionManager = new DataSourceTransactionManager();
        transactionManager.setDataSource(dataSource);
        return transactionManager;
    }
}

关于No qualifying bean of type 'org.springframework.transaction.PlatformTransactionManager' available: expected single matching bean but found 2: transactionManager,getDataSourceTransactionManager报错问题。

由于在原来注解方式进行事务管理时存在一个数据源和事务管理器,@Transactional()注解时不直接指定,Spring就不知道具体使用哪一个事务管理器来进行事务管理了。使用只要@Transactional(transactionManager = “aaaTransactionManager”) 来进行指定具体的事务管理即可,当然最直接的方法是直接删除原来的事务管理。

6、Spring5 框架新功能

6.1 基础

整个 Spring5 框架的代码基于 Java8,运行时兼容 JDK9,许多不建议使用的类和方 法在代码库中删除

  1. Spring 5.0 框架自带了通用的日志封装。

    Spring5 已经移除 Log4jConfigListener,官方建议使用 Log4j2

  2. Spring5 框架核心容器支持@Nullable 注解

    @Nullable 注解可以使用在方法、属性,参数上,分别表示方法返回可以为空,属性值可以为空,参数值可以为空 。

  3. Spring5 核心容器支持函数式风格 GenericApplicationContext

    //函数式风格创建对象,交给 spring 进行管理
    @Test
    public void testGenericApplicationContext() {
        //1 创建 GenericApplicationContext 对象
        GenericApplicationContext context = new GenericApplicationContext();
        //2 调用 context 的方法对象注册
        context.refresh();
        context.registerBean("user1",User.class,() -> new User());
        //3 获取在 spring 注册的对象
        // User user = (User)context.getBean("com.atguigu.spring5.test.User");
        User user = (User)context.getBean("user1");
        System.out.println(user);
    }
    
  4. Spring5 支持整合 JUnit4和 JUnit5

    @ExtendWith(SpringExtension.class)
    @ContextConfiguration("classpath:bean1.xml")
    public class JTest5 {
        @Autowired
        private UserService userService;
        @Test
        public void test1() {	
        	userService.accountMoney();
        }
    }
    
    //使用一个复合注解替代上面两个注解完成整合
    @SpringJUnitConfig(locations = "classpath:bean1.xml")
    public class JTest5 {
        @Autowired
        private UserService userService;
        @Test
        public void test1() {
        	userService.accountMoney();
        }
    }
    

6.1 Webflux

1、SpringWebflux 介绍

  • 是 Spring5 添加新的模块,用于 web 开发的,功能和 SpringMVC 类似的,Webflux 使用 当前一种比较流行的响应式编程出现的框架

  • 使用传统 web 框架,比如 SpringMVC这些基于 Servlet 容器,Webflux 是一种异步非阻塞的框架,异步非阻塞的框架在 Servlet3.1 以后才支持,核心是基于 Reactor 的相关 API 实现 的。

  • 异步和同步针对调用者,调用者发送请求,如果等着对方回应之后才去做其他事情就是同步,如果发送请求之后不等着对方回应就去做其他事情就是异步。

  • 阻塞和非阻塞针对被调用者,被调用者受到请求之后,做完请求任务之后才给出反馈就是阻塞,受到请求之后马上给出反馈然后再去做事情就是非阻塞。

  • Webflux 特点:

    • 非阻塞式:在有限资源下,提高系统吞吐量和伸缩性,以Reactor为基础实现响应式编程。
    • 函数式编程:Spring5 框架基于 java8,Webflux 使用 Java8 函数式编程方式实现路由请求
  • 比较 SpringMVC

    • 两个框架都可以使用注解方式,都运行在 Tomet 等容器中。
    • SpringMVC 采用命令式编程(代码一行一行执行),Webflux 采用异步响应式编程

2、响应式编程

响应式编程编程RP,即Reactive Programming,响应式编程是一种面向数据流和变化传播编程范式。这意味着可以在编程语言中很方便地表达静态或动态的数据流,而相关的计算模型会自动将变化的值通过数据流进行传播。

电子表格程序就是响应式编程的一个例子。单元格可以包含字面值或类似"=B1+C1"的公 式,而包含公式的单元格的值会依据其他单元格的值的变化而变化。

响应式编程的思想是观察者模式。Java8 及其之前版本提供的观察者模式两个类Observer Observable

posted @ 2022-03-28 17:40  三淳  阅读(159)  评论(1编辑  收藏  举报