spring

spring介绍
是一个轻量级的控制反转(IOC),面向切面编程(AOP)的框架(容器),AOP的主要作用就是支持事务的处理

  • springboot 是一个快速开发的脚手架
    -- 基于springboot可以快速开发单个微服务
    -- 约定大于配置

  • springcloud
    -- springcloud是基于springboot实现的

现在多数公司在使用springboot进行快速开发,学习springboot需要先学习spring和springMVC

spring理念:整合现有技术,是个大杂烩。弊端就是配置十分繁琐

2. IOC:控制反转

什么叫控制反转,本来程序的主动权在程序员手里,客户需要什么,程序员调用什么。控制反转就是把控制权转交到客户手里,客户需要什么就自己选择,不需要程序员调用


2.1 XML

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


2.11 spring 对bean管理

  • 创建bean的三种方式

    • 创建方式一:
      • 使用默认构造函数
        spring的配置文件使用bean标签且只有id和class属性,没有其他属性和标签,
      • 如果没有默认构造函数,就不能创建
  •  <bean id="accountService" class="service.impl.AccountServiceImpl"/>
    
    • 创建方法二:
      • 使用jar包中的方法创建对象(某个类中的方法创建对象),并存入spring容器
  • <bean id="instanceFactory" class="com.gx.InstanceFactory"/>
    <bean id="accountService" factory-bean="instancefactory" factory-method="saveAccount"/>
    
    • 创建方法三:
      • 使用工厂中的静态方法创建对象(某个类中的静态方法),并存入spring容器
        static不需要实例化没,所以可以直接合并成一句话
  •  <bean id="staticFactory" class="com.gx.staticFactory" factory-method="saveAccount"></bean>
    

  • 2.12 bean对象的作用范围

    • bean的scope属性: 用于指定bean的作用范围
      • 取值:singleton:单例的(默认值)
        • prototype:多例的
        • request:作用web请求的请求范围
        • session:作用于web应用的会话范围
        • blobal-session;作用于全局会话(集群)范围,不是集群范围就是session
  •    <bean id="accountService" class="service.impl.AccountServiceImpl" scope="prototype"/>
    

  • 2.13 bean对象的生命周期

    • 单例对象:单例对象的生命周期跟随容器
      • 出生:但容器创建时,对象就出生
      • 存活:容器活着对象就活着
      • 死亡:容器销毁,对象死亡
    • 多例对象:
      • 出生:当我们使用对象时spring框架为我们创建
      • 存活:单对象在使用中就存活
      • 死亡:单对象长时间不用,且没有别的对象应用,就被被垃圾回收
  •    <bean id="accountService" class="service.impl.AccountServiceImpl" scope="prototype" init-method="init" destroy-method="destroy"/>
    

  • 2.14 spring依赖注入

    • 依赖注入DI:Dependency Injection:降低程序的耦合
      • 能注入的数据由三种:
        • 基本类型和string
        • 其他bean类型(在配置文件中活着注解配置的bean)
        • 复杂类型、集合类型
      • 注入的方式三种:
        • 构造函数提供
        • 使用set方法
        • 使用注解
  • 构造函数注入
    • construct-arg :因为默认构造已经不见了,所以要使用标签:construct-arg
      • 标签中的属性:
        • type:指定要注入的数据类型
        • index:索引位置,指定要注入的数据,从0开始
          -(常用)name:给指定名称赋值
        • value:给基本类型和String类型数据
        • ref:给其他类型数据,在spring容器中出现过的
    • 优势:创建bean对象时,必须注入数据,否赵不能创建
    • 劣势:改变了bean实例化,当我们用不到某些数据也必须赋值
      所以除了非用不可之外,一般使用set方法
  •   <bean id="accountService" class="service.impl.AccountServiceImpl">
      <constructor-arg name="name" value="YY"></constructor-arg>
      <constructor-arg name="age" value="18"></constructor-arg>
      <constructor-arg name="birthday" ref="date"></constructor-arg>
      </bean>
      <bean id="date" class="java.util.Date" ></bean> 
    

  • set方法注入
    • 标签: property,出现在bean内部
      • 标签中的属性:
        • name:给指定名称赋值,这边的name不是变量名,而是set的名称
        • value:给基本类型和String类型数据
        • ref:给其他类型数据,在spring容器中出现过的类里没有默认构造函数不能使用
    • 优势;创建对象没有明确限制,可以直接使用默认构造方法
    • 弊端:没有set方法就不能注入
  •   <bean id="accountService2" class="service.impl.AccountServiceImpl2">
      <property name="name" value="YY"/>
      <property name="age" value="18"/>
      <property name="birthday" ref="date"/>
      </bean>
      <bean id="date" class="java.util.Date" ></bean>        
    

  • 复杂类型、集合类型的注入
    • 用于给List结构注入的标签:
      • list array set
    • 用于Map结构集合注入的标签:
      • map props
    • 结构相同,可以互换,所以只要记两组就行了
  •   <bean id="accountService3" class="service.impl.AccountServiceImpl3">
           <property name="strings">
               <array>
                   <value>YY</value>
                   <value>YZY</value>
               </array>
           </property>
           <property name="set">
               <set>
                   <value>YY</value>
                   <value>YZY</value>
               </set>
           </property>
           <property name="map">
               <map>
                   <entry key="A" value="B"></entry>
               </map>
           </property>
           <property name="list">
               <array>
                   <value>YY</value>
                   <value>YZY</value>
               </array>
           </property>
           <property name="properties">
               <props>
                   <prop key="A" >a</prop>
                   <prop key="B" >b</prop>
               </props>
           </property>
       </bean>
       <bean id="person" class="entity.Person" autowire="byType">
    
       </bean>
       <bean id="dog" class="entity.Dog"/>
       <bean id="cat" class="entity.Cat"/>
    

2.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"
       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/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">
</beans>

注解的第一种方式:不用@Component,直接类在xml里手动注入

<!--支持注解模式-->
    <context:annotation-config></context:annotation-config>
    <!--使用@Autowired注解就不用写property标签属性了,也可以在bean后面加autowired属性-->
    <bean id="person" class="entity.Person"></bean>
    <bean id="dog" class="entity.Dog"/>
    <bean id="cat" class="entity.Cat"/>

注解的第二种方式:使用@Component,等价于<bean id=""class=""/>

  • @Controller:一般用在表现层
  • @Service:业务层
  • @Repository:持久层
<!--指定要扫描的包,包下的注解会生效,其实这条写了,支持注解模式那条就已经包含进去了-->
    <context:component-scan base-package="entity"></context:component-scan>
    @Autowired
    @Qualifier(value = "accountDao2")
    //这哥们一个顶上面俩
    @Resource(name = "accountDao1")
    private Dog dog;

  • 2.21 注入数据的注解

        相当于配置文件中的<bean>标签下的<property>标签
        使用注解注入时就可以不用set方法了
    
    • @Autowired:
      自动按照类型注入只要容器中有唯一的bean对象类型和注入的类型匹配,就可以注入成功

      • 注意点 用于匹配指定类型,但是我拥有两个相同的类所以就会无法匹配到确定的一个类上,这时候就匹配与变量名相同的bean,如果都不对就报错,这时候
        加上@Qualifier指定变量名
    • @Qualifier
      在按照类注入的基础上按变量名注入,在给类成员注入时不能单独使用,但是方法注入时可以
      属性:value
      指定bean的id值

    • @Resource :上面两个的集合体

      • 属性 :name
        指定bean的id值
  • 以上三个注入都只能注入其他bean类型的数据,基本类型无法使用,集合类型的只能在xml文件实现

    • @Value:用于注入基本类型和string
      • 属性:value
        用于指定数据的值
      •        @Value(value="lanmao") 相当于<property id="cat" value="lanmao">
                   private Cat cat;
        

2.22 注解作用范围

相当于配置文件中的<bean>标签中的scope属性

  • @Scope :指定bean的作用范围
    • 属性:value 指定范围的取值 这个和配置文件中一样
  •                   @Scope(value = "prototype")
    

2.23 注解生命周期(了解)

  • 相当于配置文件中的<bean>标签中的init-method和destroy-method属性

  • @PostConstruct:创建方法

    •        @PostConstruct: 创建方法
             public void init(){
             System.out.println("对象出生");}  
      
  • @PreDestroy :销毁方法

    •       public void destroy(){
            System.out.println("对象死亡");
            }
      

小结 xml和注解

  • xml: 更万能,维护简单
  • 注解:不是自己的类用不了,维护复杂

最佳实践

  • xml用来管理bean
  • 注解负责属性注入

3. 代理模式

为什么要学习代理模式,因为这是SpringAOP的底层

是二十三种设计模式之一,springAOC和springMVC必考

  • 代理模式的分类

    • 静态代理
    • 动态代理

3.1 静态代理

角色分析

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

步骤:

  1. 接口(租房操作)
//租房接口
public interface Rent {
    
    public void rent();
    
}
  1. 真实角色(房东)
//房东要租出去的房子
public class House implements Rent {
    String housName;

    public String getHousName() {
        return housName;
    }

    public House(String housName){
       this.housName = housName;
    }

    public void rent(){
        System.out.println("房东要出租房子"+housName);
    }
}
  1. 代理角色(中介)
//代理
public class Proxy implements Rent{

    private House house;

    public Proxy(){

    }
    //代理去帮你租这个房子
    public Proxy(House house){
        this.house = house;
    }
//    房子被代理租出去了
    @Override
    public void rent(){
        house.rent();
        seeHouse(house.getHousName());
        money(house.housName);
        fire(house.getHousName());
    }
    public void seeHouse(String housName){
        System.out.println("中介带你看房"+ housName);
    }
    public void money(String housName){
        System.out.println("中介收费"+ housName);
    }
    public void fire(String housName){
        System.out.println("签合同"+ housName);
    }


}
  1. 客户端访问代理角色(租客)
public class Client {
    public static void main(String[] args) {
//        看中了某个房子
        House house = new House("无名公寓");
//        找中介
        Proxy proxy = new Proxy(house);
//        中介的附属操作,不用和房东接触
        proxy.rent();

    }

}

静态代理好处:

  • 使真实角色(租客,房东)更加操作纯粹,不用关注公共业务(看房,签合同)
  • 公共业务交给了代理(中介),实现了业务的分工(签合同,看房)
  • 公共业务发生拓展的时候,方便管理

缺点

  • 一个真实角色就要一个代理,代码量翻倍,开发效率低

3.2 静态代理加深理解

有一个service类用于使用增删改查的操作

public class UserServiceImpl implements UserService{

    //        如果我还想再方法里加语句就很麻烦,不方便去改源代码,那么这个时候就要用代理类

    @Override
    public void add() {
        System.out.println("增");
    }

    @Override
    public void delete() {
        System.out.println("删");
    }

    @Override
    public void update() {
        System.out.println("改");
    }

    @Override
    public void query() {
        System.out.println("查");
    }
}

现在我想加一个日志功能,能不能在service类里直接修改呢?

最好不要,因为直接修改源代码可能会出错,而且如果代码量多的话十分麻烦,那么我们可以通过一个代理类来增加功能。

public class UserServiceProxy implements UserService {
    private UserService userService;
//    在代理类里调用并加上日志
public void setUserService(UserService userService) {
    this.userService = userService;
}
    public void log(String callname){
        System.out.println("调用了一次"+callname+"方法");
    }
    @Override
    public void add() {
        userService.add();
        log("add");
    }
    @Override
    public void delete() {
        userService.delete();
        log("delete");
    }
    @Override
    public void update() {
        userService.update();
        log("update");
    }
    @Override
    public void query() {
        userService.query();
        log("query");
    }
}

这就是只拓展不修改,不在改变原有代码的基础上增加新功能

没有加一层封装解决不了的方法,如果有,就再加一层。

横切.png

不改变业务的情况下,我们要增加功能,日志就要横切进去,这就是横向开发,这就是切面!

3.3 动态代理

  • 动态代理和静态代理角色是一样的
  • 动态代理是动态生成的,不是我们直接写好的
  • 分为两大类
    • 基于接口理:基于JDK动态代理(我们学习的)
    • 基于类:cglik

需要了解两个类:

  • Proxy:代理
    • 提供静态方法方法能创建动态代理和实例
  • invocationHandler:调用处理程序
    • 每一个代理类都会有一个调用处理程序,当代理类调用该方法,代理类分配到invoke方法通过反射反射

3.3.1 如果是单独创建一个动态代理类然后main方法调用

 import java.lang.reflect.Method;//用这个类自动生成代理类
 public class ProxyInvocationhandler implements InvocationHandler {
     //    被代理的接口
     private Object target;
 
     public void setRent(Object target) {
         this.target = target;
     }
     /*newProxyInstance方法的参数:
         classLoader:类加载器
             用于加载代理对象字节码。和被代理对象使用 相同的字节码,固定写法
     class[]:字节码数组
             用于让代理对象和被代理对象有相同的方法。固定写法
     InovocationHandler:用于提供增强的代码
             让我们写如何代理。通常情况下都是匿名内部类,但不是必须 */
     //得到代理类
     public Object getProxy() {
         return Proxy.newProxyInstance(this.getClass().getClassLoader(), target.getClass().getInterfaces(), this);
     }
 
 //    处理实例并返回结果
     @Override
     public Object invoke(Object proxy,Method method,Object[] args){
//                      增强功能
                        log("YY");
//                          返回target类的方法和方法的参数
        return method.invoke(target,args);
}
     public void log(String callname){
         System.out.println("调用了一次"+callname+"方法");
     }
 }

3.32 如果是直接写在main方法里

public class Client {
public static void main(String[] args){
  final House house = new House();
        /*newProxyInstance方法的参数:
                 classLoader:类加载器
                     用于加载代理对象字节码。和被代理对象使用 相同的字节码,固定写法
             class[]:字节码数组
                     用于让代理对象和被代理对象有相同的方法。固定写法
             InovocationHandler:用于提供增强的代码
                     让我们写如何代理。通常情况下都是匿名内部类,但不是必须 */
//                          前两个都是写被代理类的类加载器
    Rent12 rent12Proxy = (Rent12)Proxy.newProxyInstance(house.getClass().getClassLoader(),house.getClass().getInterfaces,
                      new InvocationHandler() {
                    @Override
                   /* proxy:当前代理对象的引用
                      method:当前执行的方法
                      args:执行方法需要的参数
                    **/
//          这里就相当于代理类得到了真实角色的方法,可以再里面加功能,最后在main方法里调用你想要的代理类.方法
                public Object invoke(Object proxy,Method method,Object[] args) throws Throwable{
                            if("rent".equals(method.getName())){
                                System.out.println("房子被代理了");
                            }
                    return method(house,args);
                }
});
            rent12Proxy.rent();
}
}

注意点:代理类的类型为接口类型而不是被代理类的类型

通俗的讲:代理类就是集成被代理类的接口,然后再把被代理类的具体方法方法拿过来用进行可以拓展,
然后集合成一个调用处理程序,想用就直接调用这个程序就行了。


3.33 如果是直接写在main方法里

上一个方法是接口代理,那么这个方法就是子类代理,需要用到一个第三方jar包cglib

package 动态代理子类;

import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;
import 动态代理黑马.Rent12;
import 增加日志功能.UserService;
import 增加日志功能.UserServiceImpl;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;

/**
 * @author YZY
 * @date 2020/3/31 - 20:33
 */
public class Client {
    public static void main(final String[] args) {
        final House1 house1 = new House1();
        house1.setHouseName("无名公寓");
/*动态代理子类需要第三方的jar包cglib
*   使用Enhancer的create方法
        create方法的参数:
        class:字节码
            用于指定被代理类的字节码,固定写法

    callback:用于提供增强的代码
            让我们写如何代理。通常情况下都是匿名内部类,但不是必须
            一般写的是该接口的子接口实现类:MethodInterceptor*/
//       相比于代理接口,代理子类少了一个参数,getInterfaces(),因为直接代理子类,就不需要子类的接口了
//        这里就直接选择子类类型就行了
      House1 house1Proxy = (House1) Enhancer.create(house1.getClass(), new MethodInterceptor() {
            /*前三个参数和代理接口的参数一样
            * methodProxy:当前执行方法的代理对象
            * 其他内容就和代理接口一模一样*/
            @Override
            public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
                if ("rent".equals(method.getName())) {
                    System.out.println("动态代理接口");
                }
                return method.invoke(house1, args);
            }
      });
        house1Proxy.rent();
    }
}

3.33 动态代理相比于静态代理的优势:

静态代理代理的是一个业务,而动态代理继承的是接口,可以代理一类业务,范围范围更广,应用更加灵活


4 AOP

使用AOP需要导包

<dependency>
            <groupId>org.aspectj</groupId>
            <artifactId>aspectjweaver</artifactId>
            <version>1.9.4</version>
        </dependency>

4.1 第一种方式:使用spring的API接口

配置复杂但是功能强大

先创建继承接口的类

public class methodBeforeLog implements MethodBeforeAdvice {
    @Override
    /*method:方法
    args:参数
    target:目标对象*/
    public void before(Method method, Object[] args, Object target) throws Throwable {
        System.out.println(target.getClass().getName()+"的"+method.getName()+"被执行了");
    }
}
package log;
import org.springframework.aop.AfterReturningAdvice;
import java.lang.reflect.Method;
public class afterReturnLog implements AfterReturningAdvice {
    @Override
//    returnValue:返回值
    public void afterReturning(Object returnValue, Method method, Object[] args, Object target) throws Throwable {
        System.out.println("执行了"+target.getClass().getName()+"返回的结果是"+returnValue);
    }
}

配置文件:

<?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
        http://www.springframework.org/schema/aop/spring-aop.xsd">
<!--配置bean-->
<bean id="userService" class="service.userService1Impl"></bean>
    <bean id="beforeLog" class="log.methodBeforeLog"></bean>
    <bean id="afterLog" class="log.afterReturnLog"></bean>

    <!--1.使用原生spring API注入-->
<!--配置AOP:需要导入AOP 的约束-->
    <aop:config>
    <!--需要一个切入点:即去哪个地方执行(去userServiceImpl下的所有方法*,的所有位置(..))-->
        <aop:pointcut id="pointcut" expression="execution(* service.userService1Impl.*(..))"/>

        <!--执行环绕-->
<!--        把beforelog这个类连接到切入点-->
        <aop:advisor advice-ref="beforeLog" pointcut-ref="pointcut"/>
        <aop:advisor advice-ref="afterLog" pointcut-ref="pointcut"/>
</aop:config>

</beans>

4.2 第二种方式:自定义java类实现AOP

配置简单但是功能少

先创建一个自定义类

package diy;
public class diyPointcut {

    public void MethodBeforeAdvice(){
        System.out.println("方法执行前");
    }
    public void AfterReturningAdvice(){
        System.out.println("方法返回后");
    }
}

然后写入配置文件

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:aop="http://www.springframework.org/schema/aop"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
        https://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/aop
        http://www.springframework.org/schema/aop/spring-aop.xsd">
<!--    2.自定义类-->
    <!--配置bean-->
    <bean id="userService" class="service.userService1Impl"></bean>
    <bean id="log" class="log.diyPointcut"></bean>
<!--        aop编写范围-->
        <aop:config>
            <!--    切点可以在切面里面也可以在切面外面,在外面的话一定要在切面的前面(aop约束规定)-->
            <aop:pointcut id="pointcut" expression="execution(* service.userService1Impl.*(..))"/>
<!--            自定义切面,切面是个类,ref引用你要的类-->
            <aop:aspect ref="log">
<!--            通知-->
                <!--这里就可以自定义在什么时候切入,
                    aop:brfore代表执行前-->
                <aop:before method="MethodBeforeAdvice" pointcut-ref="pointcut"/>
                <aop:after-returning method="AfterReturningAdvice" pointcut-ref="pointcut"/>
            </aop:aspect>
        </aop:config>
</beans>

4.3 第三种方式:注解

xml文件中添加bean和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
        http://www.springframework.org/schema/aop/spring-aop.xsd">
    <bean id="annotation" class="log.AnnotationPointcut"/>
    <bean id="userService" class="service.userService1Impl"/>
<!--    开启AOP注解-->
    <aop:aspectj-autoproxy></aop:aspectj-autoproxy>
</beans>

在编写通知注解

package log;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.*;
/**
 * @author YZY
 * @date 2020/4/2 - 17:37
 */
// AOP注解
@Aspect    //标志这个类成切面
public class AnnotationPointcut {

//    前置通知
    @Before("execution(* service.userService1Impl.*(..))")
    public void MethodBeforeAdvice(){
        System.out.println("Before");
    }

       /*环绕通知,功能最强大,可以自定义操作
        它可以算是spring框架给我们提供一种手动控制通知(增强)方法的方式,你可以手动在自定义通知(增强)方式
    ProceedingJoinPoint 连接点,可以从切入点里获取信息*/
    @Around("execution(* service.userService1Impl.*(..))")
    public void Around(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
        try{
        System.out.println("around before");
//        获取签名
        Signature signature = proceedingJoinPoint.getSignature();
        System.out.println("signature:"+signature);
//        执行
        Object proceed = proceedingJoinPoint.proceed();
        System.out.println("around after");
        }catch (Exception e){
            System.out.println("around throwing");
        }finally {
            System.out.println("around finnal");
        }
    }

//  返回通知,如果抛出异常不会通知
    @AfterReturning("execution(* service.userService1Impl.*(..))")
    public void AfterReturningAdvice(){
        System.out.println("AfterReturning");
    }
    //  异常通知
    @AfterThrowing("execution(* service.userService1Impl.*(..))")
    public void Throwing(){
        System.out.println("AfterThrowing");
    }
//  最终通知,结果不关心是否异常
    @After("execution(* service.userService1Impl.*(..))")
    public void After(){
        System.out.println("After");
    }
}

最后观察输出结果发现通知顺序是有问题的,而上面提到环绕通知可以手动控制通知方式,所以建议使用环绕通知来处理通知事件

图片.png
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

友情链接更多精彩内容