【Android】RecyclerView 刷新方式全解析:从 notifyDataSetChanged 到 DiffUtil

【Android】RecyclerView 刷新方式全解析:从 notifyDataSetChanged 到 DiffUtil

RecyclerView 是 Android 开发中最常用的 UI 组件之一,而"如何刷新列表"是每个开发者必然会遇到的问题。

刚开始学 Android 时,只会用:

java 复制代码
adapter.notifyDataSetChanged();

简单粗暴,能解决大部分需求,但是这种方式缺点很多,真正项目中我们更需要性能更好、动画更自然、刷新粒度更精准的刷新方式。

本文将从最基础的 notifyDataSetChanged() 开始,介绍 RecyclerView 所有刷新方式。

1. notifyDataSetChanged() ------ 全量刷新

这应该是每个新手最先掌握的一种刷新方式,使用方法很简单:

java 复制代码
adapter.notifyDataSetChanged();

调用后,通知 RecyclerView 整个数据集已变化,强制重新执行所有 item 的 onBindViewHolder,不计算差异、不做局部优化,会导致全量刷新。

优点:这种方式优点非常明显,简单易用,一行代码搞定,不需要复杂逻辑。

缺点:性能最差,所有 item 都会重新绑定、重绘;无动画效果;不能指定变化位置;可能导致界面闪烁。

适用场景:数据结构完全变化,旧数据与全新数据无任何关联;初始加载;调试、临时代码使用;数据量很小(<10)。

能不用就不用。它是最笨的方法,但也最不推荐。

2. notifyItemXXX 系列 ------ 局部刷新

RecyclerView 提供了很多局部刷新方法,只让"变化的部分"刷新。

2.1 notifyItemChanged(int position) ------ 刷新单个Item

只刷新指定 position 对应 item。

java 复制代码
adapter.notifyItemChanged(position);

优点:刷新单个 item,性能好一些。

缺点:默认仍会重绘整个 item,常出现轻微闪烁。

2.2 notifyItemChanged(int position, Object payload) ------ 局部刷新

只刷新 item 内部的某个控件,不会重绘整个 item。

需要重载带 payloadonBindViewHolder

java 复制代码
@Override
public void onBindViewHolder(@NonNull VH holder, int position, @NonNull List<Object> payloads) {
    if (!payloads.isEmpty()) {
        // 局部刷新:处理 payloads(可包含多个)
        for (Object payload : payloads) {
            if (payload instanceof String) {
                String key = (String) payload;
                if ("like".equals(key)) {
                    // 只更新 like 字段
                    User user = data.get(position);
                    holder.tvLike.setText(String.valueOf(user.likeCount));
                    return; // 局部刷新后直接返回,避免完整绑定
                }
            } else if (payload instanceof Bundle) {
                // 如果你用 Bundle 传多个字段变化,可以解析 Bundle
            }
            // 其它 payload 处理...
        }
    }

    // payloads 为空时回退到完整绑定(或直接调用 super)
    super.onBindViewHolder(holder, position, payloads);
}

使用也很简单:

java 复制代码
data.get(pos).likeCount += 1;
// 传递简单标识(也可以传 Bundle/自定义对象)
adapter.notifyItemChanged(pos, "like");

2.3 notifyItemInserted(int position) ------ 插入单个Item

java 复制代码
data.add(position, item);
adapter.notifyItemInserted(position);

2.4 notifyItemRemoved(int position) ------ 删除单个Item

java 复制代码
data.remove(position);
adapter.notifyItemRemoved(position);

2.5 notifyItemMoved(int fromPosition, int toPosition) ------ 移动Item位置(拖拽排序)

java 复制代码
Collections.swap(data, from, to);
adapter.notifyItemMoved(from, to);

2.6 notifyItemRangeChanged(int positionStart, int itemCount) ------ 批量更新Item

java 复制代码
for (int i = 0; i < newItems.size(); i++) {
    data.set(startPos + i, newItems.get(i));
}
adapter.notifyItemRangeChanged(startPos, newItems.size());

2.7 notifyItemRangeInserted(int positionStart, int itemCount) ------ 批量插入Item

java 复制代码
int startPos = data.size();
data.addAll(moreItems);
adapter.notifyItemRangeInserted(startPos, moreItems.size());

2.8 notifyItemRangeRemoved(int positionStart, int itemCount) ------ 批量删除Item

java 复制代码
data.subList(startPos, endPos).clear();
adapter.notifyItemRangeRemoved(startPos, endPos - startPos);

3. DiffUtil ------ 自动计算差异,刷新最小项

DiffUtil 是一个用于计算两个列表之间差异的实用工具类。它可以优化 RecyclerView 的刷新操作,仅刷新需要更新的部分,从而提高性能并减少不必要的操作。

3.1 基本使用方式

3.1.1 实现DiffUtil.Callback

首先需要创建一个类实现DiffUtil.Callback,用于告诉 DiffUtil 两个列表如何比较。其中有以下五个方法:

  1. int getOldListSize()

    作用:返回旧列表长度。

  2. int getNewListSize()

    作用:返回新列表长度。

  3. boolean areItemsTheSame(int oldItemPosition, int newItemPosition)

    作用 :判断旧列表中位置 oldItemPosition 的项和新列表中位置 newItemPosition 的项 是否代表同一个实体

    常用做法:比较唯一标识符,如ID。

    返回 :如果返回 true,说明是"同一个 item",接下来会调用 areContentsTheSame() 检查内容是否变化;如果返回 false,会被认为是不同的 item(insert/remove)。

  4. boolean areContentsTheSame(int oldItemPosition, int newItemPosition)

    作用 :在 areItemsTheSame() 返回 true 的前提下,判断内容是否相等(是否需要刷新 UI)。

    常用做法 :比较字段或调用 equals()(但要确保 equals 的语义是"内容相等")。

    返回true 表示内容相同(不需要刷新),false 表示内容不同(会触发 notifyItemChanged)。

  5. @Nullable Object getChangePayload(int oldItemPosition, int newItemPosition)(可选)

    作用 :当 areItemsTheSame() 返回 trueareContentsTheSame() 返回 false 时,返回"变化的差量信息 "。该对象会作为 payload 传入 Adapter 的 onBindViewHolder(holder, position, List<Object> payloads),以实现局部绑定(避免整条 item 重绑定)。

    返回 :返回null等同于"没有 payload",RecyclerView 会做完整绑定。

    注意 :如果要使用该方法,需要在 Adapter 重写onBindViewHolder(..., payloads)

代码示例

java 复制代码
public class UserDiffCallback extends DiffUtil.Callback {
    private final List<User> oldList;
    private final List<User> newList;
    
    public UserDiffCallback(List<User> oldList, List<User> newList) {
        this.oldList = oldList;
        this.newList = newList;
    }
    
    @Override
    public int getOldListSize() {
        return oldList.size();
    }
    
    @Override
    public int getNewListSize() {
        return newList.size();
    }
    
    // 判断两个Item是否代表同一个对象
    @Override
    public boolean areItemsTheSame(int oldItemPosition, int newItemPosition) {
        // 通常比较唯一标识符,如 ID
        return oldList.get(oldItemPosition).getId() == newList.get(newItemPosition).getId();
    }
    
    // 判断两个Item的内容是否相同
    @Override
    public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) {
        User oldUser = oldList.get(oldItemPosition);
        User newUser = newList.get(newItemPosition);
        
        return oldUser.equals(newItem);//需重写User的equals()和hashCode()方法
        
        // 也可以直接比较所有字段
        /* return oldUser.getName().equals(newUser.getName()) &&
               oldUser.getAge() == newUser.getAge() &&
               oldUser.getAvatarUrl().equals(newUser.getAvatarUrl());*/
    }
    
    // 可选:当 areItemsTheSame 返回 true 但 areContentsTheSame 返回 false 时调用
    // 用于局部更新,避免重新绑定整个 item
    @Nullable
    @Override
    public Object getChangePayload(int oldItemPosition, int newItemPosition) {
        User oldUser = oldList.get(oldItemPosition);
        User newUser = newList.get(newItemPosition);
        
        Bundle diffBundle = new Bundle();
        
        if (!oldUser.getName().equals(newUser.getName())) {
            diffBundle.putString("name", newUser.getName());
        }
        
        if (oldUser.getAge() != newUser.getAge()) {
            diffBundle.putInt("age", newUser.getAge());
        }
        
        if (!oldUser.getAvatarUrl().equals(newUser.getAvatarUrl())) {
            diffBundle.putString("avatar", newUser.getAvatarUrl());
        }
       
        return diffBundle.size() == 0 ? null :diffBundle;
    }
}

注意事项

  • 输入列表必须是快照 :在计算 diff 时不要修改 old/new 列表。最保险的做法是先 new ArrayList<>(old)new ArrayList<>(newData)

  • areItemsTheSame 一定要稳定且唯一:不稳定的 id 会导致错误的插入/删除/移动。

  • areContentsTheSame 要尽量高效:避免复杂计算,或提前计算 hash/code。(但要注意 equals 语义)

  • getChangePayload 返回轻量对象:不要返回大的集合或含 Context 的对象,推荐返回 Bundle 或简单 POJO。

3.1.2 调用 DiffUtil.calculateDiff 计算差异结果

使用你的 Callback 实例调用 DiffUtil.calculateDiff,它将计算旧列表和新列表之间的差异(通过你在 Callback 中的规则),返回一个 DiffResult(最小变更集)。

该方法有两个参数:

  • Callback

    基于你实现的 DiffUtil.CallbackareItemsTheSame / areContentsTheSame / 可选 getChangePayload)计算出把旧列表变成新列表所需的最小操作序列(增、删、改、移)。

  • detectMoves可选

    默认为true,会尝试检测**元素的移动(move)**并把它作为 move 操作;检测 move 会额外消耗时间。

    设置为false,则不检测 move,只用 insert/remove/changed,性能更好但没有移动动画。

    检测移动会消耗更多时间,如果你不关心 item 的移动动画,可设为 false 以提升性能。

常用签名

java 复制代码
public static DiffUtil.DiffResult calculateDiff(DiffUtil.Callback cb);
public static DiffUtil.DiffResult calculateDiff(DiffUtil.Callback cb, boolean detectMoves);

注意事项尽量避免在主线程大规模计算 。大数据量时把 calculateDiff 放到后台线程或使用 AsyncListDiffer / ListAdapter(它们内部异步计算)。

3.1.3 DiffResult.dispatchUpdatesTo 刷新

使用diffResult.dispatchUpdatesTo(target)把变更集"应用"到目标 (通常是 RecyclerView.AdapterListUpdateCallback),最终触发 notifyXXX 调用与 RecyclerView 的动画/刷新。

该方法的作用是把 DiffResult 中的变更以正确顺序 分发给目标 ListUpdateCallback(或直接传 RecyclerView.Adapter),目标收到回调后通常会调用 notifyItemInserted/Removed/Changed/Moved,从而驱动 RecyclerView 做动画和局部更新。

常用签名

java 复制代码
public void dispatchUpdatesTo(RecyclerView.Adapter adapter);
public void dispatchUpdatesTo(ListUpdateCallback updateCallback);
  • 如果传 RecyclerView.Adapter,内部会封装成 AdapterListUpdateCallback(adapter)(它会把回调转换为 adapter.notifyItemInserted(...) 等)。
  • 也可以传自定义的 ListUpdateCallback,用于手工控制如何应用变更(例如先更新 UI 数据结构,再调用 notify)。

推荐做法

  1. 在后台或同步地调用 calculateDiff 得到 diffResult
  2. 主线程 (UI 线程)先替换 Adapter 的 backing dataadapter.data = newListadapter.setData(newList))。
  3. 立即调用 diffResult.dispatchUpdatesTo(adapter)

代码示例

java 复制代码
public void updateList(List<User> newList) {
    // 计算差异
    DiffUtil.DiffResult diffResult = DiffUtil.calculateDiff(
        new UserDiffCallback(this.userList, newList)
    );

    // 更新数据源
    this.userList.clear();
    this.userList.addAll(newList);

    // 分发更新
    diffResult.dispatchUpdatesTo(this);
}

为什么先替换数据?

因为 dispatchUpdatesTo 在回调(例如 notifyItemChanged)时,RecyclerView 会调用 Adapter 的 getItemCount() / getItem(position) 等方法;若数据还没更新,会导致 IndexOutOfBoundsException 或显示旧数据。 如果你不想先替换数据,可以 dispatch 到自定义 ListUpdateCallback,在回调里按你需要的顺序先更新数据结构再调用 adapter.notifyXXX(但这样更易出错,普通场景不建议)。

注意dispatchUpdatesTo(...) 在调用线程上同步执行 (会立即触发对应的 notify... 调用)。因此,必须在主线程对 RecyclerView/Adapter 进行 dispatch(否则会抛异常或 UI 不安全)。

如果你想在每个回调前后做额外工作(例如更新其他数据结构、统计日志、或在 dispatch 前后禁用动画),可以自定义 ListUpdateCallback

java 复制代码
DiffUtil.DiffResult result = DiffUtil.calculateDiff(cb, true);
result.dispatchUpdatesTo(new ListUpdateCallback() {
    @Override
    public void onInserted(int position, int count) {
        // 先更新 backing list 或做统计
        adapter.notifyItemRangeInserted(position, count);
    }
    @Override 
    public void onRemoved(int position, int count) {
        adapter.notifyItemRangeRemoved(position, count);
    }
    @Override 
    public void onMoved(int fromPosition, int toPosition) {
        adapter.notifyItemMoved(fromPosition, toPosition);
    }
    @Override 
    public void onChanged(int position, int count, Object payload) {
        adapter.notifyItemRangeChanged(position, count, payload);
    }
});

但负责手动同步数据和 dispatch 的复杂性较高,不建议滥用。

3.2 在后台线程执行计算

当数据量不大时,我们可以在UI线程中直接更新数据,数据量大时建议在后台线程执行计算,避免阻塞主线程。

代码示例:

java 复制代码
Executors.newSingleThreadExecutor().execute(new Runnable() {
    @Override
    public void run() {
        DiffUtil.DiffResult diffResult =
            DiffUtil.calculateDiff(new ItemDiffCallback(oldList, newList));

        // 切回主线程
        new Handler(Looper.getMainLooper()).post(new Runnable() {
            @Override
            public void run() {
                mData.clear();
                mData.addAll(newList);

                // 更新 RecyclerView
                diffResult.dispatchUpdatesTo(adapter);
            }
        });
    }
});

3.3 AsyncListDiffer 和 ListAdapter

ListAdapter 和 AsyncListDiffer 都是 Android 官方架构组件中为了简化 RecyclerView.Adapter 的数据更新和 DiffUtil 的使用而提供的工具。 它们都是基于 DiffUtil 的,但封装了后台计算差异和主线程更新的细节,让我们更容易使用。

3.3.1 AsyncListDiffer

AsyncListDiffer 是一个可以在 RecyclerView 适配器中使用的工具类,它负责持有当前列表的数据,并且当提交新列表时,会自动计算新旧列表的差异,然后通过 RecyclerView.Adapter 的更新方法(如 notifyItemInserted 等)来更新列表。

使用步骤

  1. 创建一个 DiffUtil.ItemCallback 的实现,用于定义如何比较列表中的项目。
  2. 在适配器中创建一个 AsyncListDiffer 实例。
  3. 使用 AsyncListDiffer 的 submitList 方法来更新数据。
  4. 在适配器的方法中,通过 AsyncListDiffer 获取数据。

代码示例

java 复制代码
// 1. 数据模型
public class User {
    private String id;
    private String name;
    private int age;
    
    // 构造函数、getter、setter...
    
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        User user = (User) o;
        return age == user.age &&
               Objects.equals(id, user.id) &&
               Objects.equals(name, user.name);
    }
    
    @Override
    public int hashCode() {
        return Objects.hash(id, name, age);
    }
}

// 2. 创建 DiffUtil.ItemCallback
public class UserDiffCallback extends DiffUtil.ItemCallback<User> {
    @Override
    public boolean areItemsTheSame(@NonNull User oldItem, @NonNull User newItem) {
        return oldItem.getId().equals(newItem.getId());
    }
    
    @Override
    public boolean areContentsTheSame(@NonNull User oldItem, @NonNull User newItem) {
        return oldItem.equals(newItem);
    }
    
    // 可选:获取变更内容(用于局部更新)
    // 需要在Adapter中处理payload
    @Nullable
    @Override
    public Object getChangePayload(@NonNull User oldItem, 
                                   @NonNull User newItem) {
        // ...
    }
}

// 3. 在 Adapter 中使用 AsyncListDiffer
public class UserAdapter extends RecyclerView.Adapter<UserViewHolder> {
    
    private final AsyncListDiffer<User> differ = 
        new AsyncListDiffer<>(this, new UserDiffCallback());
    
    public void submitList(List<User> users) {
        differ.submitList(users);
    }
    
    @Override
    public int getItemCount() {
        return differ.getCurrentList().size();
    }
    
    @NonNull
    @Override
    public UserViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
        View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_user, parent, false);
        return new UserViewHolder(view);
    }
    
    @Override
    public void onBindViewHolder(@NonNull UserViewHolder holder, int position) {
        User user = differ.getCurrentList().get(position);
        holder.bind(user);
    }
}

// 4. 使用示例
UserAdapter adapter = new UserAdapter();
recyclerView.setAdapter(adapter);

// 更新数据(自动计算差异)
List<User> newUsers = fetchUsersFromNetwork();
adapter.submitList(newUsers);

这里面有几个方法需要介绍一下:

  1. differ.getCurrentList()(返回 List)
    • 作用 :返回当前已"提交并应用"的不可变快照(List<T>)。
    • 特点 :安全在主线程读取,返回的是内部快照引用,不应修改;getCurrentList().size() 可用于 getItemCount()
  2. differ.getItem(int position)
    • 作用 :返回 getCurrentList().get(position) 的元素,便于在 onBindViewHolder 中取数据。
    • 注意getItem 是只读视图。
  3. differ.submitList(List<T> newList)
    • 作用 :提交新列表(可以为 null,表示清空列表)让 AsyncListDiffer 异步计算差异并把结果 dispatch 给 ListUpdateCallback
    • 注意 :每次都应该提交一个新的 List 实例(副本),不要传递同一个对象。submitList 是线程安全的,内部会安排后台计算,但最好在主线程调用以避免边界情况(尽管 AsyncListDiffer 允许从任意线程提交)。
    • 可选回调void submitList(List<T> list, @Nullable Runnable commitCallback)commitCallback会在差异计算完成并且更新已分发到 Adapter/RecyclerView 后被调用(在主线程)。

工作原理

AsyncListDiffer内部维护了一个当前列表(在后台线程中计算差异时,会拷贝当前列表作为旧列表)。当你调用 submitList 提交新列表时,它会启动一个后台任务(通过一个后台Executor)来执行DiffUtil.calculateDiff,然后根据计算结果,在主线程中通过RecyclerView.Adapter的更新方法来更新列表。

注意:AsyncListDiffer 要求 RecyclerView.Adapter 实现 ListUpdateCallback 接口(实际上 RecyclerView.Adapter 已经实现了这个接口),以便在计算得到差异后,调用相应的更新方法。

3.3.2 ListAdapter

ListAdapter 是 Google 官方提供的一个 RecyclerView.Adapter 的子类,它内部已经封装了 AsyncListDiffer,使得我们使用起来更加方便。我们只需要指定一个 DiffUtil.ItemCallback,然后通过 submitList 方法来更新数据即可。

使用步骤

  1. 创建一个 DiffUtil.ItemCallback 的实现(同上)。
  2. 继承 ListAdapter,并传入 ItemCallback。
  3. 实现 onCreateViewHolder 和 onBindViewHolder。
  4. 使用submitList更新数据。

代码示例

java 复制代码
// 1. 直接继承 ListAdapter(无需手动创建 AsyncListDiffer)
public class UserListAdapter extends ListAdapter<User, UserViewHolder> {
    
    // 2. 在构造函数中传入 DiffCallback
    public UserListAdapter() {
        super(new UserDiffCallback);
    }
    
    @NonNull
    @Override
    public UserViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
        View view = LayoutInflater.from(parent.getContext())
            .inflate(R.layout.item_user, parent, false);
        return new UserViewHolder(view);
    }
    
    @Override
    public void onBindViewHolder(@NonNull UserViewHolder holder, int position) {
        User user = getItem(position); // ListAdapter 提供,内部调用differ
        holder.bind(user);
    }
}

// 3. 使用示例
UserListAdapter adapter = new UserListAdapter();
recyclerView.setAdapter(adapter);

// 提交新数据(自动后台计算差异)
List<User> users = fetchUsersFromNetwork();
adapter.submitList(users);

需要注意的是,ListAdapter 已经覆写并实现了 getItemCount()(它返回 getCurrentList().size())。不用重写 getItemCount(),也不要去维护自己的 data list。

工作原理

ListAdapter 内部已经持有一个 AsyncListDiffer,并在构造时传入一个 DiffUtil.ItemCallback。ListAdapter 已经实现了 getItemCount() 和 getItem() 等方法,这些方法都是通过内部的 AsyncListDiffer 来获取数据。

当我们调用 submitList 方法时,实际上就是调用了内部 AsyncListDiffer 的 submitList 方法。

注意事项
  • 为了确保 DiffUtil 能够正确工作,数据模型类应该正确实现 equals 和 hashCode 方法,或者在 ItemCallback 中正确实现 areContentsTheSame。
  • AsyncListDiffer 和 ListAdapter 在比较列表时,会比较列表的引用。如果你提交了一个和当前列表相同(引用相同)的列表,那么不会触发计算。因此,每次更新数据时,应该创建一个新的列表对象,或者确保在修改列表后提交一个新列表。
  • AsyncListDiffer 和 ListAdapter的submitList 方法可以连续调用,但只有最后一次提交的列表会被计算和更新。也就是说,如果连续提交多次,可能会跳过中间状态。
  • 由于 DiffUtil 计算是在后台线程进行的,所以确保在计算期间,旧列表和新列表不会被修改。因此,在 ItemCallback 中,应该只读取列表中的数据,而不应该修改它们。

总结

刷新方式回顾

  • notifyDataSetChanged():最基础的刷新方式,但效率最低,会重新绑定所有可见项,没有动画效果。适用于数据完全改变或列表很小的情况。
  • 局部刷新方法(notifyItemXxx) :包括notifyItemChangednotifyItemInsertednotifyItemRemovednotifyItemMoved以及对应的范围方法。这些方法针对特定位置或范围进行更新,有动画效果,效率较高。适用于明确知道变化位置的情况。
  • DiffUtil:智能计算新旧列表差异,并自动调用合适的局部刷新方法。效率高,有动画效果,但需要实现 DiffUtil.Callback。适用于复杂的数据变化,特别是从后台获取新数据并更新列表时。
  • AsyncListDiffer:封装了DiffUtil和后台线程计算,简化了使用方式。需要在 Adapter 中创建 AsyncListDiffer 实例,并实现 ItemCallback。
  • ListAdapter:Google 官方推荐的 Adapter 基类,内部使用 AsyncListDiffer,让开发者更专注于 ViewHolder 和绑定逻辑。适用于大多数场景,特别是新项目。
  • Payload:配合使用,允许局部更新ItemView中的特定部分,避免重新绑定整个ItemView,进一步提升性能。

如何选择?

  • 对于简单的列表,数据量小,变化不频繁,可以使用notifyDataSetChanged()或局部刷新方法。
  • 对于数据源复杂,频繁更新,且追求高性能和动画效果,推荐使用DiffUtil或其封装类(AsyncListDifferListAdapter)。
  • 在新项目中,建议直接使用ListAdapter,它已经封装了最佳实践。
  • 当列表项视图复杂,且只有部分内容需要更新时,使用Payload进行局部更新。

最后,感谢阅读!如果你有任何问题或建议,欢迎在评论区留言讨论。

相关推荐
用户693717500138438 分钟前
23.Kotlin 继承:继承的细节:覆盖方法与属性
android·后端·kotlin
努力学算法的蒟蒻39 分钟前
day23(12.3)——leetcode面试经典150
java
Haha_bj42 分钟前
五、Kotlin——条件控制、循环控制
android·kotlin
luod43 分钟前
RabbitMQ简单生产者和消费者实现
java·rabbitmq
弥巷43 分钟前
【Android】深入理解Window和WindowManager
android·java
AllBlue1 小时前
安卓调用unity中的方法
android·unity·游戏引擎
okseekw1 小时前
Java抽象类详解:从“不能生孩子”的类到模板设计模式实战
java
古城小栈1 小时前
Spring中 @Transactional 和 @Async注解 容易不消停
java·spring
q_19132846951 小时前
基于Springboot+uniapp的智慧停车场收费小程序
java·vue.js·spring boot·小程序·uni-app·毕业设计·计算机毕业设计