Java代理模式详解

发布于:2025-09-12 ⋅ 阅读:(18) ⋅ 点赞:(0)

Java代理模式详解

前言:

Java代理模式是23种设计模式中的一种,属于其中的结构性模式。

我们先不去聊什么是静态代理什么是动态代理,先来理解一下“代理”是什么。以我们生活中的现象为例,假如现在你要去买一张演唱会门票,你自己去抢,大概率是抢不到的(就像我们自己去处理一些复杂的业务逻辑一样,费时费力)。于是,你找到了“黄牛”(或者叫票务代理)。

在这个场景里:

  • 你(Client):是最终的客户,你的需求是买票
  • 歌手(Target Object / 被代理对象):是提供演唱会核心服务的人,但他太忙不能直接提供票给你
  • 黄牛/销售平台/等等各种中间商(Proxy / 代理对象):他们就是代理,他们不唱歌,但是能给你票。

黄牛帮你(Client)去接触周杰伦的售票系统(Target),完成了“买票”这个行为。

代理模式的核心思想就是: 不直接访问目标对象,而是通过一个代理对象来间接访问。

这样做有啥好处呢?黄牛(代理)在帮你买票(调用核心功能)前后,可以干很多“附加”的事情,比如:

  1. 加价:在票价基础上加钱,这是他的盈利模式。(功能增强)
  2. 验证身份:问你是不是真的粉丝,是不是黄牛的同行。(访问控制/前置检查)
  3. 记录交易:把你买票的记录下来,以后好继续做你生意。(日志记录)
  4. 打包销售:卖给你票的同时,还卖给你荧光棒、海报等。(事务管理)

对应到我们程序里,这些“附加”的操作就像是 日志记录、性能监控、事务管理、权限控制 等等。这些代码如果写在核心业务(比如 OrderServicecreateOrder 方法)里,会让业务代码变得特别臃肿,而且每个业务方法都可能要重复写,这叫“代码耦合”。

代理模式就是为了解决这个问题,把这些非核心、但又通用的功能(我们称之为 “横切关注点”)从业务代码里抽离出来,让代理去做。业务代码只关心自己的核心逻辑。(讲到这里是不是感觉和AOP很相似啊,抽离出非核心但通用的功能。这个下文再解释)

1. 静态代理(Static Proxy)

静态代理需要我们 手动 为每一个需要被代理的接口(或类)创建一个代理类。

比如,有一个卖火车票的接口 TicketService 和它的实现类 RailwayStation

// 共同接口
interface TicketService {
    void sellTicket();
}

// 目标对象:火车站
class RailwayStation implements TicketService {
    public void sellTicket() {
        System.out.println("====== 火车站卖出一张票 ======");
    }
}

// 代理对象:代售点 (我们需要手动创建这个类)
class TicketProxy implements TicketService {
    private RailwayStation target; // 持有被代理的对象

    public TicketProxy(RailwayStation target) {	//构造方法
        this.target = target;
    }

    public void sellTicket() {
        System.out.println("--- 代售点:开始处理卖票业务 ---"); // 前置增强
        target.sellTicket(); // 调用目标对象的核心方法
        System.out.println("--- 代售点:收取服务费10元 ---");   // 后置增强
    }
}

静态代理的缺点是啥? 很明显,如果现在我们不止卖火车票,还要卖飞机票 (AirTicketService)、汽车票 (BusTicketService) 呢?那我们是不是得为每一个接口都手动创建一个代理类 (AirTicketProxy, BusTicketProxy)?如果接口里的方法变了,代理类和目标类都得改。太麻烦了!项目一大,代理类能堆成山,没法维护。

核心痛点:代理类是“写死”的,在编译期就已经确定了。(从 JVM 层面来说, 静态代理在编译时就将接口、实现类、代理类这些都变成了一个个实际的 class 文件。

2.动态代理:(Dynamic Proxy):进入正题!

为了解决静态代理的痛点,Java 提供了“动态代理”技术。

“动态”这两个字是精髓,它指的是:代理类是在程序运行时,由 JVM 动态生成的,我们不需要手动编写代理类的 .java 文件。

这就牛逼了!不管你有多少个接口,多少个方法,我都可以用一套通用的逻辑来生成代理。

Java 实现动态代理主要有两种方式:JDK 动态代理CGLIB 动态代理。这里先重点讲 JDK 动态代理先从它入手,这是面试最常问的。

2.1吃透JDK动态代理

写在前面:记住,JDK 动态代理有两个最重要的成员:

  1. java.lang.reflect.Proxy:这是生成动态代理类的“工厂”,最核心的方法是 newProxyInstance()
  2. java.lang.reflect.InvocationHandler:这是一个接口,直译过来叫“调用处理器”。所有对代理对象方法的调用,都会被转发到这个处理器的 invoke 方法上。 咱们的增强逻辑(比如打印日志、计算时间)就写在这里面。

感觉有点抽象?但别担心,咱们还是用“买票”的例子,一步一步把代码敲出来。

第一步:定义一个接口

JDK 动态代理是 基于接口 的,也就是说,被代理的那个目标类,必须实现一个或多个接口。(非常之重要的一句话请记住)

// Step 1: 定义一个卖票服务的接口
public interface SellTickets {
    void sell(); // 卖票方法
    void refund(); // 退票方法
}
第二步:创建被代理的目标类

这个类就是我们的“火车站”,它实现了卖票接口,专注于核心业务。

// Step 2: 创建一个目标对象(火车站),实现接口
public class TrainStation implements SellTickets {
    @Override
    public void sell() {
        System.out.println("【核心业务】火车站成功卖出一张票!");
    }

    @Override
    public void refund() {
        System.out.println("【核心业务】火车站办理了退票!");
    }
}
第三步:创建我们需要的InvocationHandler类

这是最关键的一步!我们要创建一个类,实现 InvocationHandler 接口。所有对代理对象的调用,都会跑到这个类的 invoke 方法里。

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

// Step 3: 创建一个调用处理器,我们所有的代理逻辑都在这里
public class MyInvocationHandler implements InvocationHandler {

    // 我们需要知道被代理的真实对象是谁,所以把它传进来
    private Object target;

    public MyInvocationHandler(Object target) {
        this.target = target;
    }

    /**
     * @param proxy  JVM 动态生成的代理对象,我们一般不用它
     * @param method 正在被调用的方法(比如 sell 或 refund)
     * @param args   调用方法时传入的参数
     * @return 方法的返回值
     */
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        
        // 在调用真实方法【之前】,我们可以做一些增强操作
        System.out.println("======== [动态代理] 交易开始 ========");
        System.out.println("======== [动态代理] 调用的方法是: " + method.getName() + " ========");

        // 通过【反射】来调用真实对象的核心方法
        Object result = method.invoke(target, args);

        // 在调用真实方法【之后】,我们也可以做一些增强操作
        System.out.println("======== [动态代理] 交易结束,记录日志 ========");
        
        return result;
    }
}

invoke 方法是核心中的核心!

  • 我们在这里加上了“交易开始”、“交易结束”的日志,这就是所谓的“功能增强”。
  • method.invoke(target, args) 这行代码,就是通过 Java 的反射机制,去真正调用我们那个 TrainStation 对象的 sell() 或者 refund() 方法。

这个invoke与上面方法的invoke虽然名字一样但是是不同的两个方法,这里的invoke是来自于 java.lang.reflect.Method 。它是 Java 反射 API 的一部分。

这是一个 “执行器”。它的作用就是去真正地执行一个方法。

  • 第一个参数 target:告诉它要在 哪个对象 上执行这个方法。(比如,在 trainStation 对象上执行)
  • 第二个参数 args:告诉它执行方法时需要传递的 参数 是什么。(比如 sell("北京", "上海") 的参数)
  • 这样一来,业务代码(TrainStation)和我们的代理逻辑(MyInvocationHandler)就彻底分开了!TrainStation 根本不知道自己被代理了。
第四步:使用Proxy.newProxyInstance()生成代理对象

上述都准备完毕之后,现在只差我们现在用 Proxy 工厂来创建代理实例。(这里是与静态代理直接相差异的步骤)

package com.proxy.demo;

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

public class Client {
    public static void main(String[] args) {
        System.out.println("=============== 代理【火车站】售票业务 ===============");
        // 1. 创建被代理的真实对象:火车站
        TiceketSell trainStation = new TrainStation();
        // 2. 创建一个 InvocationHandler,并把【火车站】对象传进去
        InvocationHandler trainStationHandler = new MyInvocationHandler(trainStation);
        // 3. 使用 Proxy.newProxyInstance() 为【火车站】创建一个代理对象
        TiceketSell trainStationProxy = (TiceketSell) Proxy.newProxyInstance(
                trainStation.getClass().getClassLoader(),
                trainStation.getClass().getInterfaces(),
                trainStationHandler
        );
        // 4. 通过代理对象调用方法
        trainStationProxy.sell();
        System.out.println("---------------------------------------------------");
        trainStationProxy.refund();

        //如果想新增一个业务那么只需要创建并引入一个新的被代理对象
        System.out.println("\n=============== 代理【电影院】售票业务 ===============");

        // 1. 创建新的被代理对象:电影院
        TiceketSell cinema = new Cinema();
        // 2. 复用我们的 InvocationHandler,这次把【电影院】对象传进去
        InvocationHandler cinemaHandler = new MyInvocationHandler(cinema);
        // 3. 使用 Proxy.newProxyInstance() 为【电影院】创建一个代理对象
        TiceketSell cinemaProxy = (TiceketSell) Proxy.newProxyInstance(
                cinema.getClass().getClassLoader(),
                cinema.getClass().getInterfaces(),
                cinemaHandler
        );
        // 4. 通过新的代理对象调用方法(代理逻辑 MyInvocationHandler 一行都不用改,直接复用)
        cinemaProxy.sell();
        System.out.println("---------------------------------------------------");
        cinemaProxy.refund();

        // 还可以看看这个代理对象到底是个啥
        System.out.println("\n代理对象的类型: " + trainStationProxy.getClass());
        System.out.println("\n代理对象的类型: " + cinemaProxy.getClass());
    }
}

我们来拆解一下 Proxy.newProxyInstance() 的三个参数,面试经常问:

  1. ClassLoader loader: 类加载器。JVM 需要用它来加载动态生成的代理类。我们一般直接用被代理类的类加载器就行。
  2. Class<?>[] interfaces: 接口数组。告诉 JVM,新生成的代理类要实现哪些接口。这里我们传入 TrainStation 实现的所有接口。
  3. InvocationHandler h: 调用处理器。把我们自己写的那个 handler 传进去。当代理对象的方法被调用时,JVM 就知道该去执行我们 handler 里的 invoke 方法。

看结果:

在这里插入图片描述

  • 我们调用的明明是 proxyInstance.sell(),但它自动执行了我们 MyInvocationHandler 里的前后增强逻辑。
  • proxyInstance 的类型是 $Proxy0,这是一个由 JVM 在运行时动态创建出来的类,它不是 TrainStation,也不是 MyInvocationHandler,它是一个全新的、实现了 SellTickets 接口的代理类。

3.解决一些小问题:

问题 1:代理模式 vs AOP (面向切面编程)

上面提到代理模式和AOP干的活好像差不多。其实代理模式和 AOP 的目标确实高度一致:都是为了在不修改核心业务代码的前提下,给程序增加额外的通用功能(比如日志、事务),实现代码解耦。

但它们的关系,可以这样理解:

  • AOP (Aspect-Oriented Programming) 是一种编程思想、一种范式 (Paradigm)。
    • 它就像 OOP (面向对象编程) 一样,是一种组织代码的指导思想。OOP 的思想是把世界看作成一个个对象,通过封装、继承、多态来组织代码。
    • 而 AOP 的思想是,把那些分散在各个业务方法中,但功能上又属于同一类的代码(比如每个方法开始时的日志记录,结束时的事务提交),“横切”出来,集中到一个地方管理。这个“地方”就叫做 切面 (Aspect)
  • 代理模式 (Proxy Pattern) 是一种具体的设计模式 (Design Pattern)。
    • 它是一种经过验证的、用来解决特定问题的具体“招式”或“套路”。

所以,它们的关系是:AOP 是一种思想,而动态代理是实现这种思想的一种主流技术手段。

问题2:为什么MyInvocationHandler中用 Object 而不是 TrainStation

这个问题直接关系到我们代码的 通用性可复用性

如果我们这样写:

public class MyInvocationHandler implements InvocationHandler {
    private TrainStation target; // 写死了类型

    public MyInvocationHandler(TrainStation target) {
        this.target = target;
    }
    // ...
}

那这个 MyInvocationHandler 是不是就变成了一个 “火车票专用” 的调用处理器了?它只能接收 TrainStation 类型的对象。

如果我们还想代理卖电影票的 Cinema 对象,怎么办?难道要再复制一份代码,创建一个 MyCinemaInvocationHandler 吗?那不又回到静态代理的老路上了吗?

所以,这里用 Object 的目的是为了让我们的 MyInvocationHandler 变得通用!

Object 是 Java 中所有类的父类。

  • TrainStationObject
  • Cinema 也是 Object
  • 未来任何想代理的对象,它都是 Object

通过使用 Object target,我们的 MyInvocationHandler 就能接收 任意类型 的被代理对象,从而实现了一套代理逻辑,到处使用的效果。这就是面向对象中 多态 的一个典型应用。

问题3:为什么两个invoke的方法中的参数不一样?

这个问题其实上面已经有了部分回答,因为两个invoke的功能和来源完全不同!我们来看这部分代码:

public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {     
        // 在调用真实方法【之前】,我们可以做一些增强操作
        System.out.println("======== [动态代理] 交易开始 ========");
        System.out.println("======== [动态代理] 调用的方法是: " + method.getName() + " ========");

        // 通过【反射】来调用真实对象的核心方法
        Object result = method.invoke(target, args);

        // 在调用真实方法【之后】,我们也可以做一些增强操作
        System.out.println("======== [动态代理] 交易结束,记录日志 ========");
        
        return result;
    }

public Object invoke(Object proxy, Method method, Object[] args)

  • 来源:这个方法来自于 InvocationHandler 接口。是我们 必须实现 的方法。
  • 角色:它是代理机制的 “回调入口”。当你的代理对象(proxyInstance)的任何方法被调用时(比如 proxyInstance.sell()),JVM 会自动来调用 我们写的这个 invoke 方法。我们在这里面写增强逻辑。

Object result = method.invoke(target, args)

  • 来源:这个 invoke 方法来自于 java.lang.reflect.Method 。它是 Java 反射 API 的一部分。
  • 角色:这是一个 “执行器”。它的作用就是去真正地执行一个方法。
    • 第一个参数 target:告诉它要在 哪个对象 上执行这个方法。(比如,在 trainStation 对象上执行)
    • 第二个参数 args:告诉它执行方法时需要传递的 参数 是什么。(比如 sell("北京", "上海") 的参数)

简单来说的调用流程就是: 你调用代理对象的方法 -> JVM 调用我们写的 InvocationHandler.invoke() -> 我们在自己的 invoke 内部,通过反射调用 Method.invoke() -> 最终执行到真实对象的方法

问题4:Proxy.newProxyInstance() 的目的和参数

Proxy.newProxyInstance() 的唯一目的就是:动态地在内存中创建一个代理类的实例对象

我们可以把它想象成一个“代理对象生成工厂”。我们需要向这个工厂提供一份“生产说明书”,告诉它要造一个什么样的代理对象。这份“说明书”就是它的三个参数:

  1. ClassLoader loader (类加载器):
    • 作用:告诉工厂用哪个“车间”来生产和加载这个新的代理类。
    • 怎么理解:Java 中所有的类都是由类加载器加载到内存的。我们动态生成的这个代理类也是一个.class文件(虽然只在内存里),也需要一个类加载器来加载它。通常,我们用被代理对象的类加载器就行,保证它们在同一块区域中。
  2. Class<?>[] interfaces (实现的接口数组):
    • 作用:告诉工厂这个新生产出来的代理对象要长什么样,要有哪些“功能按钮”。
    • 怎么理解:代理对象必须“假装”成被代理对象的样子,这样才能被客户端无缝使用。让它实现相同的接口,就意味着它拥有了和被代理对象完全相同的方法签名。比如,TrainStation 实现了 SellTickets 接口,那么我们生成的代理也必须实现 SellTickets 接口,这样它才会有 sell()refund() 方法。
  3. InvocationHandler h (调用处理器):
    • 作用:告诉工厂,当代理对象的“功能按钮”(方法)被按下时,具体要执行什么操作。
    • 怎么理解:这是代理对象的“灵魂”。虽然代理对象有了 sell() 方法的外壳,但方法体里到底是什么逻辑?就是由我们传入的这个 InvocationHandlerinvoke 方法来决定的。所有对代理对象方法的调用,最终都会被路由到这里。

所以把这三样东西(用哪个车间、造成什么样子、灵魂是什么)交给了 Proxy 工厂,它就能在内存里轻易造出一个全新的、符合我们所有要求的代理对象实例。

4.CGLIB动态代理(Code Generate Library)

在后端,特别是 Spring AOP 的世界里,CGLIB 和 JDK 动态代理是左膀右臂,缺一不可。

搞懂了 JDK 动态代理,再来看 CGLIB,会发现它们思想相通,只是实现方式上走了不同的技术路线。

Part 1: 为什么需要 CGLIB?—— JDK 动态代理的“天花板”

刚才说到,JDK 动态代理有一个硬性要求:被代理的类必须实现一个或多个接口

那问题来了,如果现实中我们接手了一个第三方库的类,或者一个项目中历史遗留的类,它功能很强大,但就是没有实现任何接口。我们想在不改动它源码的情况下,给它的方法增加一些日志、监控等功能,怎么办?

这时候,JDK 动态代理就束手无策了。为了突破这个禁锢,CGLIB 就闪亮登场了。

CGLIB 的核心价值:即使一个类没有实现任何接口,我也能进行代理!

Part 2: CGLIB 的核心思想

我们可以这么理解:JDK 代理是“冒名顶替”,是找了个“同名同姓”的人(实现了相同接口)来办事。

CGLIB 的思路则完全不同,它用的是 继承 (Inheritance)

  • 核心原理:CGLIB 会在运行时,动态地创建一个被代理类的子类。然后,这个子类会重写父类(也就是被代理类)的所有非 final 方法。
  • 如何增强:当调用代理对象(也就是那个子类)的方法时,它不会立刻去执行父类的原始逻辑,而是会先执行我们预设好的拦截逻辑(类似于 JDK 代理里的 invoke 方法),在拦截逻辑里,我们可以选择性地调用父类的原始方法。
Part 3: 手把手带你实践 CGLIB

CGLIB 是一个第三方的代码库,所以第一步我们需要在项目中引入它的依赖。

步骤〇:在 pom.xml 中添加 CGLIB 依赖

如果你的项目是 Maven 项目,打开 pom.xml 文件,在 <dependencies> 标签里加入下面的内容:

<dependency>
    <groupId>cglib</groupId>
    <artifactId>cglib</artifactId>
    <version>3.3.0</version>
</dependency>
步骤一:创建一个被代理的类(注意:它没有实现任何接口)

我们就用一个简单的支付类 AliPay 来举例。

package com.proxy.demo;

// 一个普通的类,没有实现任何接口
public class AliPay {

    public void pay(String name, double money) {
        System.out.println("【核心业务】" + name + " 使用支付宝消费了 " + money + " 元。");
    }
}
步骤二:创建方法拦截器 MethodInterceptor

MethodInterceptor 在 CGLIB 里的角色,就完全等同于 InvocationHandler 在 JDK 代理中的角色。我们所有的增强逻辑都写在这里。

package com.proxy.demo;

import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;
import java.lang.reflect.Method;

public class AliPayMethodInterceptor implements MethodInterceptor {

    /**
     * @param obj    CGLIB生成的代理对象(子类实例)
     * @param method 被拦截的父类方法
     * @param args   方法参数
     * @param proxy  用于调用父类方法的代理对象
     * @return
     * @throws Throwable
     */
    @Override
    public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
        
        System.out.println("====== [CGLIB 代理] 支付前,进行安全检查 ======");

        // 通过 MethodProxy 调用父类的原始方法
        Object result = proxy.invokeSuper(obj, args);

        System.out.println("====== [CGLIB 代理] 支付后,记录支付日志 ======");

        return result;
    }
}

注意关键点:这里调用父类方法,我们用的是 proxy.invokeSuper(obj, args),而不是像 JDK 代理那样用反射 method.invoke(target, args)invokeSuper 效率更高,因为它内部避免了反射。

步骤三:使用 Enhancer 来创建代理对象

Enhancer 在 CGLIB 中的角色,就相当于 JDK 代理里的 Proxy 类,是创建代理对象的工厂。

我们修改一下 Client.java 来看看怎么用它。

package com.proxy.demo;

import net.sf.cglib.proxy.Enhancer;

public class Client {
    public static void main(String[] args) {
        // 创建一个 Enhancer 对象
        Enhancer enhancer = new Enhancer();

        // 1. 设置父类(告诉 CGLIB 要继承谁)
        enhancer.setSuperclass(AliPay.class);

        // 2. 设置回调(告诉 CGLIB 拦截到的方法要去执行谁的 intercept 逻辑)
        enhancer.setCallback(new AliPayMethodInterceptor());

        // 3. 创建代理对象(实例)
        AliPay aliPayProxy = (AliPay) enhancer.create();

        // 通过代理对象调用方法
        aliPayProxy.pay("张三", 15.5);
    }
}
步骤四:运行并查看结果

运行 Clientmain 方法,你将看到如下输出:

====== [CGLIB 代理] 支付前,进行安全检查 ======
【核心业务】张三 使用支付宝消费了 15.5 元。
====== [CGLIB 代理] 支付后,记录支付日志 ======

我们成功地为一个没有实现任何接口的 AliPay 类,添加了前置和后置的增强逻辑!

Part 4: CGLIB 的“天花板”

CGLIB 虽然强大,但它也有自己的局限性,这个是面试高频题:

  1. 无法代理 final:因为 CGLIB 的原理是继承,而 Java 语法规定 final 类是不能被继承的。所以如果 AliPay 是一个 final 类,CGLIB 就无能为力了。
  2. 无法代理 final 方法:同样,如果类中的某个方法被声明为 final,子类是无法重写(override)这个方法的。因此,CGLIB 的拦截逻辑对这个 final 方法也不生效。

5.总结:

1. 什么是动态代理?

  • 它是一种在运行时动态创建代理对象的技术,相较于静态代理,我们无需手动编写代理类。它是AOP(面向切面编程)思想的底层实现之一。

2. 为什么需要动态代理?

  • 为了解决静态代理的痛点:当需要代理的类或接口增多时,会导致代理类爆炸,难以维护。
  • 核心优势是 解耦:将通用的、非核心的业务逻辑(如日志、事务、监控)与核心业务代码分离,使代码结构更清晰,可维护性更高。

3.动态代理相比于静态代理的核心:(实现原理)

  • 他的增强逻辑在InvocationHandler接口的实现类重写的invoke方法中体现;
  • 而代理类则是通过Proxy类中的Proxy.newProxyInstance()方法利用 反射 机制,接收类加载器、接口数组和 InvocationHandler 作为参数,在运行时动态生成一个代理类(比如 $Proxy0
  • 当调用代理对象的任何方法时,它内部其实是调用了我们传入的 InvocationHandlerinvoke 方法。在 invoke 方法里,我们又通过反射调用了真实目标对象的方法,并在调用前后执行我们自定义的增强逻辑。

4. JDK 动态代理有什么限制?

  • 它必须基于接口。被代理的目标类一定要实现一个或多个接口。如果一个类没有实现任何接口,那 JDK 动态代理就无能为力了。
  • (进阶)如果面试官追问:“那如果我想代理一个没有实现接口的类怎么办?” 可以肯定地回答:“可以使用 CGLIB 动态代理。” CGLIB 是通过继承被代理类的方式来创建代理对象的,所以不要求被代理类实现接口,但要求该类不能是 final 的。这也是 Spring AOP 的选择策略:如果目标对象实现了接口,就用 JDK 动态代理;如果没有,就用 CGLIB。

在工作中,我们不会经常直接去写 Proxy.newProxyInstance,因为像 Spring 这样的框架已经把这些底层细节封装好了,我们通过 @Aspect, @Before, @After 这样的注解就能轻松实现 AOP。但是,理解它的底层原理,对排查问题、深入学习框架、以及在面试中展现技术深度,都有巨大的帮助。


网站公告

今日签到

点亮在社区的每一天
去签到