ssm-Spring-全部知识点-胜哥

第一章 初识Spring

1.1 Spring简介

  • Spring是一个为简化企业级开发而生的开源框架

    • c c++ c#
  • Spring是一个IOC(DI)和AOP容器框架。

    • IOC:Inversion of Control【控制反转】
    • DI:Dependency Injection【依赖注入】
    • AOP:Aspect-Oriented Programming,面向切面编程
  • Spring官网:spring.io

1.2 Spring之Helloworld

  • 导入jar包

    <!--导入spring-context-->
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-context</artifactId>
        <version>5.3.1</version>
    </dependency>
    
    <!--导入junit4.12-->
    <dependency>
        <groupId>junit</groupId>
        <artifactId>junit</artifactId>
        <version>4.12</version>
        <scope>test</scope>
    </dependency>
    
  • 创建Employee对象

    public class Employee {
    
        private Integer id;
        private String lastName;
        private Integer age;
        private Double salary;
     	//.....   
    }
    
  • 编写配置文件【beans.xml或applicationContext.xml或spring.xml】

    • 配置文件位置:resources下

    • 配置文件名称: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 http://www.springframework.org/schema/beans/spring-beans.xsd">
      
          <!--    Spring管理Employee对象  -->
          <!--    将Employee交给Spring管理【将Employee注入到容器中】-->
          <bean id="empCheng" class="com.atguigu.pojo.Employee">
              <property name="id" value="1001"></property>
              <property name="lastName" value="chengcheng"></property>
              <property name="age" value="18"></property>
              <property name="salary" value="10.0"></property>
          </bean>
      </beans>
      
  • 创建并使用容器对象,获取Employee对象

    @Test
    public void testSpringHello(){
        //传统方式获取对象
        Employee e = new Employee();
    
        //Spring方式获取对象
        //1.先创建容器对象
        ApplicationContext ioc =
                new ClassPathXmlApplicationContext("applicationContext.xml");
        //2.再从容器中获取需要对象【Employee】
        Employee empCheng = (Employee)ioc.getBean("empCheng");
    
        System.out.println("empCheng:"+empCheng);
    
    }
    

1.3 Spring中getBean()三种方式

  • getBean(String beanId)

    • 不足:需要强转【不方便】
  • getBean(Class clazz)

    • 不足:当容器中有多个相同类型bean时,会报如下错:

      expected single matching bean but found 2: empCheng,empJing
      
  • getBean(String beanId,Class clazz):推荐使用

day06

1.4 Spring 特性

  • 非侵入式:基于Spring开发的应用中的对象可以不依赖于Spring的API。
  • 容器:Spring是一个容器,因为它包含并且管理应用对象的生命周期。
  • 组件化:Spring实现了使用简单的组件配置组合成一个复杂的应用。在 Spring 中可以使用XML和Java注解组合这些对象。
  • 一站式:在IOC和AOP的基础上可以整合各种企业应用的开源框架和优秀的第三方类库(实际上Spring 自身也提供了表述层的SpringMVC和持久层的JDBCTemplate)。
  • 控制反转:IOC——Inversion of Control,指的是将对象的创建权交给Spring去创建。使用Spring之前,对象的创建都是由我们自己在代码中new创建。而使用Spring之后。对象的创建都是由给了Spring框架。
  • 依赖注入:DI——Dependency Injection,是指依赖的对象不需要手动调用setXXX方法去设置,而是通过配置赋值。
  • 面向切面编程:AOP——Aspect Oriented Programming,在不修改源代码的基础上进行功能扩展。

第二章 Spring之IOC底层实现

  • 在获取Bean之前,首先需要创建IOC容器。Spring提供了IOC容器的两种实现方式:

2.1 BeanFactory对象工程

  • BeanFactory:IOC容器的基本实现,是Spring内部的使用接口,是面向Spring本身的,不是提供给开发人员使用的。

2.2 ApplicationContext容器对象

  • ApplicationContext:BeanFactory的子接口,提供了更多高级特性。面向Spring的使用者,几乎所有场合都使用ApplicationContext而不是底层的BeanFactory。

  • ApplicationContext常用实现类及接口

    • BeanFactory
      • ....
        • ApplicationContext【容器对象根接口】
          • ConfigurableApplicationContext【容器对象根接口的子接口,提供了刷新关闭容器对象方法】
            • .....
              • FileSystemXmlApplicationContext
              • ClassPathXmlApplicationContext【容器对象实现类,一般使用该类创建容器对象】

2.3 依赖注入方式【重点】

  • set注入

    • 语法:property标签
      • name属性:设置属性名
      • value属性:设置属性值
    • 原理
      • 调用setXXX()方法,入参
  • 构造注入

    • 语法:constructor-arg标签
      • name:设置构造器中参数名
      • value:设置属性值
    • 原理
      • 调用有参构造器,入参
  • p名称空间注入

第三章 Spring中依赖注入数值【重点】

3.1 字面量数值

  • 数据类型:基本数据类型+包装类+String
  • 注入语法:可以使用value属性或value标签注入数值

3.2 使用CDATA区,解决XML中特殊字符问题

  • 语法:<![CDATA[]]>

  • 作用:解决XML中特殊字符问题

  • 示例代码

    <bean id="empCheng" class="com.atguigu.pojo.Employee">
        <property name="id" value="1001"></property>
        <property name="lastName">
            <value><![CDATA[<chengcheng>]]></value>
        </property>
        <property name="age" value="18"></property>
        <property name="salary" value="10.0"></property>
    </bean>
    

3.3 引用外部已声明的bean

  • 示例代码

    <bean id="dept1" class="com.atguigu.pojo.Dept">
        <property name="deptId" value="101"></property>
        <property name="deptName" value="程序员鼓励师"></property>
    </bean>
    
    <bean id="empCheng2" class="com.atguigu.pojo.Employee">
        <property name="id" value="1001"></property>
        <property name="lastName" value="chengcheng"></property>
        <property name="age" value="18"></property>
        <property name="salary" value="10.0"></property>
        <property name="dept" ref="dept1"></property>
        <property name="dept.deptName" value="秘书部门"></property>
    </bean>
    
  • 注意:修改员工中部门信息,部门信息也会同时改变

    image-20220308111009680

3.4 内部bean

  • 概念:在一个bean中,完整定义另一个bean,此时里边的bean称之为内部bean。

  • 示例代码

    <!--    测试内部bean-->
        <bean id="empCheng3" class="com.atguigu.pojo.Employee">
            <property name="id" value="10086"></property>
            <property name="lastName" value="chengcheng3"></property>
            <property name="age" value="18"></property>
            <property name="salary" value="10.0"></property>
            <property name="dept">
                <bean class="com.atguigu.pojo.Dept">
                    <property name="deptId" value="102"></property>
                    <property name="deptName" value="小卖部"></property>
                </bean>
            </property>
        </bean>
    

3.5 依赖注入数值—List

<bean id="dept2" class="com.atguigu.pojo.Dept">
    <property name="deptId" value="102"></property>
    <property name="deptName" value="研发部门"></property>
    <property name="employees">
        <list>
            <ref bean="empCheng"></ref>
            <ref bean="empJing"></ref>
            <bean class="com.atguigu.pojo.Employee">
                <property name="id" value="1005"></property>
                <property name="lastName" value="鑫哥"></property>
                <property name="age" value="18"></property>
                <property name="salary" value="20.0"></property>
            </bean>
        </list>
    </property>
</bean>

3.6 依赖注入数值—Map

<bean id="dept3" class="com.atguigu.pojo.Dept">
        <property name="deptId" value="103"></property>
        <property name="deptName" value="测试部门"></property>
        <property name="maps">
            <map>
<!--                <entry key="" value=""></entry>-->
                <entry>
                    <key>
                        <value>1001</value>
                    </key>
                    <ref bean="empCheng"></ref>
                </entry>
                <entry>
                    <key>
                        <value>1002</value>
                    </key>
                    <ref bean="empJing"></ref>
                </entry>
            </map>
        </property>
    </bean>

3.7 依赖注入数值—其他【Prop|Array等】

  • 提取可重用List
<util:list id="empList">
        <ref bean="empCheng"></ref>
        <ref bean="empJing"></ref>
        <bean class="com.atguigu.pojo.Employee">
            <property name="id" value="1005"></property>
            <property name="lastName" value="鑫哥"></property>
            <property name="age" value="18"></property>
            <property name="salary" value="20.0"></property>
        </bean>
    </util:list>

<!--    测试List-->
    <bean id="dept2" class="com.atguigu.pojo.Dept">
        <property name="deptId" value="102"></property>
        <property name="deptName" value="研发部门"></property>
        <property name="employees" ref="empList"></property>
    </bean>

第四章 Spring管理第三方bean【重点】

  • spring管理德鲁伊数据源步骤

    • 导入jar包

      <!--导入druid的jar包-->
      <dependency>
          <groupId>com.alibaba</groupId>
          <artifactId>druid</artifactId>
          <version>1.1.10</version>
      </dependency>
      <!--导入mysql的jar包-->
      <dependency>
          <groupId>mysql</groupId>
          <artifactId>mysql-connector-java</artifactId>
          <version>8.0.26</version>
      </dependency>
      
    • 导入db.properties属性文件

      #key=value
      db.username=root
      db.password=root
      db.url=jdbc:mysql://localhost:3306/db1216?时区useUnicode=true&characterEncoding=UTF-8&rewriteBatchedStatements=true
      db.driverClassName=com.mysql.jdbc.Driver
      
    • 使用spring框架管理数据源【将DruidDataSource装配到IOC容器中】

      <!--    引入外部db.properties文件    -->
          <context:property-placeholder location="classpath:db.properties"></context:property-placeholder>
      
      <!--    管理德鲁伊数据源-->
      <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
          <property name="driverClassName" value="${db.driverClassName}"></property>
          <property name="url" value="${db.url}"></property>
          <property name="username" value="${db.username}"></property>
          <property name="password" value="${db.password}"></property>
      </bean>
      

第五章 FactoryBean【工厂bean】

5.1 FactoryBean概述

  • Spring中有两种类型的bean,一种是普通bean,另一种是工厂bean,即FactoryBean。
  • 使用FactoryBean可以帮助我们程序员参与bean的创建中

5.2 使用FactoryBean步骤

  • 实现接口:FactoryBean
  • 重写接口中方法

5.3 示例代码

package com.atguigu.factory;

import com.atguigu.pojo.Employee;
import org.springframework.beans.factory.FactoryBean;

/**
 * @author Chunsheng Zhang 尚硅谷
 * @create 2022/3/8 15:33
 */
public class MyFactoryBean implements FactoryBean<Employee> {
    /**
     * 参与Employee对象创建
     * @return
     * @throws Exception
     */
    @Override
    public Employee getObject() throws Exception {
        Employee employee = new Employee();
        employee.setId(1001);
        employee.setLastName("zhaolusi");
        employee.setSalary(100.0);
        return employee;
    }

    @Override
    public Class<?> getObjectType() {
        return Employee.class;
    }

}
<?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="employee" class="com.atguigu.factory.MyFactoryBean"></bean>


</beans>
@Test
    public void testFactoryBean(){
        ApplicationContext context =
                new ClassPathXmlApplicationContext("applicationContext_factoryBean.xml");

        Employee employee = context.getBean("employee", Employee.class);
//        MyFactoryBean employee = context.getBean("employee", MyFactoryBean.class);
        System.out.println("employee = " + employee);


    }

第六章 Spring中Bean作用域【重点】

6.1 Bean的四个作用域

  • singleton:单例,当前工程中只有一个对象
    • 创建容器对象时,spring创建bean对象
  • prototype:多例,当前工程中可以有多个对象
    • 每次调用getBean()方法时,spring创建bean对象
  • request:当前请求有效,离开当前请求失效
    • 当前请求:发送请求后,URL不变即为当前请求;URL改变不在当前请求
  • session:当前会话有效,离开当前会话失效
    • 当前会话:会话指的是浏览器与服务器通信状态,称之为浏览器与服务器会话
    • 关闭浏览器或更换浏览器即为结束会话

6.2 设置bean作用域语法

  • 在bean标签中添加属性:scope

第七章 Spring中bean的生命周期【重点】

Servlet生命周期

  • 默认情况
    • 第一次请求Servlet时,服务器创建Servlet对象,并调用init()方法进行初始化,最后调用service()方法处理请求,做出响应。以后再次请求Servlet,只调用service(request,response)处理请求,....。当关闭或重启服务器时,调用destroy()方法,销毁Servlet对象
  • 配置情况

7.1 Bean的生命周期

① 通过构造器或工厂方法创建bean实例

② 为bean的属性设置值和对其他bean的引用

③ 调用bean的初始化方法

④ bean可以使用了

当容器关闭时,调用bean的销毁方法

7.2 Bean的生命周期示例代码

public class Dept {
    //.....
    /**
     * Dept初始化方法
     */
    public void initDept(){
        System.out.println("==>3. 初始化方法,initDept()");
    }
    /**
     * Dept销毁方法
     */
    public void destroyDept(){
        System.out.println("==>5. 销毁方法,destroyDept()");
    }
}
<bean id="dept001" class="com.atguigu.pojo.Dept"
        init-method="initDept"
        destroy-method="destroyDept">
    <property name="deptId" value="001"></property>
</bean>
@Test
public void testBeanLc(){

    //创建容器对象
    ConfigurableApplicationContext context =
            new ClassPathXmlApplicationContext("applicationContext_beanLifeCycle.xml");
    Dept dept001 = context.getBean("dept001", Dept.class);
    System.out.println("4. 使用dept001 = " + dept001);
    //关闭容器对象
    context.close();
}

7.3 Bean的后置处理器

  • 创建后置处理器实现类,实现接口:BeanPostProcessor

  • 重写接口中两个方法

    • postProcessBeforeInitialization:在bean初始化之前执行
    • postProcessAfterInitialization:在bean初始化之后执行
  • 将实现类装配IOC容器中

  • 示例代码

    package com.atguigu.processor;
    
    import org.springframework.beans.BeansException;
    import org.springframework.beans.factory.config.BeanPostProcessor;
    import org.springframework.lang.Nullable;
    
    /**
     * @author Chunsheng Zhang 尚硅谷
     * @create 2022/3/8 16:36
     */
    public class MyPostProcessor implements BeanPostProcessor {
    
        /**
         * 在bean初始化之前执行
         * @param bean
         * @param beanName
         * @return
         * @throws BeansException
         */
        public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
            System.out.println("**************初始化之前!!!!!");
            return bean;
        }
    
        /**
         * 在bean初始化之后执行
         * @param bean
         * @param beanName
         * @return
         * @throws BeansException
         */
        public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
            System.out.println("**************初始化之后!!!!!");
            return bean;
        }
    
    }
    
    <bean class="com.atguigu.processor.MyPostProcessor"></bean>
    

7.4 装配后置处理器,bean的生命周期

7.4 添加后置处理器,bean的生命周期

① 通过构造器或工厂方法创建bean实例

② 为bean的属性设置值和对其他bean的引用

  • 将bean实例传递给bean后置处理器的postProcessBeforeInitialization()方法

③ 调用bean的初始化方法

  • 将bean实例传递给bean后置处理器的postProcessAfterInitialization()方法

④ bean可以使用了

当容器关闭时,调用bean的销毁方法

day07

第八章 Spring中自动装配【XML】

8.1 装配方式

  • 手动装配:使用value或ref方式为属性注入数值
  • 自动装配:无需value或ref方式为属性注入数值,直接在bean标签中添加属性【autowire】即可

8.2 自动装配

  • 语法:在bean标签中添加属性【autowire】

  • 特点:自动装配只能为非字面量值实现自动装配

  • 装配方式

    • byName:按照属性名称,与IOC容器中id进行匹配
    • byType:按照属性类型,与IOC容器中class进行匹配
  • 底层逻辑:使用setXXX()注入方式,实现自动装配

  • 总结:自动装配建议使用哪种方式

    • 不建议直接使用byName或byType
      • byName需要手动设置名称一致
      • byType方式不足:在IOC容器中如装配多个类型相同bean对象时,会报错
    • 推荐使用注解方式

第九章 Spring中常用注解【重点】

约定>注解>配置>代码

使用注解步骤

  1. 添加注解支持【jar已经导入了】
  2. 开启组件扫描
  3. 使用相关注解

9.1 注解_管理对象【装配对象】

  • 位置:在class上面

  • 常用注解

    • @Component:装配普通组件
    • @Repository:装配持久化层组件
    • @Service:装配业务逻辑层组件
    • @Controller:装配表示层【控制层】组件
  • 注意

    • 默认类名首字母小写作为bean的id
    • 可以使用value属性,标识bean的id
      • 注解中如只使用一个value属性时,value关键字可省略
    • 上述四个注解本质都是@Component注解,分为四个注解目的只有一个:提高代码可读性

9.2 注解_管理对象中属性【装配对象中属性】

  • 常用注解

    • @Autowired

      • 作用:为对象中属性自动装配

      • 装配原理:反射原理

      • 装配方式

        • 先按照byType进行匹配

          • 匹配0个,【默认情况(required=true)】会报如下错误

             expected at least 1 bean which qualifies as autowire candidate. Dependency annotations: {@org.springframework.beans.factory.annotation.Autowired(required=true)}
            
          • 匹配多个,再按照byName进行唯一筛选

            • 筛选出一个对象,正常使用
            • 未筛选出对象,会报如下错
          expected single matching bean but found 2: employeeDao,employeeMapper2
          
          • 匹配1个,正常使用
      • 属性:required,默认值true

        • true:表示被@Autowired注解标识的属性,必须装配数值,如未装配数值,会报错
        • false:表示被@Autowired注解标识的属性,不必须装配数值,IOC容器中有相应数值则装配,容器中没有相应数值,则不装配【不会报装配错误】
    • @Qualifier("employeeDao")【@Autowired兄弟】

      • 作用:为需要装配的属性,设置bean的id
    • @Value():为字面量属性装配数值【初始值】

第十章 完全注解研发【0配置文件】

  • 编写配置类

    package com.atguigu.annotation.config;
    
    import org.springframework.context.annotation.ComponentScan;
    import org.springframework.context.annotation.Configuration;
    
    /**
     * @author Chunsheng Zhang 尚硅谷
     * @create 2022/3/9 14:28
     */
    @Configuration
    @ComponentScan(basePackages = "com.atguigu.annotation")
    public class SpringConfig {
    }
    
  • 使用容器对象AnnotationConfigApplicationContext

    @Test
    public void testNoXml(){
        //创建容器对象
        ApplicationContext ioc =
                new AnnotationConfigApplicationContext(SpringConfig.class);
    
        EmployeeService employeeService = ioc.getBean("employeeService", EmployeeService.class);
    
        employeeService.getAllEmps();
    }
    

第十一章 Spring整合Junit4

  • 导入相关jar包

    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-test</artifactId>
        <version>5.3.1</version>
    </dependency>
    
  • 在测试类上面添加两个注解

    @ContextConfiguration(locations = "classpath:applicationContext_annotation.xml")
    @RunWith(SpringJUnit4ClassRunner.class)
    public class TestAnnotation {
         @Test
        public void testAnnotation2(){
            System.out.println("@@@@"+employeeService);
            employeeService.getAllEmps();
        }
    }
    

第十二章 Spring中组件扫描

12.1 语法

<context:component-scan base-package="com.atguigu.annotation"></context:component-scan>
  • 作用:扫描base-pachage指定的包及其子包下所有注解,被注解标识的类,会将其对象装配到IOC容器中

12.2 包含扫描

  • context:include-filter
  • type
    • annotation:设置包含扫描的,注解的全类名
    • assignable:设置包含扫描的,实现类的全类名
 <!--        设置包含扫描包-->
    <context:component-scan base-package="com.atguigu.annotation" use-default-filters="false">
        <context:include-filter type="annotation" expression="org.springframework.stereotype.Repository"/>
<!--            <context:include-filter type="assignable" expression="com.atguigu.annotation.mapper.EmployeeMapperImpl"/>-->
    </context:component-scan>

12.3 排除扫描

  • context:exclude-filter
  • type
    • annotation:设置排除扫描的,注解的全类名
    • assignable:设置排除扫描的,实现类的全类名
<!--    假设annotation中一共有100个包,排除扫描mapper包-->
<context:component-scan base-package="com.atguigu.annotation">
    <!--        <context:exclude-filter type="annotation" expression="org.springframework.stereotype.Service"/>-->
    <context:exclude-filter type="assignable" expression="com.atguigu.annotation.service.impl.EmployeeServiceImpl"/>
</context:component-scan>
  • 注意:0配置文件,会影响排除扫描【注解>配置】

    //@Configuration
    //@ComponentScan(basePackages = "com.atguigu.annotation")
    public class SpringConfig {
    }
    

第十三章 扩展动态代理模式

13.1 代理模式

  • 概述:程序中需要,但不期望在此处实现。此时使用【中介】代理对象实现。

  • 底层原理

    image-20220309163340612

day08

13.2 为什么使用代理模式

  • 以计算器添加日志功能为例,发现如下问题:
    • 代码分散【日志功能】
    • 代码混乱【业务代码与非业务代码相耦合】
  • 解决思路
    • 将非核心业务代码【日志功能】,先横向提取,再动态织入到业务代码
    • 动态织入:需要动态代理

13.3 手动搭建动态代理模式

  • 实现方式
    • 基于接口实现动态代理: JDK动态代理
    • 基于继承实现动态代理: Cglib、Javassist动态代理
  • 实现JDK动态代理关键步骤
    • 一个类:Proxy
      • 概述:Proxy代理类的基类【类似Object】
      • 方法:newProxyInstance():创建代理对象
    • 一个接口:InvocationHandler
      • 概述:实现动态代理关键【实现动态织入效果】
      • 方法:invoke():实现动态织入

13.4 手动搭建动态代理步骤及代码

  • 创建类【工具类:为实现类创建代理对象】

  • 提供属性【目标对象:实现类】

  • 提供方法【创建代理对象】

  • 提供有参构造器

    package com.atguigu.aopbefore;
    
    import java.lang.reflect.InvocationHandler;
    import java.lang.reflect.Method;
    import java.lang.reflect.Proxy;
    
    /**
     * @author Chunsheng Zhang 尚硅谷
     * @create 2022/3/11 9:29
     */
    public class MyProxy {
    
        /**
         * 提供属性【目标对象:实现类】
         */
        private Object target;
    
        /**
         * 提供有参构造器
         */
        public MyProxy(Object target){
            this.target = target;
        }
    
        /**
         * 提供方法【创建代理对象】
         */
        public Object getProxyObject(){
            Object proxyObj = null;
            //类加载器
            ClassLoader classLoader = target.getClass().getClassLoader();
            //目标对象实现接口Class
            Class<?>[] interfaces = target.getClass().getInterfaces();
            //创建代理对象
            proxyObj = Proxy.newProxyInstance(classLoader, interfaces, new InvocationHandler() {
                @Override
                public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                    //获取方法名称
                    String methodName = method.getName();
                    //执行方法前日志
                    MyLogging.methodBefore(methodName,args);
                    //执行目标对象的相应方法【加减乘除】
                    Object rs = method.invoke(target, args);
                    //执行方法后日志
                    MyLogging.methodAfter(methodName,rs);
                    return rs;
                }
            });
            return proxyObj;
        }
    }
    
  • 注意:代理对象不能转换为目标对象【代理对象与目标对象是兄弟关系,不能相互转换】

第十四章 Spring之AOP【重点】

14.1 使用AOP框架AspectJ

  • 概述:

    • Java社区里最完整最流行的AOP框架。
    • 在Spring2.0以上版本中,可以使用基于AspectJ注解或基于XML配置的AOP。
  • 使用步骤

    • 导入jar包

      <!--spirng-aspects的jar包-->
      <dependency>
          <groupId>org.springframework</groupId>
          <artifactId>spring-aspects</artifactId>
          <version>5.3.1</version>
      </dependency>
      
    • 开启AspectJ注解支持

      <!--配置自动扫描的包-->
      <context:component-scan base-package="com.atguigu.aop"></context:component-scan>
      
      <!--开启AspectJ注解支持-->
      <aop:aspectj-autoproxy></aop:aspectj-autoproxy>
      
    • 创建切面类【非核心业务代码提取类】

      @Component	//将当前类的对象装配到IOC容器中
      @Aspect		//标识当前类的对象是一个【切面类】
      public class MyLogging {
          /**
           * 计算之前
           */
          @Before("execution(public int com.atguigu.aop.CalcImpl.add(int,int))")
          public void methodBefore(JoinPoint joinPoint){
              //获取方法名
              String methodName = joinPoint.getSignature().getName();
              //获取参数
              Object[] args = joinPoint.getArgs();
              System.out.println("计算器日志==>"+methodName+"运算,参数:"+ Arrays.toString(args));
          }
      }
      

14.2 AOP概述

  • AOP(Aspect-Oriented Programming,面向切面编程)
    • 是一种通过动态代理实现程序功能扩展和统一维护的一种技术。
    • 横向扩展机制
    • 优势:解决代码分散及代码混乱问题
  • OOP(Object-Oriented Programming,面向对象编程)
    • 纵向继承机制

14.3 AOP相关术语

  • 横切关注点:非核心业务称之为横切关注点【日志功能】

  • 切面(Aspect):将非核心业务提取到一个类中,这个类称之为切面类【MyLogging切面类】

  • 通知(Advice):非核心业务提取到切面后,称之为通知【切面中的日志功能】

  • 目标(Target):被通知的对象【CalcImpl】

  • 代理(Proxy):向目标对象应用通知之后创建的对象,代理对象如下:

    //手动搭建动态代理方式
    Calc proxyObject = (Calc)myProxy.getProxyObject();
    //使用AOP框架AspectJ
    Calc calc = ioc.getBean("calcImpl", Calc.class);
    
  • 连接点(Joinpoint):被通知位置,称之为连接点。【通知之前】

  • 切入点(pointcut):被通知位置,称之为切入点。【通知之后】

14.4 AspectJ之切入点表达式

  • 语法

    定义:execution(访问修饰符 返回值类型 方法名全路径(参数列表类型) )

execution(public int com.atguigu.aop.CalcImpl.add(int,int))
  • 通配符

    【*】

    • 可以代表任意【访问修饰符 返回值类型 】

    • 可以代表任意包名、类名、方法名

    【..】

    • 可以代表任意参数数据类型及数量
  • 重用切入点表达式

    @Pointcut("execution(* com.atguigu.aop.*.*(..))")
    public void myPointCut(){}
    
    @Before("myPointCut()")
    public void methodBefore(JoinPoint joinPoint){}
    

14.5 JoinPoint对象【连接点对象|切入点对象】

  • joinPoint.getArgs():获取参数
  • joinPoint.getSignature():获取方法签名
    • 方法签名:方法名+参数列表
  • JoinPoint子接口:ProceedingJoinPoint对象
    • proceedingJoinPoint.proceed():触发目标对象的相应方法

14.6 AspectJ中5种通知

通知作用:将非核心业务代码【以何种形式】动态织入到核心业务代码中

  • 前置通知

    • 语法:@Before

    • 注意:在业务代码执行之前执行,即使核心业务代码报错也会执行

      @Pointcut("execution(* com.atguigu.aop.*.*(..))")
      public void myPointCut(){}
      
      /**
       * 前置通知
       */
      @Before("myPointCut()")
      public void methodBefore(JoinPoint joinPoint){
          //获取方法名
          String methodName = joinPoint.getSignature().getName();
          //获取参数
          Object[] args = joinPoint.getArgs();
          System.out.println("计算器日志==>"+methodName+"运算,参数:"+ Arrays.toString(args));
      }
      
  • 后置通知

    • 语法:@After

    • 注意:在业务代码执行之后执行,即使核心业务代码报错也会执行

      /**
       * 后置通知
       */
      @After("myPointCut()")
      public void methodAfter(JoinPoint joinPoint){
          //获取方法名
          String methodName = joinPoint.getSignature().getName();
               System.out.println("计算器日志==>"+methodName+"运算,参数:"+Arrays.toString(args));
          
      }
      
  • 返回通知

    • 语法:@AfterReturning

    • 作用:在业务方法返回结果后执行

    • 注意:

      • 触发到return之后执行【正常:报错不会执行返回通知】
      • 要求:returning属性值与接收结果参数名一致
      @AfterReturning(value = "myPointCut()",returning = "rs")
      public void methodAfterReturning(JoinPoint joinPoint,Object rs){
          //获取方法名
          String methodName = joinPoint.getSignature().getName();
          System.out.println("返回通知,计算器日志==>"+methodName+"运算,结果:"+rs);
      }
      
  • 异常通知

    • 语法:@AfterThrowing

    • 作用:在业务代码中出现异常时执行

    • 注意:要求throwing属性值与接收异常参数名一致

      /**
       * 异常通知
       * @param joinPoint
       * @param ex
       */
      @AfterThrowing(value = "myPointCut()",throwing = "ex")
      public void methodAfterThrowing(JoinPoint joinPoint,Exception ex){
          //获取方法名
          String methodName = joinPoint.getSignature().getName();
          System.out.println("异常通知,计算器日志==>"+methodName+"运算,异常:"+ex);
      }
      
  • 环绕通知

    • 语法:@Around

    • 作用:环绕通知,相当于所有通知整合【前置通知、后置通知、返回通知、异常通知】

    • 注意:

      • 入参使用ProceedingJoinPoint对象

      • 必须设置环绕通知方法的返回值,如未设置或未接收返回值,会报错:

        Null return value from advice does not match primitive return type for: public abstract int com.atguigu.aop.Calc.add(int,int)
        
      @Around("myPointCut()")
      public Object methodAround(ProceedingJoinPoint pjp){
          //获取方法名
          String methodName = pjp.getSignature().getName();
          //获取参数
          Object[] args = pjp.getArgs();
          //返回值
          Object rs = null;
          try {
              //前置通知   后置通知 返回通知  异常通知
              System.out.println("前置通知,计算器日志==>"+methodName+"运算,参数:"+ Arrays.toString(args));
              //执行目标对象的相应方法【加减乘除】
              rs = pjp.proceed();
              //返回通知
              System.out.println("返回通知,计算器日志==>"+methodName+"运算,结果:"+rs);
          } catch (Throwable throwable) {
              throwable.printStackTrace();
              //异常通知
              System.out.println("异常通知,计算器日志==>"+methodName+"运算,异常:"+throwable);
          } finally {
              //后置通知
              System.out.println("后置通知,计算器日志==>"+methodName+"运算,参数:"+Arrays.toString(args));
          }
          return rs;
      }
      

14.7 AspectJ之切面优先级

  • 语法:@Order(value=index)

    • index是int类型,默认值:int可存储最大值
    • 一般使用正整数,数值越小优先级越高
  • 示例代码

    @Component      //标识普通组件
    @Aspect         //标识切面类
    @Order(2)
    public class MyLogging {}
    
    @Component
    @Aspect
    @Order(1)
    public class MyValidate {}
    

14.8 AspectJ之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">

    <!--配置计算器实现类-->
    <bean id="calculator" class="com.atguigu.spring.aop.xml.CalculatorImpl"></bean>

    <!--配置切面类-->
    <bean id="loggingAspect" class="com.atguigu.spring.aop.xml.LoggingAspect"></bean>

    <!--AOP配置-->
    <aop:config>
        <!--配置切入点表达式-->
        <aop:pointcut id="pointCut"
                      expression="execution(* com.atguigu.spring.aop.xml.Calculator.*(..))"/>
        <!--配置切面-->
        <aop:aspect ref="loggingAspect">
            <!--前置通知-->
            <aop:before method="beforeAdvice" pointcut-ref="pointCut"></aop:before>
            <!--返回通知-->
            <aop:after-returning method="returningAdvice" pointcut-ref="pointCut" returning="result"></aop:after-returning>
            <!--异常通知-->
            <aop:after-throwing method="throwingAdvice" pointcut-ref="pointCut" throwing="e"></aop:after-throwing>
            <!--后置通知-->
            <aop:after method="afterAdvice" pointcut-ref="pointCut"></aop:after>
            <!--环绕通知-->
            <aop:around method="aroundAdvice" pointcut-ref="pointCut"></aop:around>
        </aop:aspect>
    </aop:config>
</beans>

第十五章 Spring之JdbcTemplate

15.1 JdbcTemplate概述

  • JdbcTemplate是一个小型的轻量级持久化层框架

15.2 搭建JdbcTemplate环境

  • 导入相关jar包

    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-context</artifactId>
        <version>${spring-version}</version>
    </dependency>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-orm</artifactId>
        <version>${spring-version}</version>
    </dependency>
    
    <!--导入druid的jar包-->
    <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>druid</artifactId>
        <version>${druid-version}</version>
    </dependency>
    <!--导入mysql的jar包-->
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <version>${mysql-version}</version>
    </dependency>
    <!--junit-->
    <dependency>
        <groupId>junit</groupId>
        <artifactId>junit</artifactId>
        <version>${junit-version}</version>
        <scope>test</scope>
    </dependency>
    
  • 将JdbcTemplate装配到IOC容器中

    <!--    加载外部属性文件-->
    <context:property-placeholder location="classpath:db.properties"></context:property-placeholder>
    <!--    装配DataSource-->
    <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
        <property name="driverClassName" value="${db.driverClassName}"></property>
        <property name="url" value="${db.url}"></property>
        <property name="username" value="${db.username}"></property>
        <property name="password" value="${db.password}"></property>
    </bean>
    <!--    将JdbcTemplate装配到IOC容器中-->
    <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
        <property name="dataSource" ref="dataSource"></property>
    </bean>
    
  • 使用JdbcTemplate相关API

    ApplicationContext context =
            new ClassPathXmlApplicationContext("applicationContext_jdbcTemplate.xml");
    
    JdbcTemplate jdbcTemplate = context.getBean("jdbcTemplate", JdbcTemplate.class);
    
    System.out.println("jdbcTemplate = " + jdbcTemplate);
    //添加部门信息
    String sql = "INSERT INTO tbl_dept(dept_name) VALUES(?)";
    jdbcTemplate.update(sql,"董事部门2");
    

day09

15.3 JdbcTemplate常用API

  • 通用增删改
    • update(String sql,Object... param);
  • 通用批量增删改
    • batchUpdate(String sql,List<Object[]>);
  • 通用查询单个值
    • queryForObject(String sql,Class clazz,Object... param);
  • 通用查询单个对象
    • queryForObject(String sql,RowMapper mapper, Object... param);
  • 通用查询多个对象
    • query(String sql,RowMapper mapper,Object... param);

15.4 使用JdbcTemplate封装Dao层

<!--    将JdbcTemplate装配到IOC容器中-->
<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
    <property name="dataSource" ref="dataSource"></property>
</bean>
@Repository("employeeDao")
public class EmployeeDaoImpl implements EmployeeDao {

    @Autowired
    @Qualifier("jdbcTemplate")
    private JdbcTemplate jdbcTemplate;

    @Override
    public List<Employee> selectAllEmps() {
        String sql = "select id,last_name,email,salary from tbl_employee";
        RowMapper<Employee> rowMapper = new BeanPropertyRowMapper<>(Employee.class);
        return jdbcTemplate.query(sql,rowMapper);
    }

}
@Test
public void testDao(){

    ApplicationContext context =
            new ClassPathXmlApplicationContext("applicationContext_jdbcTemplate.xml");

    EmployeeDao employeeDao = context.getBean("employeeDao", EmployeeDaoImpl.class);

    System.out.println("***************");

    for (Employee employee : employeeDao.selectAllEmps()) {
        System.out.println("employee = " + employee);
    }

}

第十六章 Spring 中事务管理

事务回顾

  • 事务四大特性
    • 原子性
    • 一致性
    • 隔离性
    • 持久性
  • 事务三种操作
    • 开启事务:connection.setAutoCommit(false);
    • 提交事务:connection.commit();
    • 回滚事务:connection.rollback();

16.1 Spring中支持两种事务管理

  • 编程式事务管理【使用原生的JDBC API进行事务管理的步骤:】

    1. 获取数据库连接Connection对象

    2. 取消事务的自动提交

    3. 执行操作【核心业务】

    4. 正常完成操作时手动提交事务

    5. 执行失败时回滚事务

    6. 关闭相关资源

    • 不足:事务管理代码【非核心业务代码】与核心业务代码相耦合。【代码分散、代码混乱】
    • 解决方案:使用AOP思想解决事务管理问题【先将事务管理代码横向提取,再动态织入回到业务代码中】
  • 声明式事务管理:使用AOP解决方案解决事务管理方式。

16.2 Spring中声明式事务管理使用步骤

  • 添加AspectJ支持

    <!--spring-aspects-->
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-aspects</artifactId>
        <version>${spring-version}</version>
    </dependency>
    
  • 配置声明式事务管理

    • 配置事务管理器
    • 开启声明式事务管理
        <!--    将事务管理器装配IOC容器中-->
    <!--    <bean id="dataSourceTransactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">-->
    <!--        <property name="dataSource" ref="dataSource"></property>-->
    <!--    </bean>-->
        <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
            <property name="dataSource" ref="dataSource"></property>
        </bean>
    
        <!--    开启声明式事务管理-->
    <!--    <tx:annotation-driven transaction-manager="dataSourceTransactionManager"></tx:annotation-driven>-->
        <tx:annotation-driven></tx:annotation-driven>
    
  • 使用@Transactional为需要业务代码中添加事务管理

    @Transactional
    public void purchase(String username, String isbn) {
        //查询book价格
        Integer price = bookShopDao.findBookPriceByIsbn(isbn);
        //修改库存
        bookShopDao.updateBookStock(isbn);
        //修改余额
        bookShopDao.updateUserAccount(username, price);
    }
    

16.3 Spring声明式事务管理相关属性

  • 事务传播行为【Propagation】

    • 概述:当事务方法被另一个事务方法调用时,必须指定事务应该如何传播。

      • 如:执行method1()方法【x事务】后,调用method2()【y事务】,此时需要设置method2()方法事务的传播行为。
    • 7种传播行为

      传播属性 描述
      REQUIRED 如果有事务在运行,当前的方法就在这个事务内运行;否则就启动一个新的事务,并在自己的事务内运行。
      REQUIRES_NEW 当前的方法*必须*启动新事务,并在自己的事务内运行;如果有事务正在运行,应该将它挂起。
      SUPPORTS 如果有事务在运行,当前的方法就在这个事务内运行,否则可以不运行在事务中。
      NOT_SUPPORTED 当前的方法不应该运行在事务中,如果有运行的事务将它挂起
      MANDATORY 当前的方法必须运行在事务中,如果没有正在运行的事务就抛出异常。
      NEVER 当前的方法不应该运行在事务中,如果有正在运行的事务就抛出异常。
      NESTED 如果有事务正在运行,当前的方法就应该在这个事务的嵌套事务内运行,否则就启动一个新的事务,并在它自己的事务内运行。
    • 图解事务传播行为

      • REQUIRED

        image-20220312142825086

        • 事务执行机制:只有一个Tx1事务
      • REQUIRES_NEW

        image-20220312142838707

        • 事务执行机制:事务1-2-1-3-1
    • 使用场景

      /*
      需求1:去结账之前判断余额,如余额不足:所有图书不让购买
          @Transactional(propagation = Propagation.REQUIRED)
      需求2:去结账之前判断余额,如余额不足:最后导致余额不足那本书不让购买
          @Transactional(propagation = Propagation.REQUIRES_NEW)
      */
      
  • 事务隔离级别【Isolation】

    • 隔离级别概述:事务与事务之间隔离等级【1,2,4,8】

    • 隔离等级

      • 读未提交【1】:READ UNCOMMITTED

        • 存在问题,可能出现脏读现象:可读到未提交数据
      • 读已提交【2】:READ COMMITTED

        • 存在问题,可能出现不可重复度现象
      • 可重复读【4】:REPEATABLE READ

      • 串行化【8】:SERIALIZABLE

    • 图解

  • 事务超时:timeout

  • 事务只读:readonly

    • 一般在查询中使用事务只读属性,可以提高一点性能
  • 事务回滚:

    • rollbackFor:设置回滚的异常Class
    • noRollbackFor:设置不回滚的异常Class

16.4 基于XML方式配置声明式事务管理

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

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

    <!--配置数据源-->
    <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
        <property name="username" value="${jdbc.username}"></property>
        <property name="password" value="${jdbc.password}"></property>
        <property name="url" value="${jdbc.url}"></property>
        <property name="driverClassName" value="${jdbc.driverClassName}"></property>
        <property name="initialSize" value="${jdbc.initialSize}"></property>
        <property name="maxActive" value="${jdbc.maxActive}"></property>
    </bean>

    <!--配置JdbcTemplate-->
    <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
        <!--配置数据源属性-->
        <property name="dataSource" ref="dataSource"></property>
    </bean>

    <!--配置BookShopDaoImpl-->
    <bean id="bookShopDao" class="com.atguigu.spring.tx.xml.BookShopDaoImpl">
        <property name="jdbcTemplate" ref="jdbcTemplate"></property>
    </bean>

    <!--配置BookShopServiceImpl-->
    <bean id="bookShopService" class="com.atguigu.spring.tx.xml.BookShopServiceImpl">
        <property name="bookShopDao" ref="bookShopDao"></property>
    </bean>

    <!--配置事务管理器-->
    <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <!--配置数据源属性-->
        <property name="dataSource" ref="dataSource"></property>
    </bean>

    <!--配置声明式事务-->
    <tx:advice id="tx" transaction-manager="transactionManager">
        <!--设置添加事务的方法-->
        <tx:attributes>
            <!--设置查询的方法的只读属性为true-->
            <tx:method name="find*" read-only="true"/>
            <tx:method name="get*" read-only="true"/>
            <tx:method name="purchase" propagation="REQUIRES_NEW" isolation="READ_COMMITTED"></tx:method>
        </tx:attributes>
    </tx:advice>

    <!--AOP配置-->
    <aop:config>
        <!--配置切入点表达式-->
        <aop:pointcut id="pointCut"
                      expression="execution(* com.atguigu.spring.tx.xml.BookShopServiceImpl.purchase(..))"/>
        <!--将事务方法和切入点表达式关联起来-->
        <aop:advisor advice-ref="tx" pointcut-ref="pointCut"></aop:advisor>
    </aop:config>

</beans>

第十七章 Spring5新特性

17.1 新添加一些注解

名称 含义 可标注位置
@Nullable 可以为空 @Target({ElementType.*METHOD*, ElementType.*PARAMETER*, ElementType.*FIELD*}
@NonNull 不应为空 @Target({ElementType.*METHOD*, ElementType.*PARAMETER*, ElementType.*FIELD*})
@NonNullFields 在特定包下的字段不应为空 @Target(ElementType.*PACKAGE*) @TypeQualifierDefault(ElementType.*FIELD*)
@NonNullApi 参数和方法返回值不应为空 @Target(ElementType.*PACKAGE*) @TypeQualifierDefault({ElementType.*METHOD*, ElementType.*PARAMETER*})
  • 以@Nullable注解为例

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

    ​ 此注解通常用来消除NullPointerException

17.2 Spring5整合Log4j2

  • 导入jar包

    <dependency>
        <groupId>org.apache.logging.log4j</groupId>
        <artifactId>log4j-slf4j-impl</artifactId>
        <version>2.11.2</version>
        <scope>test</scope>
    </dependency>
    
  • 编写配置文件【log4j2.xml】

    <?xml version="1.0" encoding="UTF-8"?>
    <!--日志级别以及优先级排序: OFF > FATAL > ERROR > WARN > INFO > DEBUG > TRACE > ALL -->
    <!--Configuration后面的status用于设置log4j2自身内部的信息输出,可以不设置,当设置成trace时,可以看到log4j2内部各种详细输出-->
    <configuration status="INFO">
        <!--先定义所有的appender-->
        <appenders>
            <!--输出日志信息到控制台-->
            <console name="Console" target="SYSTEM_OUT">
                <!--控制日志输出的格式-->
                <PatternLayout pattern="%d{yyyy-MM-dd HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n"/>
            </console>
        </appenders>
        <!--然后定义logger,只有定义了logger并引入的appender,appender才会生效-->
        <!--root:用于指定项目的根日志,如果没有单独指定Logger,则会使用root作为默认的日志输出-->
        <loggers>
            <root level="DEBUG">
                <appender-ref ref="Console"/>
            </root>
        </loggers>
    </configuration>
    

17.3 Spring5整合Junit5

  • 导入jar包【注意:将junit4jar包删除】

    <!--spring-test-->
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-test</artifactId>
        <version>5.3.1</version>
    </dependency>
    
    <!--junit5-->
    <dependency>
        <groupId>org.junit.jupiter</groupId>
        <artifactId>junit-jupiter-api</artifactId>
        <version>5.7.2</version>
        <scope>test</scope>
    </dependency>
    
  • 使用测试类

    /**
     * @author Chunsheng Zhang 尚硅谷
     * @create 2022/3/12 16:26
     */
    //@ContextConfiguration(locations = "classpath:applicationContext_transaction.xml")
    //@ExtendWith(SpringExtension.class)
    @SpringJUnitConfig(locations = "classpath:applicationContext_transaction.xml")
    public class TestSpringAndJunit5 {
    
        @Autowired(required = true)
        @Qualifier("cashierService")
        private CashierService cashierService;
    
        @Autowired
        private BookShopService bookShopService;
    
        @Test
        public void testJunit5(){
            System.out.println("cashierService = " + cashierService);
            System.out.println("bookShopService = " + bookShopService);
        }
    }
    
posted @   jiejie0830  阅读(109)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 全网最简单!3分钟用满血DeepSeek R1开发一款AI智能客服,零代码轻松接入微信、公众号、小程
· .NET 10 首个预览版发布,跨平台开发与性能全面提升
· 《HelloGitHub》第 107 期
· 全程使用 AI 从 0 到 1 写了个小工具
· 从文本到图像:SSE 如何助力 AI 内容实时呈现?(Typescript篇)
点击右上角即可分享
微信分享提示