BaseMapper 接口介绍

基于 mybatis-mapper/provider 核心部分实现的基础的增删改查操作,提供了一个核心的 io.mybatis.mapper.BaseMapper 接口和一个 预定义 的 io.mybatis.mapper.Mapper 接口,BaseMapper 接口定义如下:


/**
 * 基础 Mapper 方法,可以在此基础上继承覆盖已有方法
 *
 * @param <T> 实体类类型
 * @param <I> 主键类型
 * @author liuzh
 */
public interface BaseMapper<T, I extends Serializable>
    extends EntityMapper<T, I>, ExampleMapper<T, Example<T>>, CursorMapper<T, Example<T>> {

  /**
   * Example 查询封装
   */
  default ExampleWrapper<T, I> wrapper() {
    return new ExampleWrapper<>(BaseMapper.this, example());
  }

  /**
   * 根据主键更新实体中不为空的字段,强制字段不区分是否 null,都更新
   * <p>
   * 当前方法来自 {@link io.mybatis.mapper.fn.FnMapper},该接口中的其他方法用 {@link ExampleMapper} 也能实现
   *
   * @param entity            实体类
   * @param forceUpdateFields 强制更新的字段,不区分字段是否为 null,通过 {@link Fn#of(Fn...)} 创建 {@link Fn.Fns}
   * @return 1成功,0失败
   */
  @Lang(Caching.class)
  @UpdateProvider(type = FnProvider.class, method = "updateByPrimaryKeySelectiveWithForceFields")
  int updateByPrimaryKeySelectiveWithForceFields(@Param("entity") T entity, @Param("fns") Fn.Fns<T> forceUpdateFields);

  /**
   * 根据指定字段集合查询:field in (fieldValueList)
   * <p>
   * 这个方法是个示例,你也可以使用 Java8 的默认方法实现一些通用方法
   *
   * @param field          字段
   * @param fieldValueList 字段值集合
   * @param <F>            字段类型
   * @return 实体列表
   */
  default <F> List<T> selectByFieldList(Fn<T, F> field, Collection<F> fieldValueList) {
    Example<T> example = new Example<>();
    example.createCriteria().andIn((Fn<T, Object>) field.in(entityClass()), fieldValueList);
    return selectByExample(example);
  }

  /**
   * 根据指定字段集合删除:field in (fieldValueList)
   * <p>
   * 这个方法是个示例,你也可以使用 Java8 的默认方法实现一些通用方法
   *
   * @param field          字段
   * @param fieldValueList 字段值集合
   * @param <F>            字段类型
   * @return 实体列表
   */
  default <F> int deleteByFieldList(Fn<T, F> field, Collection<F> fieldValueList) {
    Example<T> example = new Example<>();
    example.createCriteria().andIn((Fn<T, Object>) field.in(entityClass()), fieldValueList);
    return deleteByExample(example);
  }

}

这个接口展示了好几个通用方法的特点:

1.可以继承其他通用接口

2.可以直接复制其他接口中的通用方法定义

3.可以使用 Java8 默认方法灵活实现通用方法

在看 Mapper 接口:


/**
 * 自定义 Mapper 示例,这个 Mapper 基于主键自增重写了 insert 方法,主要用作示例
 * <p>
 * 当你使用 Oracle 或其他数据库时,insert 重写时也可以使用 @SelectKey 注解对主键进行定制
 *
 * @param <T> 实体类类型
 * @param <I> 主键类型
 * @author liuzh
 */
public interface Mapper<T, I extends Serializable> extends BaseMapper<T, I> {

  /**
   * 保存实体,默认主键自增,并且名称为 id
   * <p>
   * 这个方法是个示例,你可以在自己的接口中使用相同的方式覆盖父接口中的配置
   *
   * @param entity 实体类
   * @return 1成功,0失败
   */
  @Override
  @Lang(Caching.class)
  //@SelectKey(statement = "SELECT SEQ.NEXTVAL FROM DUAL", keyProperty = "id", before = true, resultType = long.class)
  @Options(useGeneratedKeys = true, keyProperty = "id")
  @InsertProvider(type = EntityProvider.class, method = "insert")
  int insert(T entity);

  /**
   * 保存实体中不为空的字段,默认主键自增,并且名称为 id
   * <p>
   * 这个方法是个示例,你可以在自己的接口中使用相同的方式覆盖父接口中的配置
   *
   * @param entity 实体类
   * @return 1成功,0失败
   */
  @Override
  @Lang(Caching.class)
  //@SelectKey(statement = "SELECT SEQ.NEXTVAL FROM DUAL", keyProperty = "id", before = true, resultType = long.class)
  @Options(useGeneratedKeys = true, keyProperty = "id")
  @InsertProvider(type = EntityProvider.class, method = "insertSelective")
  int insertSelective(T entity);

}

这个接口中通过重写继承接口对主键进行了设置,除非你系统正好使用自增的 id 字段作为主键,否则不应该继承 Mapper 接口使用,应该使用 BaseMapper 作为基础。这个接口主要体现了一个特点:

 4. 可以重写继承接口的定义

除了上面已经提到的4个特点外,在下面内容中,还能看到一个特点,5. 那就是一个 provider 实现,通过修改接口方法的返回值和入参,就能变身无数个通用方法,通用方法的实现极其容易。

下面开始详细介绍这些特性。

2.1.1 继承其他通用接口

上面接口定义中,继承了 EntityMapperExampleMapper 和 CursorMapper 接口。这些接口中定义了大量的通用方法, 通过继承使得 BaseMapper 接口获得了大量的通用方法,通过继承可以组合不同类别的方法。 你可以以 BaseMapper 为基础创建自己的基类接口,也可以完全自己创建集成 EntityMapper 等接口来选择需要的通用方法。

提供的最基础的接口可以通过 2.2~2.7 来了解其中具体的方法。

2.1.2 复制其他接口中的通用方法定义

这是最灵活的一点,在 BaseMapper 中直接复制了 FnMapper 的一个方法:


/**
 * 根据主键更新实体中不为空的字段,强制字段不区分是否 null,都更新
 * <p>
 * 当前方法来自 {@link io.mybatis.mapper.fn.FnMapper},该接口中的其他方法用 {@link ExampleMapper} 也能实现
 *
 * @param entity            实体类
 * @param forceUpdateFields 强制更新的字段,不区分字段是否为 null,通过 {@link Fn#of(Fn...)} 创建 {@link Fn.Fns}
 * @return 1成功,0失败
 */
@Lang(Caching.class)
@UpdateProvider(type = FnProvider.class, method = "updateByPrimaryKeySelectiveWithForceFields")
int updateByPrimaryKeySelectiveWithForceFields(@Param("entity") T entity, @Param("fns") Fn.Fns<T> forceUpdateFields);

 这就是完全的复制粘贴,利用这一点,你可以不用 BaseMapper 接口作为自己的基类接口,你可以定义一个自己的接口,复制粘贴自己的需要的通用方法作为基础接口, 例如一个 GuozilanMapper 示例如下:

public interface GuozilanMapper<T> {

  /**
   * 保存实体
   *
   * @param entity 实体类
   * @return 1成功,0失败
   */
  @Lang(Caching.class)
  @InsertProvider(type = EntityProvider.class, method = "insert")
  int insert(T entity);

  /**
   * 根据主键查询实体
   *
   * @param id 主键
   * @return 实体
   */
  @Lang(Caching.class)
  @SelectProvider(type = EntityProvider.class, method = "selectByPrimaryKey")
  Optional<T> selectByPrimaryKey(Long id);
}

只要继承了上面的接口,你就直接拥有了这两个基础方法。

使用这种方式可以自定义一些自己项目需要用到的不同类别的通用接口,例如,如果你有大量实体都没有主键,默认的 BaseMapper<T, I> 就不太适合, 此时你可以自己创建一个 NoIdMapper<T>,把除了主键操作方法外的其他方法(有选择的)都拷过来,就形成了符合自己实际需要的通用 Mapper。

推而广之之后,还有更绝的用法,不继承接口,或者基础接口没有某个方法,直接复制注解过来,不需要自己写 XML:


public interface UserMapper {

   /**
    * 保存实体
    *
    * @param entity 实体类
    * @return 1成功,0失败
   */
  @Lang(Caching.class)
  @InsertProvider(type = EntityProvider.class, method = "insert")
  int insert(User entity);
}

你不需要任何具体的 SQL,上面的 insert 方法就可以直接使用了。

2.1.3 使用 Java8 默认方法灵活实现通用方法

在 BaseMapper 接口中,利用现有的 Example 方法,实现了两个非常常用的通用方法:


/**
 * 根据指定字段集合查询:field in (fieldValueList)
 * <p>
 * 这个方法是个示例,你也可以使用 Java8 的默认方法实现一些通用方法
 *
 * @param field          字段
 * @param fieldValueList 字段值集合
 * @param <F>            字段类型
 * @return 实体列表
 */
default <F> List<T> selectByFieldList(Fn<T, F> field, List<F> fieldValueList) {
  Example<T> example = new Example<>();
  example.createCriteria().andIn((Fn<T, Object>) field, fieldValueList);
  return selectByExample(example);
}

/**
 * 根据指定字段集合删除:field in (fieldValueList)
 * <p>
 * 这个方法是个示例,你也可以使用 Java8 的默认方法实现一些通用方法
 *
 * @param field          字段
 * @param fieldValueList 字段值集合
 * @param <F>            字段类型
 * @return 实体列表
 */
default <F> int deleteByFieldList(Fn<T, F> field, List<F> fieldValueList) {
  Example<T> example = new Example<>();
  example.createCriteria().andIn((Fn<T, Object>) field, fieldValueList);
  return deleteByExample(example);
}

这两个方法可以直接根据某个字段值的集合进行批量查询或者删除,用法示例如下:


List<User> users = mapper.selectByFieldList(User::getUserName, Arrays.asList("张无忌", "赵敏", "周芷若"));
mapper.deleteByFieldList(User::getUserName, Arrays.asList("张无忌", "赵敏", "周芷若"));

除了这个例子外,还有一段 EntityMapper 被注释的示例:


/**
 * 根据实体字段条件分页查询
 *
 * @param entity    实体类
 * @param rowBounds 分页信息
 * @return 实体列表
 */
List<T> selectList(T entity, RowBounds rowBounds);

/**
 * 根据查询条件获取第一个结果
 *
 * @param entity 实体类
 * @return 实体
 */
default Optional<T> selectFirst(T entity) {
  List<T> entities = selectList(entity, new RowBounds(0, 1));
  if (entities.size() == 1) {
    return Optional.of(entities.get(0));
  }
  return Optional.empty();
}

/**
 * 根据查询条件获取指定的前几个对象
 *
 * @param entity 实体类
 * @param n      指定的个数
 * @return 实体
 */
default List<T> selectTopN(T entity, int n) {
  return selectList(entity, new RowBounds(0, n));
}

合理的通过 Java8 的默认方法,能够实现海量的通用方法。至于那些是真正需要用到的通用方法,就需要根据自己的需要来选择,因此虽然上面的方法能通用, 但是在缺乏频繁使用场景的情况下,BaseMapper 接口并没有接纳这几个方法。

特别注意
上面示例中 List<T> selectList(T entity, RowBounds rowBounds); 没有添加 @SelectProvider 注解, 这是因为 MyBatis 中不允许出现相同名称的方法,同时对于 RowBounds 参数有特殊处理, 这个方法会直接复用List<T> selectList(T entity);方法,这个方法已经有了 @SelectProvider 注解配置。

2.1.4 重写继承接口的定义

在 EntityMapper 中有 insert 方法定义如下:


/**
 * 保存实体
 *
 * @param entity 实体类
 * @return 1成功,0失败
 */
@Lang(Caching.class)
@InsertProvider(type = EntityProvider.class, method = "insert")
int insert(T entity);

这个定义没有处理主键,需要自己设置好主键后调用该方法新增数据。

特别注意 在 2.x 版本之后支持在实体上配置主键策略,因此在实体配置主键策略的情况下,这个方法可以直接使用。 主键策略示例如下:


@Entity.Table("user")
public class User {
  @Entity.Column(value = "user_id", id = true, useGeneratedKeys = true)
  private Long   userId;

当调用 insert(user) 方法的时候会自动处理主键,而且也可以避免主键名称必须固定为统一名称的问题。

如果我使用的 MySql 自增怎么办?主键null也能直接保存,但是不回写。

如果使用 Oracle 序列怎么办?直接用这个方法是没有办法的。

因为可以 重写继承接口的定义,所以可以支持所有 MyBatis 本身能支持的所有主键方式。

在 Mapper 中,覆盖定义如下:


/**
 * 保存实体,默认主键自增,并且名称为 id
 * <p>
 * 这个方法是个示例,你可以在自己的接口中使用相同的方式覆盖父接口中的配置
 *
 * @param entity 实体类
 * @return 1成功,0失败
 */
@Override
@Lang(Caching.class)
@Options(useGeneratedKeys = true, keyProperty = "id")
@InsertProvider(type = EntityProvider.class, method = "insert")
int insert(T entity);

首先 @Override 是重写父接口定义,然后和原来相比增加了下面的注解:

@Options(useGeneratedKeys = true, keyProperty = "id")

这个注解对应 xml 中的配置如下:

<insert id="insert" useGeneratedKeys="true" keyProperty="id">

seGeneratedKeys 意思是要用JDBC接口方式取回主键,主键字段对应的属性名为 id,就是要回写到 id 字段。

上面的配置对 MySQL 这类自增数据库是可行的,如果你自己的主键不叫 id,甚至如果每个表的主键都不统一(如 {tableName}_id), 你需要在每个具体实现的接口中重写。例如:


public interface UserMapper extends Mapper<User, Long> {
  /**
   * 保存实体,默认主键自增,并且名称为 id
   * <p>
   * 这个方法是个示例,你可以在自己的接口中使用相同的方式覆盖父接口中的配置
   *
   * @param entity 实体类
   * @return 1成功,0失败
   */
  @Override
  @Lang(Caching.class)
  @Options(useGeneratedKeys = true, keyProperty = "userId")
  @InsertProvider(type = EntityProvider.class, method = "insert")
  int insert(User entity);

}

如果是Oracle序列或者需要执行SQL生成主键或者取回主键时,可以配置 @SelectKey 注解,示例如下:


@Override
@Lang(Caching.class)
@SelectKey(statement = "CALL IDENTITY()", keyProperty = "id", resultType = Long.class, before = false)
@InsertProvider(type = EntityProvider.class, method = "insert")
int insert(User entity);

上面还只是通过增加注解重新定义了接口方法。实际上你还可以更换 @InsertProvider(type = EntityProvider.class, method = "insert"), 将其中的实现换成其他的也可以,如果对默认的方法和逻辑不满意,就可以改成别的。

通过 重写继承接口的定义,应该能感觉出有多强大,多么灵活。

特别注意 在 2.x 版本之后支持在实体上配置主键策略,这种方式更方便,详情看 3. 实体类注解

2.1.5 通过修改接口方法的返回值和入参,就能变身无数个通用方法

以 EntityProvider 中的 select 方法为例,方法的具体实现如下:


/**
 * 根据实体字段条件查询唯一的实体,根据实体字段条件批量查询,查询结果的数量由方法定义
 *
 * @param providerContext 上下文
 * @return cacheKey
 */
public static String select(ProviderContext providerContext) {
  return SqlScript.caching(providerContext, new SqlScript() {
    @Override
    public String getSql(EntityTable entity) {
      return "SELECT " + entity.baseColumnAsPropertyList()
          + " FROM " + entity.table()
          + ifParameterNotNull(() ->
          where(() ->
              entity.whereColumns().stream().map(column ->
                  ifTest(column.notNullTest(), () -> "AND " + column.columnEqualsProperty())
              ).collect(Collectors.joining(LF)))
      )
          + entity.groupByColumn().orElse("")
          + entity.havingColumn().orElse("")
          + entity.orderByColumn().orElse("");
    }
  });
}

最终会生成一个 SELECT .. FROM .. WHERE ... 的 SQL,在 MyBatis 中,SQL 只定义了如何在数据库执行, 执行后的结果和取值方式是通过接口方法定义决定的,因此就这样一个 SELECT 查询,能够实现很多个方法,举例如下:


@Lang(Caching.class)
@SelectProvider(type = EntityProvider.class, method = "select")
Optional<T> selectOne(T entity);

@Lang(Caching.class)
@SelectProvider(type = EntityProvider.class, method = "select")
List<T> selectList(T entity);

@Lang(Caching.class)
@SelectProvider(type = EntityProvider.class, method = "select")
List<T> selectAll();

@Lang(Caching.class)
@SelectProvider(type = EntityProvider.class, method = "select")
Cursor<T> selectCursor(T entity); 

利用这一特点,通过修改接口方法的返回值和入参,就能变身无数个通用方法。

如果在加个 RowBounds 分页参数,通用方法直接翻倍。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/747671.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

React useImperativeHandle Hook

useImperativeHandle Hook 是一个比较比较简单的 hook&#xff0c;为 ref 节点添加一些处理方法&#xff0c;下面是来自官网例子&#xff0c;为 ref 添加了两个方法。 import { forwardRef, useRef, useImperativeHandle } from react;const MyInput forwardRef(function MyI…

香港办公室顺利落地,量子之歌发布白皮书开启银发新篇章

6月25日&#xff0c;量子之歌香港办公室开业典礼暨《2023年中国中老年服务市场白皮书&#xff1a;银发经济&#xff0c;耀眼的黄金赛道》发布会于香港中环交易广场隆重开幕。 这一里程碑事件不仅彰显了量子之歌在银发经济领域的行业领军者风范&#xff0c;更凸显了其在专业服务…

一文了解自定义表单系统开源的多个优势

降本、提质、增效&#xff0c;是当前很多企业都想实现的目的。什么样的软件可以助力企业创造价值&#xff1f;低代码技术平台是近些年得到了很多客户喜爱的平台产品&#xff0c;因为它能帮助大家减少编程代码的撰写&#xff0c;能轻松助力各部门之间做好协调沟通工作&#xff0…

算法导论 总结索引 | 第四部分 第十六章:贪心算法

1、求解最优化问题的算法 通常需要经过一系列的步骤&#xff0c;在每个步骤都面临多种选择。对于许多最优化问题&#xff0c;使用动态规划算法求最优解有些杀鸡用牛刀了&#xff0c;可以使用更简单、更高效的算法 贪心算法&#xff08;greedy algorithm&#xff09;就是这样的算…

13.1 Go 反射(Reflection)

&#x1f49d;&#x1f49d;&#x1f49d;欢迎莅临我的博客&#xff0c;很高兴能够在这里和您见面&#xff01;希望您在这里可以感受到一份轻松愉快的氛围&#xff0c;不仅可以获得有趣的内容和知识&#xff0c;也可以畅所欲言、分享您的想法和见解。 推荐:「stormsha的主页」…

文本生成图像综述

本调查回顾了扩散模型在生成任务中广泛流行的背景下文本到图像的扩散模型。作为一份自成一体的作品&#xff0c;本调查首先简要介绍了基本扩散模型如何用于图像合成&#xff0c;然后介绍了条件或指导如何改善学习。基于这一点&#xff0c;我们介绍了文本到图像生成方面的最新方…

条码二维码读取设备在医疗设备自助服务的重要性

医疗数字信息化建设的深入推进&#xff0c;医疗设备自助服务系统已成为医疗服务领域的一大趋势&#xff0c;条码二维码读取设备作为自助设备的重要组成部分&#xff0c;通过快速、准确地读取条形码二维码信息&#xff0c;不公提升了医疗服务效率&#xff0c;还为患者提供了更加…

Flutter页面状态保留策略

目的: 防止每次点击底部按钮都进行一次页面渲染和网络请求 1. 使用IndexedStack 简单,只需要把被渲染的组件外部套一层IndexedStack即可 缺点: 在应用启动的时候,所有需要保存状态的页面都会直接被渲染,保存起来. 对性能有影响 2. 使用PageController 实现较为复杂,但是不用…

Biome-BGC生态系统模型与Python融合技术

Biome-BGC是利用站点描述数据、气象数据和植被生理生态参数&#xff0c;模拟日尺度碳、水和氮通量的有效模型&#xff0c;其研究的空间尺度可以从点尺度扩展到陆地生态系统。 在Biome-BGC模型中&#xff0c;对于碳的生物量积累&#xff0c;采用光合酶促反应机理模型计算出每天…

C++设计模式——Facade外观模式

一&#xff0c;外观模式简介 外观模式是一种结构型设计模式&#xff0c; 又称为门面模式&#xff0c;也是一种基于创建对象来实现的模式&#xff0c;为子系统中的各组接口的使用提供了统一的访问入口。 外观模式对外提供了一个对象&#xff0c;让外部客户端(Client)对子系统的…

dataguard 主备切换方式switchover 和 failover 操作步骤

作者介绍&#xff1a;老苏&#xff0c;10余年DBA工作运维经验&#xff0c;擅长Oracle、MySQL、PG数据库运维&#xff08;如安装迁移&#xff0c;性能优化、故障应急处理等&#xff09; 公众号&#xff1a;老苏畅谈运维 欢迎关注本人公众号&#xff0c;更多精彩与您分享。datagu…

【ATU Book - i.MX8系列 - OS】NXP i.MX Linux Desktop (Ubuntu) BSP 开发环境架设

一、概述 谈论嵌入式系统的开发环境&#xff0c;不得不提起近年来相当实用的 Yocto 建构工具。此工具拥有极为灵活的平台扩展性&#xff0c;广泛的软体套件与社群支持、多平台支援整合性&#xff0c;能够满足开发者特定需求和多种热门的嵌入式系统架设&#xff0c;已成为当今顶…

【深海王国】小学生都能玩的单片机?零基础入门单片机Arduino带你打开嵌入式的大门!(10)

Hi٩(๑o๑)۶, 各位深海王国的同志们&#xff0c;早上下午晚上凌晨好呀~辛勤工作的你今天也辛苦啦 (o゜▽゜)o☆ 今天大都督继续为大家带来系列——小学生都能玩的单片机&#xff01;带你一周内快速走进嵌入式的大门&#xff0c;let’s go&#xff01; &#xff08;10&#…

Java学习笔记(多线程):CompetableFuture

本文是自己的学习笔记&#xff0c;主要参考资料如下 https://www.cnblogs.com/dolphin0520/p/3920407.html JavaSE文档 https://blog.csdn.net/ThinkWon/article/details/102508721 1、Overview2、重要参数3、主要方法3.1、创建实例&#xff0c;获取返回值3.2、线程执行顺序相关…

三十九篇:UML与SysML:掌握现代软件和系统架构的关键

UML与SysML&#xff1a;掌握现代软件和系统架构的关键 1. 引言 1.1 为什么系统设计如此关键 在当今快速发展的技术环境中&#xff0c;系统设计的重要性不言而喻。无论是软件开发还是复杂的系统工程&#xff0c;良好的设计是确保项目成功的基石。系统设计不仅关系到功能的实现…

day38动态规划part01| 理论基础 509. 斐波那契数 70. 爬楼梯 746. 使用最小花费爬楼梯

**理论基础 ** 无论大家之前对动态规划学到什么程度&#xff0c;一定要先看 我讲的 动态规划理论基础。 如果没做过动态规划的题目&#xff0c;看我讲的理论基础&#xff0c;会有感觉 是不是简单题想复杂了&#xff1f; 其实并没有&#xff0c;我讲的理论基础内容&#xff0c;…

状态压缩动态规划(State Compression DP)算法详解

状态压缩动态规划&#xff08;State Compression DP&#xff09;是一种高效解决组合优化问题的技术&#xff0c;特别适用于那些状态空间较大且可以用二进制表示的情况。本文将详细讲解状态压缩DP的原理、常用的位运算技巧、以及具体的例题分析。 原理概述 状态压缩DP的核心思…

HTML5实现字母记忆配对游戏

HTML5实现字母记忆配对游戏 这个小游戏具有重新开始功能和难度设置功能。 “重新开始“按钮&#xff0c;点击它或完成一局游戏后&#xff0c;会自动开始新游戏。 下拉列表框&#xff0c;&#xff0c;难度设置&#xff0c;包含简单、中等和困难三个选项。 简单&#xff1a;8…

《梦醒蝶飞:释放Excel函数与公式的力量》5.4 Match函数

5.4 Match函数 5.4.1 match函数的概念 MATCH函数是Excel中的一个查找和引用函数&#xff0c;它用于在数据表或数组中搜索指定项&#xff0c;并返回该项在数组中的相对位置。以下是MATCH函数的几个关键概念&#xff1a; 1)查找值&#xff08;Lookup Value&#xff09; 这是…

Web 权限管理最佳实践:如何提升用户满意度与应用安全性?

引言 在当今数字化时代&#xff0c;Web应用的功能和复杂性不断增加&#xff0c;用户对在线服务的期望也在不断提升。为了提供丰富的用户体验&#xff0c;许多Web应用需要访问用户的个人信息或设备功能&#xff0c;如地理位置、摄像头和麦克风等。这些权限访问在提升应用功能的…