2023年10月

杭州亚运会实现核心系统100%上云。

10月8日晚,杭州亚运会圆满闭幕。

作为史上首届“云上亚运”,杭州亚运会创造了历史。杭州亚运会实现了核心系统100%上云,并借助后台云算力、云存储等云技术保障,建设一系列各层级、各场馆数字指挥平台,实现全面感知、高效指挥。

同时,首次实现云上转播,据统计,杭州亚运会在云上传输了60路高清和超高清信号,总计超过7200小时时长。

核心系统100%上云,赛事成绩5秒发布

杭州亚运会是史上首届云上亚运,实现核心系统100%上云,并借助后台云算力、云存储等云技术保障,建设一系列各层级、各场馆数字指挥平台,实现全面感知、高效指挥。

基于云底座,杭州亚运会全面采用云原生技术,赛事核心系统群实现了系统通和数据通。这一创新的云技术方案避免了传统模式下各自“建烟囱”、数据难以互通的“老大难”问题,可以更好地支撑丰富的智能应用。

三大核心系统群将注册管理、竞赛报名、赛事成绩等赛事信息安全地汇聚在云计算底座上,并通过应用程序编程接口、文件传输等接口统一输出,实现各类数据准确、即时地一键输出。在裁判员确认成绩之后,5秒钟就可实现赛事信息发布,这是全球综合性运动会上成绩发布快捷、实用的新型技术。


赛场上的计分牌

云上转播7200+小时,多角度、更高清地收看比赛

杭州亚运会是首届采用云上转播的亚运会,在云上传输了60路高清和超高清信号,总计超7200小时时长。除了赛事直播,杭州亚运会还在云上提供了短视频、精彩集锦、赛事新闻等视频内容。

与传统卫星转播模式相比,云上转播超越其带宽和线下设备限制,提供了更丰富画面信号和剪辑方式。泰国体育局长资深顾问 Dr. Chuchat表示,通过阿里云的云转播服务,泰国当地媒体可以在任何时间,任何地点拉取信号流,让泰国观众实时收看到精彩的亚运比赛。

杭州亚运会通过云直播中心,凭借云网络的跨地域能力,保障亚运转播信号从场馆快速、稳定地传输至上海和北京的阿里云视频直播中心,再经由阿里云位于中国香港、新加坡、印度孟买等地的节点,向亚洲乃至全球观众实时转播。

打造首个大型体育赛事一体化智能办赛平台

智能化底座支撑了亚运会的多种智能化服务,其中,“亚运钉”是杭州亚组委和钉钉联合打造的全球首个大型体育赛事一体化智能办赛平台,为十万赛事工作人员提供服务。

亚运钉接入了行政审批、气象服务、会议服务、医疗服务等各业务领域的293个应用,并接入阿里云上的多种亚运核心系统应用,包括竞赛视频系统、IT事件跟踪与管理系统、志愿者管理系统等等。

亚运期间,近十万工作人员通过亚运钉实现了在线扁平化沟通协同。此外,亚运钉还能支持汉英日泰等13种语言的实时翻译,方便不同国家工作人员的相互交流。

作者:vivo 互联网服务器团队- Li Gang

本文介绍了一次排查Dubbo线程池耗尽问题的过程。通过查看Dubbo线程状态、分析Jedis连接池获取连接的源码、排查死锁条件等方面,最终确认是因为使用了cluster pipeline模式且没有设置超时时间导致死锁问题。

一、背景介绍

Redis Pipeline是一种高效的命令批量处理机制,可以在Redis中大幅度降低网络延迟,提高读写能力。Redis Cluster Pipeline是基于Redis Cluster的pipeline,通过将多个操作打包成一组操作,一次性发送到Redis Cluster中的多个节点,减少了通信延迟,提高了整个系统的读写吞吐量和性能,适用于需要高效处理Redis Cluster命令的场景。

本次使用到pipeline的场景是批量从Redis Cluster批量查询预约游戏信息,项目内使用的Redis Cluster Pipeline的流程如下,其中的JedisClusterPipeline是我们内部使用的工具类,提供Redis Cluster模式下的pipeline能力:

JedisClusterPipeline使用:

JedisClusterPipline jedisClusterPipline = redisService.clusterPipelined();
List<Object> response;
try {
    for (String key : keys) {
        jedisClusterPipline.hmget(key, VALUE1, VALUE2);
    }
    // 获取结果
    response = jedisClusterPipline.syncAndReturnAll();
} finally {
    jedisClusterPipline.close();
}

二、故障现场记录

某天,收到了Dubbo线程池耗尽的告警。查看日志发现只有一台机器有问题,并且一直没恢复,已完成任务数也一直没有增加。

图片

查看请求数监控,发现请求数归零,很明显机器已经挂了。

图片

使用arthas查看Dubbo线程,发现400个线程全部处于waiting状态。

图片

三、故障过程分析

Dubbo线程处于waiting状态这一点没有问题,Dubbo线程等待任务的时候也是waiting状态,但是查看完整调用栈发现有问题,下面两张图里的第一张是问题机器的栈,第二张是正常机器的栈,显然问题机器的这个线程在等待Redis连接池里有可用连接。

图片

图片

使用jstack导出线程快照后发现问题机器所有的Dubbo线程都在等待Redis连接池里有可用连接。

调查到这里,能发现两个问题。

  1. 线程一直等待连接而没有被中断。

  2. 线程获取不到连接。

3.1 线程一直等待连接而没有被中断原因分析

Jedis获取连接的逻辑在org.apache.commons.pool2.impl.GenericObjectPool#borrowObject(long)方法下。

public T borrowObject(long borrowMaxWaitMillis) throws Exception {
    ...
    PooledObject<T> p = null;
 
    // 获取blockWhenExhausted配置项,该配置默认值为true
    boolean blockWhenExhausted = getBlockWhenExhausted();
 
    boolean create;
    long waitTime = System.currentTimeMillis();
 
    while (p == null) {
        create = false;
        if (blockWhenExhausted) {
            // 从队列获取空闲的对象,该方法不会阻塞,没有空闲对象会返回null
            p = idleObjects.pollFirst();
            // 没有空闲对象则创建
            if (p == null) {
                p = create();
                if (p != null) {
                    create = true;
                }
            }
            if (p == null) {
                // borrowMaxWaitMillis默认值为-1
                if (borrowMaxWaitMillis < 0) {
                    // 线程栈快照里所有的dubbo线程都卡在这里,这是个阻塞方法,如果队列里没有新的连接会一直等待下去
                    p = idleObjects.takeFirst();    
                } else {
                    // 等待borrowMaxWaitMillis配置的时间还没有拿到连接的话就返回null
                    p = idleObjects.pollFirst(borrowMaxWaitMillis,
                            TimeUnit.MILLISECONDS);
                }
            }
            if (p == null) {
                throw new NoSuchElementException(
                        "Timeout waiting for idle object");
            }
            if (!p.allocate()) {
                p = null;
            }
        
        }
 
        ...
 
    }
 
    updateStatsBorrow(p, System.currentTimeMillis() - waitTime);
 
    return p.getObject();
}

由于业务代码没有设置borrowMaxWaitMillis,导致线程一直在等待可用连接 ,该值可以通过配置jedis pool的maxWaitMillis属性来设置。

到这里已经找到线程一直等待的原因,但线程获取不到连接的原因还需要继续分析。

3.2 线程获取不到连接原因分析

获取不到连接无非两种情况:

  1. 连不上Redis,无法创建连接

  2. 连接池里的所有连接都被占用了,无法获取到连接

猜想一:是不是连不上Redis?

询问运维得知发生问题的时间点确实有一波网络抖动,但是很快就恢复了,排查时问题机器是能正常连上Redis的。那有没有可能是创建Redis连接的流程写的有问题,无法从网络抖动中恢复导致线程卡死?这一点要从源码中寻找答案。

创建连接:

private PooledObject<T> create() throws Exception {
    int localMaxTotal = getMaxTotal();
    long newCreateCount = createCount.incrementAndGet();
    if (localMaxTotal > -1 && newCreateCount > localMaxTotal ||
            newCreateCount > Integer.MAX_VALUE) {
        createCount.decrementAndGet();
        return null;
    }
 
    final PooledObject<T> p;
    try {
        // 创建redis连接,如果发生超时会抛出异常
        // 默认的connectionTimeout和soTimeout都是2秒
        p = factory.makeObject();
    } catch (Exception e) {
        createCount.decrementAndGet();
        // 这里会把异常继续往上抛出
        throw e;
    }
 
    AbandonedConfig ac = this.abandonedConfig;
    if (ac != null && ac.getLogAbandoned()) {
        p.setLogAbandoned(true);
    }
 
    createdCount.incrementAndGet();
    allObjects.put(new IdentityWrapper<T>(p.getObject()), p);
    return p;
}

可以看到,连接Redis超时时会抛出异常,调用create()函数的borrowObject()也不会捕获这个异常,这个异常最终会在业务层被捕获,所以连不上Redis的话是不会一直等待下去的,网络恢复后再次调用create()方法就能重新创建连接。

综上所诉,第一种情况可以排除,继续分析情况2,连接被占用了没问题,但是一直不释放就有问题。

猜想二:是不是业务代码没有归还Redis连接?

连接没有释放,最先想到的是业务代码里可能有地方漏写了归还Redis连接的代码,pipeline模式下需要在finally块中手动调用JedisClusterPipeline#close()方法将连接归还给连接池,而普通模式下不需要手动释放(参考redis.clients.jedis.JedisClusterCommand#runWithRetries,每次执行完命令后都会自动释放),在业务代码里全局搜索所有使用到了cluster pipeline的代码,均手动调用了JedisClusterPipeline#close()方法,所以不是业务代码的问题。

猜想三:是不是Jedis存在连接泄露的问题?

既然业务代码没问题,那有没有可能是归还连接的代码有问题,存在连接泄露?2.10.0版本的Jedis确实可能发生连接泄露,具体可以看这个issue:
https://github.com/redis/jedis/issues/1920
,不过我们项目内使用的是2.9.0版本,所以排除连接泄露的情况。

猜想四:是不是发生了死锁?

排除以上可能性后,能想到原因的只剩死锁,思考后发现在没有设置超时时间的情况下,使用pipeline确实有概率发生死锁,这个死锁发生在从连接池(LinkedBlockingDeque)获取连接的时候。

先看下cluster pipeline模式下的Redis和普通的Redis有什么区别。Jedis为每个Redis实例都维护了一个连接池,cluster pipeline模式下,先使用查询用的key计算出其所在的Redis实例列表,再从这些实例对应的连接池里获取到连接,使用完后统一释放。而普通模式下一次只会获取一个连接池的连接,用完后立刻释放。这意味着cluster pipeline模式在获取连接时是符合死锁的“占有并等待”条件的,而普通模式不符合这个条件。

JedisClusterPipeline使用:

JedisClusterPipline jedisClusterPipline = redisService.clusterPipelined();
List<Object> response;
try {
    for (String key : keys) {
        // 申请连接,内部会先调用JedisClusterPipeline.getClient(String key)方法获取连接
        jedisClusterPipline.hmget(key, VALUE1, VALUE2);
        // 获取到了连接,缓存到poolToJedisMap
    }
    // 获取结果
    response = jedisClusterPipline.syncAndReturnAll();
} finally {
    // 归还所有连接
    jedisClusterPipline.close();
}

JedisClusterPipeline部分源码:

public class JedisClusterPipline extends PipelineBase implements Closeable {
 
    private static final Logger log = LoggerFactory.getLogger(JedisClusterPipline.class);
 
    // 用于记录redis命令的执行顺序
    private final Queue<Client> orderedClients = new LinkedList<>();
    // redis连接缓存
    private final Map<JedisPool, Jedis> poolToJedisMap = new HashMap<>();
 
    private final JedisSlotBasedConnectionHandler connectionHandler;
    private final JedisClusterInfoCache clusterInfoCache;
 
    public JedisClusterPipline(JedisSlotBasedConnectionHandler connectionHandler, JedisClusterInfoCache clusterInfoCache) {
        this.connectionHandler = connectionHandler;
        this.clusterInfoCache = clusterInfoCache;
    }
 
    @Override
    protected Client getClient(String key) {
 
        return getClient(SafeEncoder.encode(key));
    }
 
    @Override
    protected Client getClient(byte[] key) {
 
        Client client;
        // 计算key所在的slot
        int slot = JedisClusterCRC16.getSlot(key);
        // 获取solt对应的连接池
        JedisPool pool = clusterInfoCache.getSlotPool(slot);
        // 从缓存获取连接
        Jedis borrowedJedis = poolToJedisMap.get(pool);
        // 缓存中没有连接则从连接池获取并缓存
        if (null == borrowedJedis) {
            borrowedJedis = pool.getResource();
            poolToJedisMap.put(pool, borrowedJedis);
        }
         
        client = borrowedJedis.getClient();
     
        orderedClients.add(client);
 
        return client;
    }
 
    @Override
    public void close() {
        for (Jedis jedis : poolToJedisMap.values()) {
            // 清除连接内的残留数据,防止连接归还时有数据漏读的现象
            try {
                jedis.getClient().getAll();
            } catch (Throwable throwable) {
                log.warn("关闭jedis时遍历异常,遍历的目的是:清除连接内的残留数据,防止连接归还时有数据漏读的现象");
            }
            try {
                jedis.close();
            } catch (Throwable throwable) {
                log.warn("关闭jedis异常");
            }
        }
        // 归还连接
        clean();
        orderedClients.clear();
        poolToJedisMap.clear();
    }
 
    /**
     * go through all the responses and generate the right response type (warning :
     * usually it is a waste of time).
     *
     * @return A list of all the responses in the order
     */
    public List<Object> syncAndReturnAll() {
        List<Object> formatted = new ArrayList<>();
        List<Throwable> throwableList = new ArrayList<>();
        for (Client client : orderedClients) {
            try {
                Response response = generateResponse(client.getOne());
                if(response == null){
                    continue;
                }
                formatted.add(response.get());
            } catch (Throwable e) {
                throwableList.add(e);
            }
        }
        slotCacheRefreshed(throwableList);
        return formatted;
    }
}

图片

举个例子:

假设有一个集群有两台Redis主节点(集群模式下最小的主节点数量是3,这里只是为了举例),记为节点1/2,有个java程序有4个Dubbo线程,记为线程1/2/3/4,每个Redis实例都有一个大小为2的连接池。

线程1和线程2,先获取Redis1的连接再获取Redis2的连接。线程3和线程4,先获取Redis2的连接再获取Redis1的连接,假设这四个线程在获取到连接第一个连接后都等待了一会,在获取第二个连接的时候就会发生死锁(等待时间越长,触发的概率越大)。

图片

所以pipeline是可能导致死锁的,这个死锁的条件很容易破坏,等待连接的时候设置超时时间即可。还可以增大下连接池的大小,资源够的话也不会发生死锁。

四、死锁证明

以上只是猜想,为了证明确实发生了死锁,需要以下条件:

  1. 线程当前获取到了哪些连接池的连接

  2. 线程当前在等待哪些连接池的连接

  3. 每个连接池还剩多少连接

已知问题机器的Dubbo线程池大小为400,Redis集群主节点数量为12,Jedis配置的连接池大小为20。

4.1 步骤一:获取线程在等待哪个连接池有空闲连接

第一步
:先通过jstack和jmap分别导出栈和堆

第二步
:通过分析栈可以知道线程在等待的锁的地址,可以看到Dubbo线程383在等待0x6a3305858这个锁对象,这个锁属于某个连接池,需要找到具体是哪个连接池。

图片

第三步
:使用mat(Eclipse Memory Analyzer Tool)工具分析堆,通过锁的地址找到对应的连接池。

图片

使用mat的with incoming references功能顺着引用一层层的往上找。

图片

引用关系:ConditionObject->LinkedBlockingDeque

图片

引用关系:LinkedBlockingDeque->GenericObjectPool

图片

引用关系:GenericObjectPool->JedisPool。这里的ox6a578ddc8就是这个锁所属的连接池地址。

图片

这样我们就能知道Dubbo线程383当前在等待0x6a578ddc8这个连接池的连接。

通过这一套流程,我们可以知道每个Dubbo线程分别在等待哪些连接池有可用连接。

4.2 步骤二:获取线程当前持有了哪些连接池的连接

第一步
:使用mat在堆中查找所有JedisClusterPipeline类(正好400个,每个Dubbo线程都各有一个),然后查看里面的poolToJedisMap,其中保存了当前JedisClusterPipeline已经持有的连接和其所属的连接池。

下图中,我们可以看到JedisClusterPipeline(0x6ac40c088)对象当前的poolToJedisMap里有三个Node对象(0x6ac40dd40, 0x6ac40dd60, 0x6ac40dd80),代表其持有三个连接池的连接,可以从Node对象中找到JedisPool的地址。

图片

第二步
:第一步拿到JedisClusterPipeline持有哪个连接池的连接后,再查找持有此JedisClusterPipeline的Dubbo线程,这样就能得到Dubbo线程当前持有哪些连接池的连接。

图片

4.3 死锁分析

通过流程一可以发现虽然Redis主节点有12个,但是所有的Dubbo线程都只在等待以下5个节点对应的连接池之一:

  • 0x6a578e0c8

  • 0x6a578e048

  • 0x6a578ddc8

  • 0x6a578e538

  • 0x6a578e838

通过流程二我们可以得知这5个连接池的连接当前被哪些线程占用:

图片

已知每个连接池的大小都配置为了20,这5个连接池的所有连接已经被100个Dubbo线程占用完了,而所有的400个Dubbo线程又都在等待这5个连接池的连接,并且其等待的连接当前没被自己占用,通过这些条件,我们可以确定发生了死锁。

五、总结

这篇文章主要展现了一次系统故障的分析过程。在排查过程中,作者使用jmap和jstack保存故障现场,使用arthas分析故障现场,再通过阅读和分析源码,从多个可能的角度一步步的推演故障原因,推测是死锁引起的故障。在验证死锁问题时,作者使用mat按照一定的步骤来寻找线程在等待哪个连接池的连接和持有哪些连接池的连接,再结合死锁检测算法最终确认故障机器发生了死锁。

排查线上问题并非易事,不仅要对业务代码有足够的了解,还要对相关技术知识有系统性的了解,推测出可能导致问题的原因后,再熟练运用好排查工具,最终确认问题原因。

前面完成了基础管理的相关API,接下来就得做一个菜单管理了,用于对接管理后台前端界面。

设计菜单结构

菜单是一个多级结构,所以我们得设计一个树形的。包含自己上级和下级的属性。同时预留Permission用于做可选的权限限制。

namespace Wheel.Domain.Menus
{
    /// <summary>
    /// 菜单
    /// </summary>
    public class Menu : Entity<Guid>
    {
        /// <summary>
        /// 名称
        /// </summary>
        public string Name { get; set; }
        /// <summary>
        /// 显示名称
        /// </summary>
        public string DisplayName { get; set; }
        /// <summary>
        /// 菜单类型
        /// </summary>
        public MenuType MenuType { get; set; }
        /// <summary>
        /// 菜单路径
        /// </summary>
        public string? Path { get; set; }
        /// <summary>
        /// 权限名称
        /// </summary>
        public string? Permission { get; set; }
        /// <summary>
        /// 图标
        /// </summary>
        public string? Icon { get; set; }
        /// <summary>
        /// 排序
        /// </summary>
        public int Sort { get; set; }
        /// <summary>
        /// 上级菜单Id
        /// </summary>
        public virtual Guid? ParentId { get; set; }
        /// <summary>
        /// 上级菜单
        /// </summary>
        public virtual Menu? Parent { get; set; }
        /// <summary>
        /// 子菜单
        /// </summary>
        public virtual List<Menu> Children { get; set; }
    }
}

然后菜单和角色关联。创建RoleMenu表。

namespace Wheel.Domain.Menus
{
    public class RoleMenu
    {
        public virtual string RoleId { get; set; }
        public virtual Role Role { get; set; }
        public virtual Guid MenuId { get; set; }
        public virtual Menu Menu { get; set; }
    }
}

修改DbContext

接下来还是老套路,修改WheelDbContext
添加代码:

#region Menu
public DbSet<Menu> Menus { get; set; }
public DbSet<RoleMenu> RoleMenus { get; set; }
#endregion
protected override void OnModelCreating(ModelBuilder builder)
{
    base.OnModelCreating(builder);

    ConfigureIdentity(builder);
    ConfigureLocalization(builder);
    ConfigurePermissionGrants(builder);
    ConfigureMenus(builder);
}
void ConfigureMenus(ModelBuilder builder)
{
    builder.Entity<Menu>(b =>
    {
        b.HasKey(o => o.Id);
        b.Property(o => o.Permission).HasMaxLength(128);
        b.Property(o => o.Path).HasMaxLength(128);
        b.Property(o => o.Name).HasMaxLength(128);
        b.Property(o => o.Icon).HasMaxLength(128);
        b.Property(o => o.DisplayName).HasMaxLength(128);
        b.HasMany(o => o.Children).WithOne(o => o.Parent);
        b.HasIndex(o => o.ParentId);
    });
    builder.Entity<RoleMenu>(b =>
    {
        b.HasKey(o => new { o.MenuId, o.RoleId });
        b.Property(o => o.RoleId).HasMaxLength(36);
    });
}

然后执行数据库迁移命令即可完成表创建。

实现菜单管理

接下来就可以来实现我们的菜单管理相关功能了。

实现MenuAppService

IMenuAppService

namespace Wheel.Services.Menus
{
    public interface IMenuAppService : ITransientDependency
    {
        Task<R> Create(CreateOrUpdateMenuDto dto);
        Task<R> Update(Guid id, CreateOrUpdateMenuDto dto);
        Task<R> Delete(Guid id);
        Task<R<MenuDto>> GetById(Guid id);
        Task<R<List<MenuDto>>> GetList();
        Task<R<List<MenuDto>>> GetRoleMenuList(string roleId);
        Task<R<List<AntdMenuDto>>> GetCurrentMenu();
        Task<R> UpdateRoleMenu(string roleId, UpdateRoleMenuDto dto);
    }
}

MenuAppService

namespace Wheel.Services.Menus
{
    public class MenuAppService : WheelServiceBase, IMenuAppService
    {
        private readonly IBasicRepository<Menu, Guid> _menuRepository;
        private readonly IBasicRepository<Role, string> _roleRepository;
        private readonly IBasicRepository<RoleMenu> _roleMenuRepository;

        public MenuAppService(IBasicRepository<Menu, Guid> menuRepository)
        {
            _menuRepository = menuRepository;
        }

        public async Task<R> Create(CreateOrUpdateMenuDto dto)
        {
            var menu = Mapper.Map<Menu>(dto);
            menu.Id = GuidGenerator.Create();
            await _menuRepository.InsertAsync(menu, true);
            return new R();
        }

        public async Task<R> Update(Guid id,CreateOrUpdateMenuDto dto)
        {
            var menu = await _menuRepository.FindAsync(id);
            if(menu != null) 
            {
                Mapper.Map(dto, menu);
                await _menuRepository.UpdateAsync(menu, true);
            }
            return new R();
        }
        public async Task<R> Delete(Guid id)
        {
            await _menuRepository.DeleteAsync(id, true);
            return new R();
        }
        public async Task<R<MenuDto>> GetById(Guid id)
        {
            var menu = await _menuRepository.FindAsync(id);

            var dto = Mapper.Map<MenuDto>(menu);
            return new R<MenuDto>(dto);
        }
        public async Task<R<List<MenuDto>>> GetList()
        {
            var items = await _menuRepository.GetListAsync(
                a => a.ParentId == null,
                propertySelectors: a=>a.Children
                );
            items.ForEach(a => a.Children = a.Children.OrderBy(b => b.Sort).ToList());
            items = items.OrderBy(a => a.Sort).ToList();
            var resultItems = Mapper.Map<List<MenuDto>>(items);
            return new R<List<MenuDto>>(resultItems);
        }
        public async Task<R> UpdateRoleMenu(string roleId, UpdateRoleMenuDto dto)
        {
            using (var uow = await UnitOfWork.BeginTransactionAsync())
            {
                if (await _roleMenuRepository.AnyAsync(a => a.RoleId == roleId))
                {
                    await _roleMenuRepository.DeleteAsync(a => a.RoleId == roleId);
                }
                if(dto.MenuIds.Any())
                {
                    var roleMenus = dto.MenuIds.Select(a => new RoleMenu { RoleId = roleId, MenuId = a });
                    await _roleMenuRepository.InsertManyAsync(roleMenus.ToList());
                }
                await uow.CommitAsync();
            }
            return new R();
        }
        public async Task<R<List<MenuDto>>> GetRoleMenuList(string roleId)
        {
            var items = await _roleMenuRepository.SelectListAsync(a => a.RoleId == roleId && a.Menu.ParentId == null, a => a.Menu, propertySelectors: a => a.Menu.Children);
            items.ForEach(a => a.Children = a.Children.OrderBy(b => b.Sort).ToList());
            items = items.OrderBy(a => a.Sort).ToList();
            var resultItems = Mapper.Map<List<MenuDto>>(items);
            return new R<List<MenuDto>>(resultItems);
        }

        public async Task<R<List<AntdMenuDto>>> GetCurrentMenu()
        {
            if (CurrentUser.IsInRoles("admin"))
            {
                var menus = await _menuRepository.GetListAsync(a => a.ParentId == null);
                return new R<List<AntdMenuDto>>(MaptoAntdMenu(menus));
            }
            else
            {
                var roleIds = await _roleRepository.SelectListAsync(a => CurrentUser.Roles.Contains(a.Name), a => a.Id);
                var menus = await _roleMenuRepository.SelectListAsync(a => roleIds.Contains(a.RoleId) && a.Menu.ParentId == null, a => a.Menu, propertySelectors: a => a.Menu.Children);

                return new R<List<AntdMenuDto>>(MaptoAntdMenu(menus.DistinctBy(a=>a.Id).ToList()));
            }
        }

        private List<AntdMenuDto> MaptoAntdMenu(List<Menu> menus)
        {
            return menus.OrderBy(m => m.Sort).Select(m =>
            {
                var result = new AntdMenuDto
                {
                    Name = m.Name,
                    Icon = m.Icon,
                    Path = m.Path,
                    Access = m.Permission
                };
                if(m.Children != null && m.Children.Count > 0)
                {
                    result.Children = MaptoAntdMenu(m.Children);
                }
                return result;
            }).ToList();
        }
    }
}

实现MenuController

namespace Wheel.Controllers
{
    /// <summary>
    /// 菜单管理
    /// </summary>
    [Route("api/[controller]")]
    [ApiController]
    public class MenuController : WheelControllerBase
    {
        private readonly IMenuAppService _menuAppService;

        public MenuController(IMenuAppService menuAppService)
        {
            _menuAppService = menuAppService;
        }
        /// <summary>
        /// 新增菜单
        /// </summary>
        /// <param name="dto"></param>
        /// <returns></returns>
        [HttpPost()]
        public Task<R> Create(CreateOrUpdateMenuDto dto)
        {
            return _menuAppService.Create(dto);
        }
        /// <summary>
        /// 删除菜单
        /// </summary>
        /// <param name="id"></param>
        /// <returns></returns>
        [HttpDelete("{id}")]
        public Task<R> Delete(Guid id)
        {
            return _menuAppService.Delete(id);
        }
        /// <summary>
        /// 获取单个菜单详情
        /// </summary>
        /// <param name="id"></param>
        /// <returns></returns>
        [HttpGet("{id}")]
        public Task<R<MenuDto>> GetById(Guid id)
        {
            return _menuAppService.GetById(id);
        }
        /// <summary>
        /// 查询菜单列表
        /// </summary>
        /// <returns></returns>
        [HttpGet]
        public Task<R<List<MenuDto>>> GetList()
        {
            return _menuAppService.GetList();
        }
        /// <summary>
        /// 修改菜单
        /// </summary>
        /// <param name="id"></param>
        /// <param name="dto"></param>
        /// <returns></returns>
        [HttpPut("{id}")]
        public Task<R> Update(Guid id, CreateOrUpdateMenuDto dto)
        {
            return _menuAppService.Update(id, dto);
        }
        /// <summary>
        /// 修改角色菜单
        /// </summary>
        /// <param name="roleId"></param>
        /// <param name="dto"></param>
        /// <returns></returns>
        [HttpPut("role/{roleId}")]
        public Task<R> UpdateRoleMenu(string roleId, UpdateRoleMenuDto dto)
        {
            return _menuAppService.UpdateRoleMenu(roleId, dto);
        }
        /// <summary>
        /// 获取角色菜单列表
        /// </summary>
        /// <param name="roleId"></param>
        /// <returns></returns>
        [HttpGet("role/{roleId}")]
        public Task<R<List<MenuDto>>> GetRoleMenuList(string roleId)
        {
            return _menuAppService.GetRoleMenuList(roleId);
        }
    }
}

就这样我们就完成了菜单管理相关的API功能,包含菜单的增删查改和角色菜单绑定功能。
到这里我们最基础的后台管理功能API基本开发完成。

轮子仓库地址https://github.com/Wheel-Framework/Wheel
欢迎进群催更。

image.png

前言

近期碰到了分析app的需求,就学习了一下
frida
的动态插桩技术。
frida
是一款轻量级HOOK框架,可用于多平台上,例如android、windows、ios等。
frida
分为两部分,服务端运行在目标机上,通过注入进程的方式来实现劫持应用函数,另一部分运行在我们自己的控制机上。
frida
上层接口支持js、python、c等。

环境配置

安装frida模块

pip install frida

安装frida-tools模块

pip install frida-tools

通过如下命令查看模拟器架构。从
https://github.com/frida/frida/releases
可下载对应架构的
frida-server
,并上传到模拟器/手机中。

getprop ro.product.cpu.abi

frida hook java层 实操

这里拿的是一个别人写的 demo文件进行练手。实现的功能是,按了按钮就会返回一个随机数。

用jadx反编译apk可以很容易找到如下代码,我们知道如果我们可以控制
Jniint
的返回值,便可以控制打印出来的数据。

通过如下代码便可hook其返回值为 6666。

Java.perform(function () {
    // we create a javascript wrapper for MainActivity
    var Activity = Java.use('com.erev0s.jniapp.MainActivity');
    // replace the Jniint implementation
    Activity.Jniint.implementation = function () {
        // console.log is used to report information back to us
        console.log("[+] Hook Jniint success!");
        // return this number of our choice
        return 6666
    };
});

参考连接

https://xz.aliyun.com/t/8211

https://erev0s.com/blog/how-hook-android-native-methods-frida-noob-friendly/

大家好,我是刘牌,今天聊一下最近的一些经历和感悟还有回到三线城市的感悟,希望对大家有一定的帮助!

欢迎关注我的公众号【刘牌】,一起探讨人生,职业规划,发展副业,一起成长!

一、我不适合躺,也躺不了

我毕业之后就到了成都,去了一家做基础软件的上市公司,不过我们部门还是属于业务部门,差不多干了两年,因为公司属于比较传统的企业,自然没有互联网的内卷,基本上没什么加班,特别是第二年,基本上没啥事做!

这种情况下我开始意识到了问题,如果再这样呆下去,对自己的发展会很不利,如果部门的业务再推动不了,那么到时候也得面临调整,总之,留与不留对自己大多都是不好的,所以我就准备离开了!

成都在别人的映像中是一个休闲城市,吃喝玩乐,但是那是属于有钱人的,打工人只有辛苦和内卷,我就住在天府软件园对面,每晚软件园里面都是灯火通明的!

二、机缘巧合

不过对于我来说,我已经没有想法继续留在成都了,当时是想去杭州或者深圳,不过在离职完的第二天,贵阳这边的公司就叫我面试,我都不记得是啥时候投的简历!

因为两年来都没有面过试了,所以练练嘴皮子,经过几面后,给了offer,从开始面试到给offer差不多半个月,给了offer后我十来天就去入职了!

从离职后到进去新公司这段时间差不多一个月,我就在成都耍了20来天,一直在做思想斗争,说实话,去一线城市和回故乡这两个抉择是很难做选择的,为什么呢?我表达一下我的观点。

三、我认为的大城市小城市

首先,一线的机会肯定会比小城市的机会多,接触到的人也相对来说比较厉害一点,这样自己的视野也会开阔一点,不过这也要根据自己的能力来看,如果自己本身就啥也不是,那基本上也无缘接触到厉害的人!

其次,一线的人情世故不像小城市那么复杂,特别像深圳这样的城市,大家都是从外面来的,所以来了就是深圳人,包容性比较高,这样的话能够减少一些心理压力,而小城市则不然,因为好一点的单位,保安都会和你吹他家那个亲戚是省里的,不然他也谋不了这个职位,往上就更不用说,哈哈!

所以小城市的人因为地缘原因,就会产生一定的优越感,所以整体下来说,其实是不那么包容的,不那么开放的,在这样的环境下对自己或多或少有一定的影响,当然,大城市也会有,只是相对于小城市来说会轻很多!

以上只是很小的一部分,还有很多就没必要说了!

四、为什么我还要回到小城市

上面也说了因为很快拿到offer,还是在自己的故乡,而且这个企业在贵州省内来说也算比较好,属于本土企业,所以这算是一个因素,还有就是心中有一点想法想回到故土,因为很多时候确实会思念家乡的,所以二者一碰撞,自然给自己顺理成章找了一个理由回来,当然,也可以说是自己无能,这也是没错的!

五、我以为小城市相对来说比较轻松

我并不想把自己的时间都花费在工作上,因为我是一个把生活和工作分得比较清楚的人,现在是这样,以后也是这样!

这边公司是早上9:00晚上5:30,中午休息两小时,所以口头上听着倒是挺舒服,但是当任务压下来,一切美梦都是泡沫。

我来了两三个月了,除了第一个月没加班,后面基本上都加班,周末有时候也来加班,前天晚上还通宵上线了,昨天下午四点过睡醒来,就觉得应该写点什么!

不光是我们公司,我听在这边工作了很久的同事说,大家都差不多这样,所以卷不卷就不用说了。

六、不光是互联网卷,其他的更卷

前天晚上通宵上线的时候,我和几个同事聊天,我说实在干不动了,我准备考公了,他说:“你别想了,我考了那么多次都没上,更别说你连书都没去看过,你拿什么和人家卷!”。

我省内的很多朋友和同学现在都在考公,不过据我所知,基本没几个真的考上了公务员,有些已经毕业很多年,一边上班一边考,有些一毕业就全职考!

不光是考公务员,在贵阳这个地方,做啥工作都卷,用他们的话说,你去当销售,不打满五百个电话你出不来!

没错,小城市的卷是你想不到的,我们常说大城市太卷了,大城市虽然卷,但是机会多,薪酬高一点,而小城市不但机会少,而且薪酬也少,但是人多,特别对于贵阳这种城市,经济比较落后,做生做死三千几真的不是开玩笑的!

七、后悔了吗

我想说,一点也不后悔,虽然之前在成都很轻松,但是那不是我想要的,现在很忙,也累,不过也不是我想要的,那么这不是自相矛盾了吗?其实一点都不矛盾!

我始终觉得,如果你脑子里觉得你这辈子只有靠打工才能赚到钱,那么你将劳累一辈子!

现在打工对于我来说更多是积淀经验,养活自己,我不会迷失自己的,我清楚自己想要过什么生活,做什么样的人,加之我这个人的物质欲望比较低,所以我不会把自己活得很累,而是把钱和时间更多的用在提升自己和丰富自己上面!

八、建议

我想说人各有志,每个人所追求的人生不一样,每个人的欲望不一样,所以无论去一线还是小城市都没有真正的对与错!

首先对于我来说,我上面已经表达过,我对物质的欲望比较低,我是没多大的欲望,但是我很爱钱,我想做其他的事,发展其他的,所以对我来说,去任何一个地方的区别都不是很大,无非钱多点少点!

不过对于刚离开学校的朋友,如果想在职业生涯有所建树,并且家里条件不怎么好的,没啥背景资源的,尽量去一线城市,即使赚不了钱也长长见识,如果可以的话,尽量去互联网企业,别去传统行业,因为对自己的发展会比较不利!

如果一毕业就回到自己的那个小地方,那么就可能一辈子都出不来了,当然,如果有能力在自己的家乡混得风生水起,那么是真的牛逼,不过对于没啥资源背景的,基本上可能性不大!

九、最后

现在这个环境真的很严峻,各个行业都很难,真的很卷,所以无论一线还是十八线都一样,普通人想躺平基本上不可能!

对于我们来说,无论处于什么样的环境,都不要过于依赖它,不要故步自封,一定要保持思想活跃,有居安思危的意识,做好准备,这样才能保证在时代的浪潮中不被拍打得遍体鳞伤!

最后借用一句话:

人生成长最有效的方法,就是无论命运把你抛在任何一个点上,你就地展开做力所能及的事情