@PostMapping("/save.json")
    @AuthAction(AuthService.PrivilegeType.WRITE_RULE)
    public Result<ApiDefinitionEntity> updateApi(@RequestBody UpdateApiReqVo reqVo) {
        String app = reqVo.getApp();
        if (StringUtil.isBlank(app)) {
            return Result.ofFail(-1, "app can't be null or empty");
        }

        Long id = reqVo.getId();
        if (id == null) {
            return Result.ofFail(-1, "id can't be null");
        }

        ApiDefinitionEntity entity = repository.findById(id);
        if (entity == null) {
            return Result.ofFail(-1, "api does not exist, id=" + id);
        }

        // 匹配规则列表
        List<ApiPredicateItemVo> predicateItems = reqVo.getPredicateItems();
        if (CollectionUtils.isEmpty(predicateItems)) {
            return Result.ofFail(-1, "predicateItems can't empty");
        }

        List<ApiPredicateItemEntity> predicateItemEntities = new ArrayList<>();
        for (ApiPredicateItemVo predicateItem : predicateItems) {
            ApiPredicateItemEntity predicateItemEntity = new ApiPredicateItemEntity();

            // 匹配模式
            int matchStrategy = predicateItem.getMatchStrategy();
            if (!Arrays.asList(URL_MATCH_STRATEGY_EXACT, URL_MATCH_STRATEGY_PREFIX, URL_MATCH_STRATEGY_REGEX).contains(matchStrategy)) {
                return Result.ofFail(-1, "Invalid matchStrategy: " + matchStrategy);
            }
            predicateItemEntity.setMatchStrategy(matchStrategy);

            // 匹配串
            String pattern = predicateItem.getPattern();
            if (StringUtil.isBlank(pattern)) {
                return Result.ofFail(-1, "pattern can't be null or empty");
            }
            predicateItemEntity.setPattern(pattern);

            predicateItemEntities.add(predicateItemEntity);
        }
        entity.setPredicateItems(new LinkedHashSet<>(predicateItemEntities));

        Date date = new Date();
        entity.setGmtModified(date);

        try {
            entity = repository.save(entity);
        } catch (Throwable throwable) {
            logger.error("update gateway api error:", throwable);
            return Result.ofThrowable(-1, throwable);
        }

        if (!publishApis(app, entity.getIp(), entity.getPort())) {
            logger.warn("publish gateway apis fail after update");
        }

        return Result.ofSuccess(entity);
    }
@Test
    public void testUpdateApi() throws Exception {
        String path = "/gateway/api/save.json";

        // Add one entity to memory repository for update
        ApiDefinitionEntity addEntity = new ApiDefinitionEntity();
        addEntity.setApp(TEST_APP);
        addEntity.setIp(TEST_IP);
        addEntity.setPort(TEST_PORT);
        addEntity.setApiName("bbb");
        Date date = new Date();
        // To make the gmtModified different when do update
        date = DateUtils.addSeconds(date, -1);
        addEntity.setGmtCreate(date);
        addEntity.setGmtModified(date);
        Set<ApiPredicateItemEntity> addRedicateItemEntities = new HashSet<>();
        addEntity.setPredicateItems(addRedicateItemEntities);
        ApiPredicateItemEntity addPredicateItemEntity = new ApiPredicateItemEntity();
        addPredicateItemEntity.setMatchStrategy(URL_MATCH_STRATEGY_EXACT);
        addPredicateItemEntity.setPattern("/order");
        addEntity = repository.save(addEntity);

        UpdateApiReqVo reqVo = new UpdateApiReqVo();
        reqVo.setId(addEntity.getId());
        reqVo.setApp(TEST_APP);
        List<ApiPredicateItemVo> itemVos = new ArrayList<>();
        ApiPredicateItemVo itemVo = new ApiPredicateItemVo();
        itemVo.setMatchStrategy(URL_MATCH_STRATEGY_PREFIX);
        itemVo.setPattern("/my_order");
        itemVos.add(itemVo);
        reqVo.setPredicateItems(itemVos);

        given(sentinelApiClient.modifyApis(eq(TEST_APP), eq(TEST_IP), eq(TEST_PORT), any())).willReturn(true);

        MockHttpServletRequestBuilder requestBuilder = MockMvcRequestBuilders.post(path);
        requestBuilder.content(JSON.toJSONString(reqVo)).contentType(MediaType.APPLICATION_JSON);

        // Do controller logic
        MvcResult mvcResult = mockMvc.perform(requestBuilder)
                .andExpect(MockMvcResultMatchers.status().isOk())
                .andDo(MockMvcResultHandlers.print()).andReturn();

        // Verify the modifyApis method has been called
        verify(sentinelApiClient).modifyApis(eq(TEST_APP), eq(TEST_IP), eq(TEST_PORT), any());

        Result<ApiDefinitionEntity> result = JSONObject.parseObject(mvcResult.getResponse().getContentAsString(), new TypeReference<Result<ApiDefinitionEntity>>() {});
        assertTrue(result.isSuccess());

        ApiDefinitionEntity entity = result.getData();
        assertNotNull(entity);
        assertEquals("bbb", entity.getApiName());
        assertEquals(date, entity.getGmtCreate());
        // To make sure gmtModified has been set and it's different from gmtCreate
        assertNotNull(entity.getGmtModified());
        assertNotEquals(entity.getGmtCreate(), entity.getGmtModified());

        Set<ApiPredicateItemEntity> predicateItemEntities = entity.getPredicateItems();
        assertEquals(1, predicateItemEntities.size());
        ApiPredicateItemEntity predicateItemEntity = predicateItemEntities.iterator().next();
        assertEquals(URL_MATCH_STRATEGY_PREFIX, predicateItemEntity.getMatchStrategy().intValue());
        assertEquals("/my_order", predicateItemEntity.getPattern());

        // Verify the entity which is update in memory repository
        List<ApiDefinitionEntity> entitiesInMem = repository.findAllByApp(TEST_APP);
        assertEquals(1, entitiesInMem.size());
        assertEquals(entity, entitiesInMem.get(0));
    }