Commit 1ecbe5aa6132b5e7c6b2af31020e1c730b499e43
1 parent
545e9d2a
增加角色的本地缓存刷新
Showing
12 changed files
with
166 additions
and
20 deletions
src/main/java/cn/iocoder/dashboard/framework/mybatis/core/mapper/BaseMapperX.java
| ... | ... | @@ -4,10 +4,13 @@ import cn.iocoder.dashboard.common.pojo.PageParam; |
| 4 | 4 | import cn.iocoder.dashboard.common.pojo.PageResult; |
| 5 | 5 | import cn.iocoder.dashboard.framework.mybatis.core.util.MyBatisUtils; |
| 6 | 6 | import com.baomidou.mybatisplus.core.conditions.Wrapper; |
| 7 | +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; | |
| 7 | 8 | import com.baomidou.mybatisplus.core.mapper.BaseMapper; |
| 8 | 9 | import com.baomidou.mybatisplus.core.metadata.IPage; |
| 9 | 10 | import org.apache.ibatis.annotations.Param; |
| 10 | 11 | |
| 12 | +import java.util.List; | |
| 13 | + | |
| 11 | 14 | /** |
| 12 | 15 | * 在 MyBatis Plus 的 BaseMapper 的基础上拓展,提供更多的能力 |
| 13 | 16 | */ |
| ... | ... | @@ -21,4 +24,8 @@ public interface BaseMapperX<T> extends BaseMapper<T> { |
| 21 | 24 | return new PageResult<>(mpPage.getRecords(), mpPage.getTotal()); |
| 22 | 25 | } |
| 23 | 26 | |
| 27 | + default List<T> selectList() { | |
| 28 | + return selectList(new QueryWrapper<>()); | |
| 29 | + } | |
| 30 | + | |
| 24 | 31 | } | ... | ... |
src/main/java/cn/iocoder/dashboard/modules/system/dal/mysql/dao/permission/SysMenuMapper.java
| 1 | 1 | package cn.iocoder.dashboard.modules.system.dal.mysql.dao.permission; |
| 2 | 2 | |
| 3 | +import cn.iocoder.dashboard.framework.mybatis.core.mapper.BaseMapperX; | |
| 3 | 4 | import cn.iocoder.dashboard.framework.mybatis.core.query.QueryWrapperX; |
| 4 | 5 | import cn.iocoder.dashboard.modules.system.controller.permission.vo.menu.SysMenuListReqVO; |
| 5 | 6 | import cn.iocoder.dashboard.modules.system.dal.mysql.dataobject.permission.SysMenuDO; |
| 6 | 7 | import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; |
| 7 | -import com.baomidou.mybatisplus.core.mapper.BaseMapper; | |
| 8 | 8 | import org.apache.ibatis.annotations.Mapper; |
| 9 | 9 | |
| 10 | 10 | import java.util.Date; |
| 11 | 11 | import java.util.List; |
| 12 | 12 | |
| 13 | 13 | @Mapper |
| 14 | -public interface SysMenuMapper extends BaseMapper<SysMenuDO> { | |
| 14 | +public interface SysMenuMapper extends BaseMapperX<SysMenuDO> { | |
| 15 | 15 | |
| 16 | 16 | default SysMenuDO selectByParentIdAndName(Long parentId, String name) { |
| 17 | 17 | return selectOne(new QueryWrapper<SysMenuDO>().eq("parent_id", parentId) |
| ... | ... | @@ -27,10 +27,6 @@ public interface SysMenuMapper extends BaseMapper<SysMenuDO> { |
| 27 | 27 | .eqIfPresent("status", reqVO.getStatus())); |
| 28 | 28 | } |
| 29 | 29 | |
| 30 | - default List<SysMenuDO> selectList() { | |
| 31 | - return selectList(new QueryWrapper<>()); | |
| 32 | - } | |
| 33 | - | |
| 34 | 30 | default boolean selectExistsByUpdateTimeAfter(Date maxUpdateTime) { |
| 35 | 31 | return selectOne(new QueryWrapper<SysMenuDO>().select("id") |
| 36 | 32 | .gt("update_time", maxUpdateTime).last("LIMIT 1")) != null; | ... | ... |
src/main/java/cn/iocoder/dashboard/modules/system/dal/mysql/dao/permission/SysRoleMapper.java
| 1 | 1 | package cn.iocoder.dashboard.modules.system.dal.mysql.dao.permission; |
| 2 | 2 | |
| 3 | +import cn.iocoder.dashboard.framework.mybatis.core.mapper.BaseMapperX; | |
| 3 | 4 | import cn.iocoder.dashboard.framework.mybatis.core.query.QueryWrapperX; |
| 4 | 5 | import cn.iocoder.dashboard.framework.mybatis.core.util.MyBatisUtils; |
| 5 | 6 | import cn.iocoder.dashboard.modules.system.controller.permission.vo.role.SysRoleExportReqVO; |
| 6 | 7 | import cn.iocoder.dashboard.modules.system.controller.permission.vo.role.SysRolePageReqVO; |
| 7 | 8 | import cn.iocoder.dashboard.modules.system.dal.mysql.dataobject.permission.SysRoleDO; |
| 8 | -import com.baomidou.mybatisplus.core.mapper.BaseMapper; | |
| 9 | +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; | |
| 9 | 10 | import com.baomidou.mybatisplus.core.metadata.IPage; |
| 10 | 11 | import org.apache.ibatis.annotations.Mapper; |
| 11 | 12 | import org.springframework.lang.Nullable; |
| 12 | 13 | |
| 13 | 14 | import java.util.Collection; |
| 15 | +import java.util.Date; | |
| 14 | 16 | import java.util.List; |
| 15 | 17 | |
| 16 | 18 | @Mapper |
| 17 | -public interface SysRoleMapper extends BaseMapper<SysRoleDO> { | |
| 19 | +public interface SysRoleMapper extends BaseMapperX<SysRoleDO> { | |
| 18 | 20 | |
| 19 | 21 | default IPage<SysRoleDO> selectPage(SysRolePageReqVO reqVO) { |
| 20 | 22 | return selectPage(MyBatisUtils.buildPage(reqVO), |
| ... | ... | @@ -43,4 +45,9 @@ public interface SysRoleMapper extends BaseMapper<SysRoleDO> { |
| 43 | 45 | return selectList(new QueryWrapperX<SysRoleDO>().in("status", statuses)); |
| 44 | 46 | } |
| 45 | 47 | |
| 48 | + default boolean selectExistsByUpdateTimeAfter(Date maxUpdateTime) { | |
| 49 | + return selectOne(new QueryWrapper<SysRoleDO>().select("id") | |
| 50 | + .gt("update_time", maxUpdateTime).last("LIMIT 1")) != null; | |
| 51 | + } | |
| 52 | + | |
| 46 | 53 | } | ... | ... |
src/main/java/cn/iocoder/dashboard/modules/system/mq/consumer/permission/SysRoleRefreshConsumer.java
0 → 100644
| 1 | +package cn.iocoder.dashboard.modules.system.mq.consumer.permission; | |
| 2 | + | |
| 3 | +import cn.iocoder.dashboard.framework.redis.core.pubsub.AbstractChannelMessageListener; | |
| 4 | +import cn.iocoder.dashboard.modules.system.mq.message.permission.SysRoleRefreshMessage; | |
| 5 | +import cn.iocoder.dashboard.modules.system.service.permission.SysRoleService; | |
| 6 | +import lombok.extern.slf4j.Slf4j; | |
| 7 | +import org.springframework.stereotype.Component; | |
| 8 | + | |
| 9 | +import javax.annotation.Resource; | |
| 10 | + | |
| 11 | +/** | |
| 12 | + * 针对 {@link SysRoleRefreshMessage} 的消费者 | |
| 13 | + * | |
| 14 | + * @author 芋道源码 | |
| 15 | + */ | |
| 16 | +@Component | |
| 17 | +@Slf4j | |
| 18 | +public class SysRoleRefreshConsumer extends AbstractChannelMessageListener<SysRoleRefreshMessage> { | |
| 19 | + | |
| 20 | + @Resource | |
| 21 | + private SysRoleService roleService; | |
| 22 | + | |
| 23 | + @Override | |
| 24 | + public void onMessage(SysRoleRefreshMessage message) { | |
| 25 | + log.info("[onMessage][收到 Role 刷新消息]"); | |
| 26 | + roleService.initLocalCache(); | |
| 27 | + } | |
| 28 | + | |
| 29 | +} | ... | ... |
src/main/java/cn/iocoder/dashboard/modules/system/mq/message/permission/SysRoleRefreshMessage.java
0 → 100644
| 1 | +package cn.iocoder.dashboard.modules.system.mq.message.permission; | |
| 2 | + | |
| 3 | +import cn.iocoder.dashboard.framework.redis.core.pubsub.ChannelMessage; | |
| 4 | +import lombok.Data; | |
| 5 | + | |
| 6 | +/** | |
| 7 | + * 角色数据刷新 Message | |
| 8 | + */ | |
| 9 | +@Data | |
| 10 | +public class SysRoleRefreshMessage implements ChannelMessage { | |
| 11 | + | |
| 12 | + @Override | |
| 13 | + public String getChannel() { | |
| 14 | + return "system.role.refresh"; | |
| 15 | + } | |
| 16 | + | |
| 17 | +} | ... | ... |
src/main/java/cn/iocoder/dashboard/modules/system/mq/producer/permission/SysRoleProducer.java
0 → 100644
| 1 | +package cn.iocoder.dashboard.modules.system.mq.producer.permission; | |
| 2 | + | |
| 3 | +import cn.iocoder.dashboard.framework.redis.core.util.RedisMessageUtils; | |
| 4 | +import cn.iocoder.dashboard.modules.system.mq.message.permission.SysRoleRefreshMessage; | |
| 5 | +import org.springframework.data.redis.core.StringRedisTemplate; | |
| 6 | +import org.springframework.stereotype.Component; | |
| 7 | + | |
| 8 | +import javax.annotation.Resource; | |
| 9 | + | |
| 10 | +/** | |
| 11 | + * Role 角色相关消息的 Producer | |
| 12 | + */ | |
| 13 | +@Component | |
| 14 | +public class SysRoleProducer { | |
| 15 | + | |
| 16 | + @Resource | |
| 17 | + private StringRedisTemplate stringRedisTemplate; | |
| 18 | + | |
| 19 | + /** | |
| 20 | + * 发送 {@link SysRoleRefreshMessage} 消息 | |
| 21 | + */ | |
| 22 | + public void sendRoleRefreshMessage() { | |
| 23 | + SysRoleRefreshMessage message = new SysRoleRefreshMessage(); | |
| 24 | + RedisMessageUtils.sendChannelMessage(stringRedisTemplate, message); | |
| 25 | + } | |
| 26 | + | |
| 27 | +} | ... | ... |
src/main/java/cn/iocoder/dashboard/modules/system/service/dept/impl/SysDeptServiceImpl.java
| ... | ... | @@ -88,7 +88,7 @@ public class SysDeptServiceImpl implements SysDeptService { |
| 88 | 88 | parentDeptCache = parentBuilder.build(); |
| 89 | 89 | assert deptList.size() > 0; // 断言,避免告警 |
| 90 | 90 | maxUpdateTime = deptList.stream().max(Comparator.comparing(BaseDO::getUpdateTime)).get().getUpdateTime(); |
| 91 | - log.info("[init][初始化 Dept 数量为 {}]", deptList.size()); | |
| 91 | + log.info("[initLocalCache][初始化 Dept 数量为 {}]", deptList.size()); | |
| 92 | 92 | } |
| 93 | 93 | |
| 94 | 94 | @Scheduled(fixedDelay = SCHEDULER_PERIOD, initialDelay = SCHEDULER_PERIOD) |
| ... | ... | @@ -201,7 +201,6 @@ public class SysDeptServiceImpl implements SysDeptService { |
| 201 | 201 | // 删除部门 |
| 202 | 202 | deptMapper.deleteById(id); |
| 203 | 203 | // TODO 需要处理下与角色的数据权限关联,等做数据权限一起处理下 |
| 204 | - | |
| 205 | 204 | // 发送刷新消息 |
| 206 | 205 | deptProducer.sendDeptRefreshMessage(); |
| 207 | 206 | } | ... | ... |
src/main/java/cn/iocoder/dashboard/modules/system/service/dict/impl/SysDictDataServiceImpl.java
| ... | ... | @@ -100,7 +100,7 @@ public class SysDictDataServiceImpl implements SysDictDataService { |
| 100 | 100 | valueDictDataCache = valueDictDataBuilder.build(); |
| 101 | 101 | assert dataList.size() > 0; // 断言,避免告警 |
| 102 | 102 | maxUpdateTime = dataList.stream().max(Comparator.comparing(BaseDO::getUpdateTime)).get().getUpdateTime(); |
| 103 | - log.info("[init][缓存字典数据,数量为:{}]", dataList.size()); | |
| 103 | + log.info("[initLocalCache][缓存字典数据,数量为:{}]", dataList.size()); | |
| 104 | 104 | } |
| 105 | 105 | |
| 106 | 106 | @Scheduled(fixedDelay = SCHEDULER_PERIOD, initialDelay = SCHEDULER_PERIOD) | ... | ... |
src/main/java/cn/iocoder/dashboard/modules/system/service/permission/SysRoleService.java
src/main/java/cn/iocoder/dashboard/modules/system/service/permission/impl/SysMenuServiceImpl.java
| ... | ... | @@ -98,7 +98,7 @@ public class SysMenuServiceImpl implements SysMenuService { |
| 98 | 98 | permMenuCache = permMenuCacheBuilder.build(); |
| 99 | 99 | assert menuList.size() > 0; // 断言,避免告警 |
| 100 | 100 | maxUpdateTime = menuList.stream().max(Comparator.comparing(BaseDO::getUpdateTime)).get().getUpdateTime(); |
| 101 | - log.info("[init][缓存菜单,数量为:{}]", menuList.size()); | |
| 101 | + log.info("[initLocalCache][缓存菜单,数量为:{}]", menuList.size()); | |
| 102 | 102 | } |
| 103 | 103 | |
| 104 | 104 | @Scheduled(fixedDelay = SCHEDULER_PERIOD, initialDelay = SCHEDULER_PERIOD) | ... | ... |
src/main/java/cn/iocoder/dashboard/modules/system/service/permission/impl/SysPermissionServiceImpl.java
| ... | ... | @@ -78,7 +78,7 @@ public class SysPermissionServiceImpl implements SysPermissionService { |
| 78 | 78 | }); |
| 79 | 79 | roleMenuCache = roleMenuCacheBuilder.build(); |
| 80 | 80 | menuRoleCache = menuRoleCacheBuilder.build(); |
| 81 | - log.info("[init][初始化角色与菜单的关联数量为 {}]", roleMenuList.size()); | |
| 81 | + log.info("[initLocalCache][初始化角色与菜单的关联数量为 {}]", roleMenuList.size()); | |
| 82 | 82 | } |
| 83 | 83 | |
| 84 | 84 | @Override | ... | ... |
src/main/java/cn/iocoder/dashboard/modules/system/service/permission/impl/SysRoleServiceImpl.java
| 1 | 1 | package cn.iocoder.dashboard.modules.system.service.permission.impl; |
| 2 | 2 | |
| 3 | +import cn.hutool.core.collection.CollUtil; | |
| 3 | 4 | import cn.hutool.core.collection.CollectionUtil; |
| 4 | 5 | import cn.iocoder.dashboard.common.enums.CommonStatusEnum; |
| 5 | 6 | import cn.iocoder.dashboard.common.exception.util.ServiceExceptionUtil; |
| 6 | 7 | import cn.iocoder.dashboard.common.pojo.PageResult; |
| 8 | +import cn.iocoder.dashboard.framework.mybatis.core.dataobject.BaseDO; | |
| 7 | 9 | import cn.iocoder.dashboard.modules.system.controller.permission.vo.role.SysRoleCreateReqVO; |
| 8 | 10 | import cn.iocoder.dashboard.modules.system.controller.permission.vo.role.SysRoleExportReqVO; |
| 9 | 11 | import cn.iocoder.dashboard.modules.system.controller.permission.vo.role.SysRolePageReqVO; |
| ... | ... | @@ -13,14 +15,18 @@ import cn.iocoder.dashboard.modules.system.dal.mysql.dao.permission.SysRoleMappe |
| 13 | 15 | import cn.iocoder.dashboard.modules.system.dal.mysql.dataobject.permission.SysRoleDO; |
| 14 | 16 | import cn.iocoder.dashboard.modules.system.enums.permission.RoleCodeEnum; |
| 15 | 17 | import cn.iocoder.dashboard.modules.system.enums.permission.SysRoleTypeEnum; |
| 18 | +import cn.iocoder.dashboard.modules.system.mq.producer.permission.SysRoleProducer; | |
| 16 | 19 | import cn.iocoder.dashboard.modules.system.service.permission.SysPermissionService; |
| 17 | 20 | import cn.iocoder.dashboard.modules.system.service.permission.SysRoleService; |
| 18 | 21 | import com.baomidou.mybatisplus.core.metadata.IPage; |
| 19 | 22 | import com.google.common.collect.ImmutableMap; |
| 20 | 23 | import lombok.extern.slf4j.Slf4j; |
| 21 | 24 | import org.springframework.lang.Nullable; |
| 25 | +import org.springframework.scheduling.annotation.Scheduled; | |
| 22 | 26 | import org.springframework.stereotype.Service; |
| 23 | 27 | import org.springframework.transaction.annotation.Transactional; |
| 28 | +import org.springframework.transaction.support.TransactionSynchronization; | |
| 29 | +import org.springframework.transaction.support.TransactionSynchronizationManager; | |
| 24 | 30 | import org.springframework.util.StringUtils; |
| 25 | 31 | |
| 26 | 32 | import javax.annotation.PostConstruct; |
| ... | ... | @@ -40,12 +46,22 @@ import static cn.iocoder.dashboard.modules.system.enums.SysErrorCodeConstants.*; |
| 40 | 46 | public class SysRoleServiceImpl implements SysRoleService { |
| 41 | 47 | |
| 42 | 48 | /** |
| 49 | + * 定时执行 {@link #schedulePeriodicRefresh()} 的周期 | |
| 50 | + * 因为已经通过 Redis Pub/Sub 机制,所以频率不需要高 | |
| 51 | + */ | |
| 52 | + private static final long SCHEDULER_PERIOD = 5 * 60 * 1000L; | |
| 53 | + | |
| 54 | + /** | |
| 43 | 55 | * 角色缓存 |
| 44 | 56 | * key:角色编号 {@link SysRoleDO#getId()} |
| 45 | 57 | * |
| 46 | 58 | * 这里声明 volatile 修饰的原因是,每次刷新时,直接修改指向 |
| 47 | 59 | */ |
| 48 | 60 | private volatile Map<Long, SysRoleDO> roleCache; |
| 61 | + /** | |
| 62 | + * 缓存菜单的最大更新时间,用于后续的增量轮询,判断是否有更新 | |
| 63 | + */ | |
| 64 | + private volatile Date maxUpdateTime; | |
| 49 | 65 | |
| 50 | 66 | @Resource |
| 51 | 67 | private SysPermissionService permissionService; |
| ... | ... | @@ -53,19 +69,54 @@ public class SysRoleServiceImpl implements SysRoleService { |
| 53 | 69 | @Resource |
| 54 | 70 | private SysRoleMapper roleMapper; |
| 55 | 71 | |
| 72 | + @Resource | |
| 73 | + private SysRoleProducer roleProducer; | |
| 74 | + | |
| 56 | 75 | /** |
| 57 | 76 | * 初始化 {@link #roleCache} 缓存 |
| 58 | 77 | */ |
| 59 | 78 | @Override |
| 60 | 79 | @PostConstruct |
| 61 | - public void init() { | |
| 62 | - // 从数据库中读取 | |
| 63 | - List<SysRoleDO> roleDOList = roleMapper.selectList(null); | |
| 80 | + public void initLocalCache() { | |
| 81 | + // 获取菜单列表,如果有更新 | |
| 82 | + List<SysRoleDO> roleList = this.loadRoleIfUpdate(maxUpdateTime); | |
| 83 | + if (CollUtil.isEmpty(roleList)) { | |
| 84 | + return; | |
| 85 | + } | |
| 86 | + | |
| 64 | 87 | // 写入缓存 |
| 65 | 88 | ImmutableMap.Builder<Long, SysRoleDO> builder = ImmutableMap.builder(); |
| 66 | - roleDOList.forEach(sysRoleDO -> builder.put(sysRoleDO.getId(), sysRoleDO)); | |
| 89 | + roleList.forEach(sysRoleDO -> builder.put(sysRoleDO.getId(), sysRoleDO)); | |
| 67 | 90 | roleCache = builder.build(); |
| 68 | - log.info("[init][初始化 Role 数量为 {}]", roleDOList.size()); | |
| 91 | + assert roleList.size() > 0; // 断言,避免告警 | |
| 92 | + maxUpdateTime = roleList.stream().max(Comparator.comparing(BaseDO::getUpdateTime)).get().getUpdateTime(); | |
| 93 | + log.info("[initLocalCache][初始化 Role 数量为 {}]", roleList.size()); | |
| 94 | + } | |
| 95 | + | |
| 96 | + @Scheduled(fixedDelay = SCHEDULER_PERIOD, initialDelay = SCHEDULER_PERIOD) | |
| 97 | + public void schedulePeriodicRefresh() { | |
| 98 | + initLocalCache(); | |
| 99 | + } | |
| 100 | + | |
| 101 | + /** | |
| 102 | + * 如果菜单发生变化,从数据库中获取最新的全量菜单。 | |
| 103 | + * 如果未发生变化,则返回空 | |
| 104 | + * | |
| 105 | + * @param maxUpdateTime 当前菜单的最大更新时间 | |
| 106 | + * @return 菜单列表 | |
| 107 | + */ | |
| 108 | + private List<SysRoleDO> loadRoleIfUpdate(Date maxUpdateTime) { | |
| 109 | + // 第一步,判断是否要更新。 | |
| 110 | + if (maxUpdateTime == null) { // 如果更新时间为空,说明 DB 一定有新数据 | |
| 111 | + log.info("[loadRoleIfUpdate][首次加载全量菜单]"); | |
| 112 | + } else { // 判断数据库中是否有更新的菜单 | |
| 113 | + if (!roleMapper.selectExistsByUpdateTimeAfter(maxUpdateTime)) { | |
| 114 | + return null; | |
| 115 | + } | |
| 116 | + log.info("[loadRoleIfUpdate][增量加载全量菜单]"); | |
| 117 | + } | |
| 118 | + // 第二步,如果有更新,则从数据库加载所有菜单 | |
| 119 | + return roleMapper.selectList(); | |
| 69 | 120 | } |
| 70 | 121 | |
| 71 | 122 | @Override |
| ... | ... | @@ -104,6 +155,8 @@ public class SysRoleServiceImpl implements SysRoleService { |
| 104 | 155 | role.setType(SysRoleTypeEnum.CUSTOM.getType()); |
| 105 | 156 | role.setStatus(CommonStatusEnum.ENABLE.getStatus()); |
| 106 | 157 | roleMapper.insert(role); |
| 158 | + // 发送刷新消息 | |
| 159 | + roleProducer.sendRoleRefreshMessage(); | |
| 107 | 160 | // 返回 |
| 108 | 161 | return role.getId(); |
| 109 | 162 | } |
| ... | ... | @@ -117,6 +170,8 @@ public class SysRoleServiceImpl implements SysRoleService { |
| 117 | 170 | // 更新到数据库 |
| 118 | 171 | SysRoleDO updateObject = SysRoleConvert.INSTANCE.convert(reqVO); |
| 119 | 172 | roleMapper.updateById(updateObject); |
| 173 | + // 发送刷新消息 | |
| 174 | + roleProducer.sendRoleRefreshMessage(); | |
| 120 | 175 | } |
| 121 | 176 | |
| 122 | 177 | @Override |
| ... | ... | @@ -128,6 +183,15 @@ public class SysRoleServiceImpl implements SysRoleService { |
| 128 | 183 | roleMapper.deleteById(id); |
| 129 | 184 | // 删除相关数据 |
| 130 | 185 | permissionService.processRoleDeleted(id); |
| 186 | + // 发送刷新消息. 注意,需要事务提交后,在进行发送刷新消息。不然 db 还未提交,结果缓存先刷新了 | |
| 187 | + TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() { | |
| 188 | + | |
| 189 | + @Override | |
| 190 | + public void afterCommit() { | |
| 191 | + roleProducer.sendRoleRefreshMessage(); | |
| 192 | + } | |
| 193 | + | |
| 194 | + }); | |
| 131 | 195 | } |
| 132 | 196 | |
| 133 | 197 | @Override | ... | ... |