Spring

1. Spring

1.1 简介

  • 2002年,首次推出了Spring框架的雏形:interface21框架
  • 2004年3月24日,Spring框架即以interface21框架为基础,正式发布了1.0版本
  • Rod Johnson,Spring Framework创始人,著名作者。 Rod在悉尼大学不仅获得了计算机学位,同时还获得了音乐学位。
  • spring理念:使现有的技术更加容易使用,简化复杂的企业级开发。
  • SSH: Struct2+Spring+Hibernate
  • SSM:SpringMVC+Spring+Mybatis
  • 官网:https://spring.io/
  • 官方下载地址:http://repo.spring.io/release/org/springframework/spring
  • GitHub:https://github.com/spring-projects/spring-framework
  • Maven

    <!-- https://mvnrepository.com/artifact/org.springframework/spring-webmvc -->
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-webmvc</artifactId>
        <version>5.2.6.RELEASE</version>
    </dependency>
    <!-- https://mvnrepository.com/artifact/org.springframework/spring-jdbc -->
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-jdbc</artifactId>
        <version>5.2.6.RELEASE</version>
    </dependency>

1.2 优点

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

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

1.3 弊端

发展了太久之后,违背了原来的理念,配置十分繁琐,人称:“配置地狱!”

1.4 组成

1219227-20170930225010356-45057485.gif (555×288)


2. IOC理论推导

  1. UserDao接口
  2. UserDaoImpl实现类
  3. UserService业务接口
  4. UserServiceImpl业务实现类

在我们之前的业务中,用户的需求可能会影响我们原来的代码,我们需要根据用户的需求去修改原代码!如果程序代码量十分大,修改一次的成本代价十分昂贵!

在这里插入图片描述

我们使用一个set接口实现

public class UserServiceImpl implements UserService {

    private UserMapper userMapper;

    public UserMapper getUserMapper() {
        return userMapper;
    }

    public void setUserMapper(UserMapper userMapper) {
        this.userMapper = userMapper;
    }

    public void getUesr() {
        userMapper.getUser();
    }
}

在调用时

public static void main(String[] args) {
    UserService userService = new UserServiceImpl();
    ((UserServiceImpl)userService).setUserMapper(new UserMapperOracleImpl());
    userService.getUesr();
}

由用户传入一个UserDao实现类对象,根据用户需求,动态改变实现类

之前,程序是主动创建对象的,控制权在程序员手上

使用了set注入之后,程序不再具有主动性,而是变成了被动的接受对象

这是IOC的原型

在这里插入图片描述

IOC本质

控制反转(Inversion of Control),是一种设计思想,DI(依赖注入)是实现IoC的一种方法,也有人认为DI是IOC的另一种说法。没有IOC的程序中,我们使用面向对象编程,对象的创建与对象间的依赖关系完全硬编码在程序中,对象的创建由程序自己控制,控制反转后将对象的创建转移给第三方,个人认为所谓的控制反转就是:获得依赖对象的方式反转了。

在这里插入图片描述

Spring容器在初始化时先读取配置文件,根据配置文件或元数据创建与组织对象存入容器中,程序使用时再从IOC容器中取出需要的对象

采用XML方式配置Bean的时候,Bean的定义信息是和实现分离的,而采用注解的方式可以把两者合为一体,Bean的定义信息直接以注解的形式定义在实现类中,从而达到了零配置的目的

控制反转是一种通过描述(XML或注解)并通过第三方去生产或获取特定对象的方式。在Spring中实现控制反转的是IOC容器,其实现方法是依赖注入(Dependency Injection,DI)


3. HelloSpring

  • 写了一个实体类

    public class Hello {
        private String str;
    
        public String getStr() {
            return str;
        }
    
        public void setStr(String str) {
            this.str = str;
        }
    
        @Override
        public String toString() {
            return "Hello{" +
                    "str='" + str + '\'' +
                    '}';
        }
    }
  • 编写配置文件 beans.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">
    
        <!--使用spring创建对象,在Spring中这些都称为Bean
            类型 变量名 = new 类型();
            Hello hello = new Hello();
            bean = 对象     new Hello();
    
            id = 变量名
            class = new 对象;
            property 相当于给对象中的属性设置一个值
        -->
        <bean id="hello" class="cn.codewei.pojo.Hello">
            <property name="str" value="Spring"/>
        </bean>
    </beans>

使用Spring创建对象,在Spring中这些都称为Bean

在之前:类型 变量名 = new 类型();

​ Hello hello = new Hello();

现在:bean 等于是一个对象

​ id:变量名

​ class:new 对象 要创建的对象的全类名

​ property:相当于给对象中的属性设置一个值

​ name:属性名 value:属性值

  • 测试

    public class MyTest {
        public static void main(String[] args) {
            // 获取Spring的上下文对象
            ApplicationContext context = new ClassPathXmlApplicationContext("beans.xml");
            // 我们的对象现在都在Spring中管理了,我们要使用,直接去里面取出来就可以了!
            Hello hello = (Hello) context.getBean("hello");
            System.out.println(hello.toString());
        }
    }
![在这里插入图片描述](https://img-blog.csdnimg.cn/20200506115619355.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80NTY4MjA1Mw==,size_16,color_FFFFFF,t_70#pic_center)

测试结果

![在这里插入图片描述](https://img-blog.csdnimg.cn/20200506115633206.png#pic_center)

4. IOC创建对象的方式

4.1 无参构造创建对象,默认!

  • 编写了一个实体类

    public class User {
        private String name;
    
        public User(){
            System.out.println("User的无参构造!");
        }
    
        public void show(){
            System.out.println("name = " + name);
        }
    
        public String getName() {
            return name;
        }
    
        public void setName(String name) {
            this.name = name;
        }
    }
  • 编写配置文件beans.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">
    
        <bean id="User" class="cn.codewei.pojo.User">
            <property name="name" value="唐嫣"/>
        </bean>
    </beans>
  • 测试

    public class MyTest {
        public static void main(String[] args) {
            ApplicationContext context = new ClassPathXmlApplicationContext("beans.xml");
        }
    }
  • 测试结果

    在这里插入图片描述

在读取配置文件,获取Spring上下文对象的时候,对象就已经创建

  • 当去除无参构造方法时(定义有参构造进行覆盖),就会报错!

    public class User {
        private String name;
    
        public User(String name){
    
        }
    
        public void show(){
            System.out.println("name = " + name);
        }
    
        public String getName() {
            return name;
        }
    
        public void setName(String name) {
            this.name = name;
        }
    }
![在这里插入图片描述](https://img-blog.csdnimg.cn/20200506115704861.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80NTY4MjA1Mw==,size_16,color_FFFFFF,t_70#pic_center)

4.2 有参构造创建对象

  • 编写实体类

    public class User {
        private String name;
    
        public User(String name){
            this.name = name;
        }
    
        public void show(){
            System.out.println("name = " + name);
        }
    
        public String getName() {
            return name;
        }
    
        public void setName(String name) {
            this.name = name;
        }
    }
  • 编写配置文件beans.xml

方式一:下标赋值

<bean id="User" class="cn.codewei.pojo.User">
    <constructor-arg index="0" value="唐嫣"/>
</bean>

通过参数的索引,为参数赋值

方式二:通过类型赋值

<bean id="User" class="cn.codewei.pojo.User">
    <constructor-arg type="java.lang.String" value="刘诗诗"/>
</bean>

通过参数类型进行赋值,如果参数类型有重复时,比如两个String类型的参数,这种构造方式就不可以,所以该方法不推荐使用

方式三:参数名赋值

<bean id="User" class="cn.codewei.pojo.User">
    <constructor-arg name="name" value="杨颖"/>
</bean>

直接通过参数名,进行赋值


5. Spring配置

5.1 别名

<!--name: 要取别名的id   alias: 别名-->
<alias name="User" alias="user"/>

在这里插入图片描述

5.2 Bean的配置

<bean id="User" class="cn.codewei.pojo.User" name="user2 u2,u3;u4">
    <constructor-arg name="name" value="杨颖"/>
</bean>

id:bean的唯一标识符,也就是相当于对象名

class:bean对象所对应的类型全限定名(全类名,包名+类名)

name:也是别名,而且name可以同时取多个别名,以空格,逗号或分号分隔

5.3 import

这个import,一般用于团队开发使用,它可以将多个配置文件,导入合并为一个

假设,现在的项目中有多个人开发,这三个人负责不同的类开发,不同的类需要注册在不同的bean中,我们可以利用import,将所有人的beans.xml合并为一个总的xml配置,使用的时候直接使用总的配置就可以了!

  • 张三
  • 李四
  • 王五
  • applicationContext.xml

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

6. 依赖注入DI

6.1 构造器注入

就是上面所提到的

<constructor-arg index="0" value="唐嫣"/>
<constructor-arg type="java.lang.String" value="刘诗诗"/>
<constructor-arg name="name" value="杨颖"/>

三种有参构造创建对象的注入方式

6.2 Set方式注入(重点)

  • 依赖注入:本质Set注入!

    • 依赖:bean对象的创建依赖于容器
    • 注入:bean对象中的所有属性由容器来注入

【环境搭建】

  1. 复杂类型

    public class Address {
        private String address;
    
        @Override
        public String toString() {
            return "Address{" +
                    "address='" + address + '\'' +
                    '}';
        }
        public String getAddress() {
            return address;
        }
        public void setAddress(String address) {
            this.address = address;
        }
    }
  1. 真实测试对象

    public class Student {
        private String name;
        private Address address;
        private String[] books;
        private List<String> hobbys;
        private Map<String,String> card;
        private Set<String> games;
        private String wife;
        private Properties info;
        Getter Setter
        toString..
        ....
    }
  2. 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">
    
        <bean id="address" class="cn.codewei.pojo.Address">
            <property name="address" value="山东"/>
        </bean>
    
        <bean id="student" class="cn.codewei.pojo.Student">
            <!--第一种,普通值注入,value-->
            <property name="name" value="唐嫣"/>
        </bean>
    </beans>
  3. 测试类

    public class MyTest {
        public static void main(String[] args) {
            ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
            Student student = (Student)context.getBean("student");
            System.out.println(student.getName());
        }
    }

完善注入信息

<?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">

    <bean id="address" class="cn.codewei.pojo.Address">
        <property name="address" value="山东"/>
    </bean>

    <bean id="student" class="cn.codewei.pojo.Student">
        <!--第一种,普通值注入,value-->
        <property name="name" value="唐嫣"/>

        <!--第二种,Bean注入,ref-->
        <property name="address" ref="address"/>

        <!--第三种,数组注入,array标签-->
        <property name="books">
            <array>
                <value>红楼梦</value>
                <value>西游记</value>
                <value>水浒传</value>
                <value>三国演义</value>
            </array>
        </property>

        <!--第四种,List注入,list标签-->
        <property name="hobbys">
            <list>
                <value>听歌</value>
                <value>跳舞</value>
            </list>
        </property>

        <!--第五种,Map注入,map标签 -> entry标签 -->
        <property name="card">
            <map>
                <entry key="身份证" value="123456789"/>
                <entry key="银行卡" value="789456123"/>
            </map>
        </property>

        <!--第六种,Set注入,value-->
        <property name="games">
            <set>
                <value>英雄联盟</value>
                <value>穿越火线</value>
                <value>绝地求生</value>
            </set>
        </property>

        <!--第七种,Null值注入-->
        <property name="wife">
            <null/>
        </property>

        <!--第八种,Properties注入,props标签 -> prop标签 -->
        <property name="info">
            <props>
                <prop key="学号">181051020220</prop>
                <prop key="性别">女</prop>
                <prop key="年龄">33</prop>
            </props>
        </property>
    </bean>
</beans>

6.3 拓展方式注入

新建一个实体类

public class User {
    private String name;
    private int age;
    public User() {
    }
    public User(String name, int age) {
        this.name = name;
        this.age = age;
    }
    @Override
    public String toString() {
        return "User{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public int getAge() {
        return age;
    }
    public void setAge(int age) {
        this.age = age;
    }
}

1. p命名空间注入

导入p命名空间

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

可以直接注入属性的值,p相当于property

<bean id="user" class="cn.codewei.pojo.User" p:name="唐嫣" p:age="18"/>

p命名空间对应的是set注入

2. c命名空间注入

导入c命名空间

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

c命名空间对应的是构造器注入,使用c命名空间注入,必须要有有参构造

<!--c命名空间注入-->
<bean id="user2" class="cn.codewei.pojo.User" c:name="唐嫣" c:age="32"/>

6.4 Bean的作用域

在这里插入图片描述

1. singleton:单例模式(Spring默认机制)

<bean id="user" class="cn.codewei.pojo.User" scope="singleton">
    <property name="name" value="唐嫣"/>
    <property name="age"  value="23"/>
</bean>
@Test
public void mytest(){
    ApplicationContext context = new ClassPathXmlApplicationContext("userbeans.xml");
    User user = (User)context.getBean("user");
    User user2 = context.getBean("user", User.class);
    System.out.println(user==user2);
}

测试结果

在这里插入图片描述

通过同一个bean,从容器中得到的对象都是同一个,因为在获得spring上下文对象ApplicationContext的时候对象就已经创建了

2. prototype:原型模式

<bean id="user" class="cn.codewei.pojo.User" scope="prototype">
    <property name="name" value="唐嫣"/>
    <property name="age"  value="23"/>
</bean>
@Test
public void mytest(){
    ApplicationContext context = new ClassPathXmlApplicationContext("userbeans.xml");
    User user = (User)context.getBean("user");
    User user2 = context.getBean("user", User.class);
    System.out.println(user==user2);
}

测试结果

在这里插入图片描述

通过同一个bean,从容器中getBean得到的对象都是不同的

3. 其余的request,session,application这些只能在web开发中使用到!


7. Bean的自动装配

  • 自动装配是Spring满足bean依赖一种方式!
  • Spring会在上下文自动寻找,并自动给bean装配属性

在Spring中有三种装配的方式:

  1. 在xml中显示的配置
  2. 在java中显示配置
  3. 隐式的自动装配Bean【重点】

7.1 测试

环境搭建:一个人有两个宠物!

public class Dog {
    public void shout(){
        System.out.println("wang~wang~wang~");
    }
}
public class Cat {
    public void shout(){
        System.out.println("miao~miao~miao~");
    }
}
public class Person {
    private Dog dog;
    private Cat cat;
    private String name;
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    @Override
    public String toString() {
        return "Person{" +
                "dog=" + dog +
                ", cat=" + cat +
                ", name='" + name + '\'' +
                '}';
    }
    public Dog getDog() {
        return dog;
    }
    public void setDog(Dog dog) {
        this.dog = dog;
    }
    public Cat getCat() {
        return cat;
    }
    public void setCat(Cat cat) {
        this.cat = cat;
    }
}

7.2 byName自动装配

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

<bean id="dog" class="cn.codewei.pojo.Dog"/>
<bean id="cat" class="cn.codewei.pojo.Cat"/>
<bean id="person" class="cn.codewei.pojo.Person" autowire="byName">
    <property name="name" value="唐嫣"/>
</bean>

当容器中id没有和自己对象set方法后面的值对应时,就会装配失败!

7.3 byType自动装配

byType:会自动在容器上下文中查找,和自己对象属性类型相同的Bean,在容器中,只要有和自己对象属性类型相同的bean就会自动装配,但容器中有多个和自己属性类型相同时,就会报错

<bean id="dog" class="cn.codewei.pojo.Dog"/>
<bean id="cat" class="cn.codewei.pojo.Cat"/>
<bean id="person" class="cn.codewei.pojo.Person" autowire="byType">
    <property name="name" value="唐嫣"/>
</bean>

小结:

  • byName的时候,需要保证所有bean的id唯一,并且这个bean需要和自动注入的set后面的值一致!
  • byType的时候,需要保证bean的class唯一,并且这个bean需要和自动注入的属性的类型一致!

7.4 使用注解实现自动装配

jdk1.5支持注解,Spring2.5支持注解

要使用注解须知:

  1. 导入约束和约束的支持
  2. 配置注解的支持

    <?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/>
    
    </beans>

@Autowired

是通过byType的方式进行自动装配的,直接在属性或set方法上使用即可!

使用Autowired注解是可以省略set方法的,前提是你这个自动装配的属性在IOC(Spring)容器中存在且符合类型byType

<context:annotation-config/>

<bean id="dog" class="cn.codewei.pojo.Dog"/>
<bean id="cat" class="cn.codewei.pojo.Cat"/>
<bean id="person" class="cn.codewei.pojo.Person"/>
public class Person {
    @Autowired
    private Dog dog;
    @Autowired
    private Cat cat;
    private String name;
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    @Override
    public String toString() {
        return "Person{" +
                "dog=" + dog +
                ", cat=" + cat +
                ", name='" + name + '\'' +
                '}';
    }
    public Dog getDog() {
        return dog;
    }
    public void setDog(Dog dog) {
        this.dog = dog;
    }
    public Cat getCat() {
        return cat;
    }
    public void setCat(Cat cat) {
        this.cat = cat;
    }
}

科普:

@Nullable     字段标记了这个注解,说明这个字段可以为null
public @interface Autowired {
    boolean required() default true;
}

@Autowired注解可以给一个属性required,默认为true

@Autowired(required = false)

false:说明这个对象可以为null

true:对象不可以为null

@Qualifier(value="xxx")

如果@Autowired自动装配的环境比较复杂,自动装配无法通过一个注解完成的时候,我们可以使用@Qualifier(value="xxx")去配合@Autowired的使用,指定一个唯一的bean对象注入

public class Person {
    @Autowired
    @Qualifier(value = "dog22")
    private Dog dog;
    @Autowired
    @Qualifier(value = "cat1123")
    private Cat cat;
    private String name;
    Getter  Setter
    ...
}

@Resource

先通过byName的方式查找,如果找不到,再通过byType的方式查找

可以指定一个name,去查找对应的id,如

@Resource(name = "xxx")

小结:

@Resource和@Autowired的区别:

  • 都是用来自动装配的,都可以放在属性字段上
  • @Autowired首先通过byType的方式查找,如果不唯一,就再通过byName的方式查找【常用】
  • @Resource默认通过byName的方式实现,如果找不到对应的id,则通过byType的方式查找

8. 使用注解开发

再Spring4之后,要使用注解开发,必须保证AOP的包导入了

<?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:component-scan base-package="cn.codewei.pojo"/>
    <context:annotation-config/>
</beans>
  1. bean

@Component

// 等价于:<bean id="user" class="cn.codewei.pojo.Uesr"/>
// Component : 组件
@Component
public class User {
    public String name="唐嫣";
}

放在类上,说明这个类被Spring管理了,就是bean,id为其类名的小写

  1. 属性如何注入

@Value

// 等价于:<property name="name" value="唐嫣"/>
@Component
public class User {
    @Value("唐嫣")
    public String name;
}
  1. 衍生的注解

@Component有几个衍生注解,我们在web开发中,会按照MVC三层架构分层!

  • dao【@Repository】
  • service【@Service】
  • controller【@Controller】

这四个注解功能都是一样的,都是代表将某个类注册到Spring容器中,装配Bean

  1. 自动装配

@Autowired:自动装配通过类型,名字

​ 如果Autowired不能唯一自动装配上属性,则需要通过@Qulifier(value="xxx")指定bean的id

@Nullable:字段标记了这个注解,说明这个字段可以为null

@Resource:自动装配通过名字,类型

  1. 作用域

@Scope("singleton") 单例

@Scope("prototype") 原型

  1. 小结

    • xml更加万能,适用于任何场合!维护简单方便
    • 注解 不是自己类使用不了,维护相对复杂

xml与注解最佳实践:

  • xml用来管理bean
  • 注解用来完成属性的注入
  • 我们在使用的过程中,只需要注意一个问题,必须让注解生效,就需要开启注解的支持

    <!--指定要扫描的包,这个包下的注解就会生效-->
    <context:component-scan base-package="cn.codewei"/>
    <context:annotation-config/>

9. 使用Java的方式配置Spring

我们现在要完全不使用Spring的xml配置了,全权交给Java来做!

JavaConfig是Spring的一个子项目,在Spring4之后,它成为了一个核心功能

要写一个配置类,如

@Configuration
public class MyConfig {
    @Bean
    public User user(){
        return new User();
    }
}

要放入Spring容器中的类:

@Component
public class User {
    @Value("唐嫣")
    private String name;
    @Override
    public String toString() {
        return "User{" +
                "name='" + name + '\'' +
                '}';
    }
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
}

配置类也会被Spring容器托管,注册到容器中,因为他本来就是一个@Component

  • @Configuration:代表这是一个配置类,和之前的beans.xml一样的,加在类上
  • @ComponentScan("cn.codewei.pojo"):扫描包。加在类上
  • @Bean:注册Bean,就相当于我们之前写的bean标签,这个方法的名字就相当于bean标签的id属性,这个方法的返回值,就相当于bean标签的class属性。加在方法上
  • @Import(配置类.class):引用配置类,相当于import标签。加在类上

如果完全使用的配置类方式去做,我们就要通过

ApplicationContext context = new AnnotationConfigApplicationContext(配置类.class);

来获取Spring的上下文对象ApplicationContext

==配置类可以存在多个==

这种纯Java的配置方式,在SpringBoot中随处可见!!


10. 代理模式

为什么要学习代理模式?

因为这是SpringAOP的底层!!【SpringAOP和SpringMVC】

代理模式的分类:

  • 静态代理
  • 动态代理

在这里插入图片描述

代理角色(中介)和真实的角色(房东)有共同的目标,就是出租房子,所以代理角色(中介)代理真实的角色(房东),就要实现和真实的角色(房东)相同的接口,来保证目标是相同的

10.1 静态代理

角色分析:

  • 抽象角色:一般会使用接口或者抽象类来解决
  • 真实角色:被代理的角色
  • 代理角色:代理真实的角色,代理真实角色后,我们一般会做一些附属操作
  • 客户:访问代理对象的人

代码步骤:

  1. 接口

    // 租房的接口
    public interface Rent {
        public void rent();
    }
    
  2. 真实角色

    // 房东
    public class Host implements Rent {
        public void rent() {
            System.out.println("房东要出租房子!");
        }
    }
    
  3. 代理角色

    // 代理,中介
    public class Proxy implements Rent{
        private Rent host;
        public Proxy() {
        }
        public Proxy(Host host) {
            this.host = host;
        }
        public void rent() {
            host.rent();
        }
        public void seeHouse(){
            System.out.println("看房");
        }
        public void hetong(){
            System.out.println("签租赁合同");
        }
        public void free(){
            System.out.println("收中介费");
        }
    }
  4. 客户端访问代理角色

    public class Client {
        public static void main(String[] args) {
            // 房东要出租房子
            Host host = new Host();
    
            // 代理,中介帮房东出租房子
            // 代理角色一般会有一些附属操作,增强功能
            Proxy proxy = new Proxy(host);
    
            // 租房的人,不用面对房东,直接找中介租房即可
            proxy.rent();
    
            // 这是代理角色(中介)做的附属操作,就是对功能的增强
            proxy.seeHouse();
            proxy.hetong();
            proxy.free();
        }
    }

代理模式的好处:

  1. 可以使真实的角色的操作更加纯粹,不用去关注一些公共的事情
  2. 公共的事情就交给了代理角色,实现了业务的分工,对功能进行增强
  3. 公共业务发生拓展的时候,方便集中管理

代理模式的缺点:

  • 一个真实的角色就会产生一个代理角色,而每一个代理角色都是一个类,这样代码量就会增加

10.2 静态代理加深理解

对我们平常的业务操作,增删改查增加日志输出!

我们不能改动原有的代码,在公司中改动原有的业务代码是大忌!!所以我们使用代理模式

  1. 接口

    public interface UserService {
        public void add();
        public void delete();
        public void update();
        public void query();
    }
  2. 接口真实的实现类

    public class UserServiceImpl implements UserService{
        public void add() {
            System.out.println("增加了一个用户");
        }
    
        public void delete() {
            System.out.println("删除了一个用户");
        }
    
        public void update() {
            System.out.println("更改了一个用户");
        }
    
        public void query() {
            System.out.println("查询了一个用户");
        }
    }
  3. 代理对象(代理类)

    public class Proxy implements UserService {
        private UserService userServiceImpl;
    
        public void setUserServiceImpl(UserService userServiceImpl) {
            this.userServiceImpl = userServiceImpl;
        }
    
        // 日志方法
        public void log(String msg){
            System.out.println("使用了"+msg+"方法");
        }
    
        public void add() {
            log("add");
            userServiceImpl.add();
        }
    
        public void delete() {
            log("delete");
            userServiceImpl.delete();
        }
    
        public void update() {
            log("update");
            userServiceImpl.update();
        }
    
        public void query() {
            log("query");
            userServiceImpl.query();
        }
    }
  4. 客户访问代理角色

    public class Client {
        public static void main(String[] args) {
            UserService userService = new UserServiceImpl();
            Proxy proxy = new Proxy();
            proxy.setUserServiceImpl(userService);
            proxy.add();
        }
    }

在这里插入图片描述

10.3 动态代理

  • 动态代理和静态代理角色一样
  • 动态代理的代理类是动态生成的,不是我们直接写好的
  • 动态代理分为两大类:

    1. 基于接口的动态代理:JDK动态代理【我们这里使用】
    2. 基于类的动态代理:cglib
    3. java字节码实现:javasist

需要了解两个类:Proxy:代理,InvocationHandler:调用处理程序

动态代理的好处:

  1. 可以使真实的角色的操作更加纯粹,不用去关注一些公共的事情
  2. 公共的事情就交给了代理角色,实现了业务的分工,对功能进行增强
  3. 公共业务发生拓展的时候,方便集中管理
  4. 一个动态代理类代理的是一个接口,一般就是对应的一类业务
  5. 一个动态代理类可以代理多个类,只要是实现了同一个接口即可

代码实现:

  1. 接口

    public interface UserService {
        public void add();
        public void delete();
        public void update();
        public void query();
    }
  2. 真实对象实现类

    public class UserServiceImpl implements UserService {
        public void add() {
            System.out.println("增加了一个用户");
        }
    
        public void delete() {
            System.out.println("删除了一个用户");
        }
    
        public void update() {
            System.out.println("更改了一个用户");
        }
    
        public void query() {
            System.out.println("查询了一个用户");
        }
    }
  3. 处理程序

    public class ProxyInvocationHandler implements InvocationHandler {
        private Object target;
    
        public void setTarget(Object target) {
            this.target = target;
        }
    
        public Object getProxy(){
            return Proxy.newProxyInstance(target.getClass().getClassLoader(),target.getClass().getInterfaces(),this);
        }
    
    
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            System.out.println("调用了"+method.getName()+"方法");
            Object result = method.invoke(target, args);
            return result;
        }
    }
  4. 客户端动态生成代理对象,调用方法

    public class Client {
        public static void main(String[] args) {
            UserService userService = new UserServiceImpl();
            ProxyInvocationHandler proxyInvocationHandler = new ProxyInvocationHandler();
            proxyInvocationHandler.setTarget(userService);
            UserService proxy =(UserService) proxyInvocationHandler.getProxy();
            proxy.add();
            proxy.delete();
        }
    }

11. AOP

11.1 什么是AOP?

AOP为Aspect Oriented Programming的缩写,意为:面向切面,通过预编译方式和运行期间动态代理实现程序功能的统一维护的一种技术。AOP是OOP的延续,是软件开发中的一个热点,也是Spring框架中的一个重要内容,是函数式编程的一种衍生范型。利用AOP可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率。

在这里插入图片描述

11.2 AOP在Spring中的作用

==提供声明事务,允许用户自定义切面==

  • 横切关注点:跨越应用程序多个模块的方法或功能。即是,与我们业务逻辑无关的,但是我们需要关注的部分,就是横切关注点。如日志,安全,缓存,事务等等....
  • 切面(ASPECT):横切关注点被模块化的特殊对象。即,它是一个类
  • 通知(Advice):切面必须要完成的工作。即,它是类中的一个方法
  • 目标(Target):被通知对象
  • 代理(Proxy):向目标对象应用通知之后创建的对象
  • 切入点(PointCut):切面通知执行的“地点”的定义
  • 连接点(JointPoint):与切入点匹配的执行点

SpringAOP中,通过Advice定义横切逻辑,Spring中支持5种类型的Advice

通知类型连接点实现接口
前置通知方法前org.springframework.aop.MethodBeforeAdvice
后置通知方法后org.springframework.aop.AfterReturningAdvice
环绕通知方法前后org.aopalliance.intercept.MethodInterceptor
异常抛出通知方法抛出异常org.springframeword.aop.ThrowsAdvice
引介通知类中增加新的方法属性org.springframeword.aop.IntroductionInterceptor

即AOP在不改变原有代码的情况下,去增加新的功能

11.3 使用Spring实现AOP

【重点】使用AOP织入,需要导入一个依赖包!

<!-- https://mvnrepository.com/artifact/org.aspectj/aspectjweaver -->
<dependency>
    <groupId>org.aspectj</groupId>
    <artifactId>aspectjweaver</artifactId>
    <version>1.9.5</version>
</dependency>

方式一:使用spring的API接口【主要是Spring接口实现】

  1. 定义一个类,实现接口

    public class Log implements MethodBeforeAdvice {
        // method: 要执行的目标对象的方法
        // objects:参数
        // o:目标对象
        public void before(Method method, Object[] objects, Object o) throws Throwable        {
            System.out.println(o.getClass().getName()+"的"+method.getName()+"被执行了");
        }
    }
  2. 在applicationContext.xml中配置AOP

需要导入AOP的约束

<?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
        https://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="userServiceImpl" class="cn.codewei.service.UserServiceImpl"/>
    <bean id="log" class="cn.codewei.log.Log"/>


    <!--方式一:使用原生的Spring API接口-->
    <!--配置AOP 需要导入aop的约束-->
    <aop:config>
        <!--切入点-->
        <aop:pointcut id="pointcut" expression="execution(* cn.codewei.service.UserServiceImpl.*(..))"/>

        <!--执行增强-->
        <aop:advisor advice-ref="log" pointcut-ref="pointcut"/>

    </aop:config>
</beans>
  1. 测试

    public class MyTest {
        public static void main(String[] args) {
            ClassPathXmlApplicationContext applicationContext = new ClassPathXmlApplicationContext("applicationContext.xml");
            UserService userServiceImpl = (UserService) applicationContext.getBean("userServiceImpl");
            userServiceImpl.add();
        }
    }
  2. 测试结果

    在这里插入图片描述

方式二:自定义来实现AOP【主要是切面定义】

  1. 定义一个普通的类(方法名自定义)

    public class DiyPointCut {
        public void before(){
            System.out.println("===============方法执行前===============");
        }
    
        public void after(){
            System.out.println("===============方法执行后===============");
        }
    }
  2. 在applicationContext.xml中配置AOP

    <?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
            https://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="userServiceImpl" class="cn.codewei.service.UserServiceImpl"/>
        <bean id="afterLog" class="cn.codewei.log.AfterLog"/>
        <bean id="log" class="cn.codewei.log.Log"/>
    
        <!--方式二:自定义类-->
        <bean id="diyPointCut" class="cn.codewei.diy.DiyPointCut"/>
    
        <aop:config>
            <!--自定义切面,ref要引用的类-->
            <aop:aspect ref="diyPointCut">
                <!--切入点-->
                <aop:pointcut id="pointcut" expression="execution(* cn.codewei.service.UserServiceImpl.*(..))"/>
    
                <aop:after method="after" pointcut-ref="pointcut"/>
                <aop:before method="before" pointcut-ref="pointcut"/>
            </aop:aspect>
        </aop:config>
    </beans>
 ![在这里插入图片描述](https://img-blog.csdnimg.cn/20200506115953964.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80NTY4MjA1Mw==,size_16,color_FFFFFF,t_70#pic_center)
  1. 测试

    public class MyTest {
        public static void main(String[] args) {
            ClassPathXmlApplicationContext applicationContext = new ClassPathXmlApplicationContext("applicationContext.xml");
            UserService userServiceImpl = (UserService) applicationContext.getBean("userServiceImpl");
            userServiceImpl.add();
        }
    }
  2. 测试结果

在这里插入图片描述

方式三:使用注解实现

@Aspect 标注这个类是一个切面,加在类上

@Before 在切入点前执行,加在方法上

@After 在切入点后执行,加在方法上

@Around 环绕增强,加在方法上 可以接收一个参数:ProceedingJoinPoint

@Aspect
public class AnnotationPointCut {

    @Before("execution(* cn.codewei.service.UserServiceImpl.*(..))")
    public void before(){
        System.out.println("==========方法执行前==========");
    }

    @After("execution(* cn.codewei.service.UserServiceImpl.*(..))")
    public void after(){
        System.out.println("==========方法执行后==========");
    }
}

使用注解同样需要applicationContext.xml配置

<!--方式三:使用注解-->
<bean id="annotationPointCut" class="cn.codewei.diy.AnnotationPointCut"/>
<aop:aspectj-autoproxy/>

12. 整合Mybatis

步骤:

  1. 导入相关jar包

    • junit
    • mybatis
    • mysql数据库
    • spring相关的
    • aop织入
    • mybatis-spring
    • spring-jdbc
    • lombok
    <dependencies>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.12</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.19</version>
        </dependency>
        <dependency>
            <groupId>org.mybatis</groupId>
            <artifactId>mybatis</artifactId>
            <version>3.5.4</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-webmvc</artifactId>
            <version>5.2.6.RELEASE</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-jdbc</artifactId>
            <version>4.1.2.RELEASE</version>
        </dependency>
        <dependency>
            <groupId>org.mybatis</groupId>
            <artifactId>mybatis-spring</artifactId>
            <version>2.0.4</version>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.12</version>
        </dependency>
        <dependency>
            <groupId>org.aspectj</groupId>
            <artifactId>aspectjweaver</artifactId>
            <version>1.9.5</version>
        </dependency>
    </dependencies>

注意:在maven中存在的资源(配置文件导出问题)

<build>
    <resources>
        <resource>
            <directory>src/main/java</directory>
            <includes>
                <include>**/*.xml</include>
                <include>**/*.properties</include>
            </includes>
            <filtering>true</filtering>
        </resource>
        <resource>
            <directory>src/main/resources</directory>
            <includes>
                <include>**/*.xml</include>
                <include>**/*.properties</include>
            </includes>
            <filtering>true</filtering>
        </resource>
    </resources>
</build>
  1. 编写配置文件
  2. 测试

12.1 回忆mybatis

  1. 编写实体类
  2. 编写核心配置文件
  3. 编写接口
  4. 编写Mapper.xml
  5. 测试

12.2 Mabatis-spring

需要导入一些新的包 spring-jdbc,mybatis-spring

创建一个新的配置文件,spring-dao.xml

  1. 编写数据源配置

这里使用的是spring提供的jdbc

<!--DataSource:使用Spring的数据源替换Mybatis的配置 c3p0 druid dbcp spingjdbc
        我们这里使用spring提供的jdbc
    -->
<bean id="datasource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
    <property name="driverClassName" value="com.mysql.cj.jdbc.Driver"/>
    <property name="url" value="jdbc:mysql://localhost:3306/mybatis?useSSL=true&amp;useUnicode=true&amp;characterEncoding=UTF-8&amp;serverTimezone=UTC"/>
    <property name="username" value="root"/>
    <property name="password" value="shw123zxc"/>
</bean>
  1. SqlSessionFactory配置

    <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
        <!--指定数据源-->
        <property name="dataSource" ref="datasource"/>   
        <!--绑定mybatis-->
        <!--指定mybatis的核心配置文件-->
        <property name="configLocation" value="classpath:mybatis-config.xml"/> 
        <!--指定mybatis的mapper.xml文件-->
        <property name="mapperLocations" value="classpath:cn/codewei/dao/*.xml"/>
    </bean>

mybatis核心配置文件中的内容都可以写在此处,但我们一般会在核心配置文件中配置别名和settings

  1. SqlSessionTemplate配置

SqlSessionTemplate和SqlSession相同,只是改了个名字

<bean id="sqlSession" class="org.mybatis.spring.SqlSessionTemplate">
    <!--只能使用构造器注入,因为没有set方法-->
    <constructor-arg name="sqlSessionFactory" ref="sqlSessionFactory"/>
</bean>
  1. 写接口的实现类

    public class UserMapperImpl implements UserMapper {
    
        // 我们的所有操作都使用sqlSession执行在之前
        // 现在都是以SqlSessionTemplate  和sqlsession是一样的 只是换了个名字
    
        private SqlSessionTemplate sqlSessionTemplate;
    
        public void setSqlSessionTemplate(SqlSessionTemplate sqlSessionTemplate) {
            this.sqlSessionTemplate = sqlSessionTemplate;
        }
    
        public List<User> showUser() {
            UserMapper mapper = sqlSessionTemplate.getMapper(UserMapper.class);
            List<User> users = mapper.showUser();
            return users;
        }
    }
  2. 将实现类放入IOC中

    <bean id="userMapperImpl" class="cn.codewei.dao.UserMapperImpl">
        <property name="sqlSessionTemplate" ref="sqlSession"/>
    </bean>

一般我们把bean放入applicationContext.xml配置文件中统一管理,spring-dao.xml专门用来管理mybatis的配置,在applicationContext.xml中使用import引入其他配置文件

  1. 测试

    @Test
    public void showUserTest() throws IOException {
        ApplicationContext classPathXmlApplicationContext = new ClassPathXmlApplicationContext("spring-dao.xml");
        UserMapper userMapperImpl = (UserMapper)classPathXmlApplicationContext.getBean("userMapperImpl");
        List<User> users = userMapperImpl.showUser();
        for (User user : users) {
            System.out.println(user);
        }
    }

另一种方式:不再用在实现类中通过setter方法注入sqlSessionTemplate,而是继承SqlSessionDaoSupport

如:

public class UserMapperImpl2 extends SqlSessionDaoSupport implements UserMapper {
    public List<User> showUser() {
        UserMapper mapper = getSqlSession().getMapper(UserMapper.class);
        return mapper.showUser();
    }
}

在spring-dao.xml中配置

<bean id="userMapperImpl2" class="cn.codewei.dao.UserMapperImpl2">
    <property name="sqlSessionFactory" ref="sqlSessionFactory"/>
</bean>

需要注入sqlSessionFactory,因为SqlSessionDaoSupport需要注入
方式三:不需要写实现类,也不用配置sqlSessionTemplate
配置dao接口扫描包,动态实现Dao接口注入到Spring中

    <!--配置dao接口扫描包,动态的实现了Dao接口注入到Spring-->
<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
    <!--注入sqlSessionFactory-->
    <property name="sqlSessionFactoryBeanName" value="sqlSessionFactory"/>
    <!--要扫描的dao包-->
    <property name="basePackage" value="cn.codewei.dao"/>
</bean>

13. 声明式事务

13.1 回顾事务

  • 要么都成功,要么都失败!
  • 事务在项目开发中,十分重要,涉及到数据的一致性问题!!
  • 确保完整性和一致性

事务的ACID原则:

  • 原子性
  • 一致性
  • 隔离性

    • 多个业务可能操作同一个资源,防止数据破坏
  • 持久性

    • 事务一旦提交,无论系统发生什么问题,结果都不再被影响,被持久化的写到存储器中!

13.2 spring中的事务管理

  • 声明式事务:AOP
  • 编程式事务:需要在代码中,进行事务的管理
  1. 在配置文件中配置声明事务,开启spring处理事务的功能

    <!--声明式事务-->
    <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <constructor-arg name="dataSource" ref="datasource"/>
    </bean>
  2. 配置事务通知

    <!--结合AOP实现事务的织入-->
    <!--配置事务的通知-->
    <tx:advice id="txAdvice" transaction-manager="transactionManager">
        <!--给哪些方法配置事务-->
        <!--配置事务的传播特性-->
        <tx:attributes>
            <!--给哪些方法配置事务  propagation 事务的传播特性-->
            <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="pointcut" expression="execution(* cn.codewei.dao.*.*(..))"/>
            <aop:advisor advice-ref="txAdvice" pointcut-ref="pointcut"/>
        </aop:config>

为什么需要事务?

  • 如果不配置事务,可能存在数据提交不一致的清空
  • 如果我们不在spring中去配置声明式事务,我们就需要在代码中手动配置事务
    serMapper.class);
    return mapper.showUser();

       }

    }

    
    在spring-dao.xml中配置
    

<bean id="userMapperImpl2" class="cn.codewei.dao.UserMapperImpl2">

   <property name="sqlSessionFactory" ref="sqlSessionFactory"/>

</bean>


需要注入sqlSessionFactory,因为SqlSessionDaoSupport需要注入

---

## 13. 声明式事务

### 13.1 回顾事务

- 要么都成功,要么都失败!
- 事务在项目开发中,十分重要,涉及到数据的一致性问题!!
- 确保完整性和一致性

**事务的ACID原则:**

- 原子性
- 一致性
- 隔离性
  - 多个业务可能操作同一个资源,防止数据破坏
- 持久性
  - 事务一旦提交,无论系统发生什么问题,结果都不再被影响,被持久化的写到存储器中!

### 13.2 spring中的事务管理

- 声明式事务:AOP
- 编程式事务:需要在代码中,进行事务的管理

1. 在配置文件中配置声明事务,开启spring处理事务的功能

<!--声明式事务-->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">

   <constructor-arg name="dataSource" ref="datasource"/>

</bean>


2. 配置事务通知

<!--结合AOP实现事务的织入-->
<!--配置事务的通知-->
<tx:advice id="txAdvice" transaction-manager="transactionManager">

   <!--给哪些方法配置事务-->
   <!--配置事务的传播特性-->
   <tx:attributes>
       <!--给哪些方法配置事务  propagation 事务的传播特性-->
       <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="pointcut" expression="execution(* cn.codewei.dao.*.*(..))"/>
       <aop:advisor advice-ref="txAdvice" pointcut-ref="pointcut"/>
   </aop:config>

为什么需要事务?

- 如果不配置事务,可能存在数据提交不一致的清空
- 如果我们不在spring中去配置声明式事务,我们就需要在代码中手动配置事务
本文作者: Author:     文章标题: Spring快速入门
本文地址: https://codewei.cn/archives/162/      
版权说明:若无注明,本文皆为“阿伟的小屋”原创,转载请保留文章出处
Last modification:October 19th, 2020 at 11:28 am
贫困山区儿童,谢谢打赏