Commit 6225e18f70a385c6bfcdfcf40ee53e23d09b304c

Authored by YunaiV
1 parent 949d5a18

【新增】AI:绘图(MJ)接入 API KEY 管理

yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/image/AiImageController.java
@@ -9,7 +9,7 @@ import cn.iocoder.yudao.framework.common.util.object.BeanUtils; @@ -9,7 +9,7 @@ import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
9 import cn.iocoder.yudao.module.ai.controller.admin.image.vo.AiImageDrawReqVO; 9 import cn.iocoder.yudao.module.ai.controller.admin.image.vo.AiImageDrawReqVO;
10 import cn.iocoder.yudao.module.ai.controller.admin.image.vo.AiImagePageReqVO; 10 import cn.iocoder.yudao.module.ai.controller.admin.image.vo.AiImagePageReqVO;
11 import cn.iocoder.yudao.module.ai.controller.admin.image.vo.AiImageRespVO; 11 import cn.iocoder.yudao.module.ai.controller.admin.image.vo.AiImageRespVO;
12 -import cn.iocoder.yudao.module.ai.controller.admin.image.vo.AiImageUpdatePublicStatusReqVO; 12 +import cn.iocoder.yudao.module.ai.controller.admin.image.vo.AiImageUpdateReqVO;
13 import cn.iocoder.yudao.module.ai.controller.admin.image.vo.midjourney.AiMidjourneyActionReqVO; 13 import cn.iocoder.yudao.module.ai.controller.admin.image.vo.midjourney.AiMidjourneyActionReqVO;
14 import cn.iocoder.yudao.module.ai.controller.admin.image.vo.midjourney.AiMidjourneyImagineReqVO; 14 import cn.iocoder.yudao.module.ai.controller.admin.image.vo.midjourney.AiMidjourneyImagineReqVO;
15 import cn.iocoder.yudao.module.ai.dal.dataobject.image.AiImageDO; 15 import cn.iocoder.yudao.module.ai.dal.dataobject.image.AiImageDO;
@@ -114,11 +114,11 @@ public class AiImageController { @@ -114,11 +114,11 @@ public class AiImageController {
114 return success(BeanUtils.toBean(pageResult, AiImageRespVO.class)); 114 return success(BeanUtils.toBean(pageResult, AiImageRespVO.class));
115 } 115 }
116 116
117 - @PutMapping("/update-public-status")  
118 - @Operation(summary = "更新绘画发布状态") 117 + @PutMapping("/update")
  118 + @Operation(summary = "更新绘画")
119 @PreAuthorize("@ss.hasPermission('ai:image:update')") 119 @PreAuthorize("@ss.hasPermission('ai:image:update')")
120 - public CommonResult<Boolean> updateImagePublicStatus(@Valid @RequestBody AiImageUpdatePublicStatusReqVO updateReqVO) {  
121 - imageService.updateImagePublicStatus(updateReqVO); 120 + public CommonResult<Boolean> updateImage(@Valid @RequestBody AiImageUpdateReqVO updateReqVO) {
  121 + imageService.updateImage(updateReqVO);
122 return success(true); 122 return success(true);
123 } 123 }
124 124
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/image/vo/AiImageUpdatePublicStatusReqVO.java renamed to yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/image/vo/AiImageUpdateReqVO.java
@@ -4,15 +4,15 @@ import io.swagger.v3.oas.annotations.media.Schema; @@ -4,15 +4,15 @@ import io.swagger.v3.oas.annotations.media.Schema;
4 import jakarta.validation.constraints.NotNull; 4 import jakarta.validation.constraints.NotNull;
5 import lombok.Data; 5 import lombok.Data;
6 6
7 -@Schema(description = "管理后台 - AI 绘画修改发布状态 Request VO") 7 +@Schema(description = "管理后台 - AI 绘画修改 Request VO")
8 @Data 8 @Data
9 -public class AiImageUpdatePublicStatusReqVO { 9 +public class AiImageUpdateReqVO {
10 10
11 @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "15583") 11 @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "15583")
  12 + @NotNull(message = "编号不能为空")
12 private Long id; 13 private Long id;
13 14
14 - @Schema(description = "是否发布", requiredMode = Schema.RequiredMode.REQUIRED, example = "true")  
15 - @NotNull(message = "是否发布不能为空") 15 + @Schema(description = "是否发布", example = "true")
16 private Boolean publicStatus; 16 private Boolean publicStatus;
17 17
18 } 18 }
19 \ No newline at end of file 19 \ No newline at end of file
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/image/AiImageService.java
@@ -5,7 +5,7 @@ import cn.iocoder.yudao.framework.common.pojo.PageParam; @@ -5,7 +5,7 @@ import cn.iocoder.yudao.framework.common.pojo.PageParam;
5 import cn.iocoder.yudao.framework.common.pojo.PageResult; 5 import cn.iocoder.yudao.framework.common.pojo.PageResult;
6 import cn.iocoder.yudao.module.ai.controller.admin.image.vo.AiImageDrawReqVO; 6 import cn.iocoder.yudao.module.ai.controller.admin.image.vo.AiImageDrawReqVO;
7 import cn.iocoder.yudao.module.ai.controller.admin.image.vo.AiImagePageReqVO; 7 import cn.iocoder.yudao.module.ai.controller.admin.image.vo.AiImagePageReqVO;
8 -import cn.iocoder.yudao.module.ai.controller.admin.image.vo.AiImageUpdatePublicStatusReqVO; 8 +import cn.iocoder.yudao.module.ai.controller.admin.image.vo.AiImageUpdateReqVO;
9 import cn.iocoder.yudao.module.ai.controller.admin.image.vo.midjourney.AiMidjourneyActionReqVO; 9 import cn.iocoder.yudao.module.ai.controller.admin.image.vo.midjourney.AiMidjourneyActionReqVO;
10 import cn.iocoder.yudao.module.ai.controller.admin.image.vo.midjourney.AiMidjourneyImagineReqVO; 10 import cn.iocoder.yudao.module.ai.controller.admin.image.vo.midjourney.AiMidjourneyImagineReqVO;
11 import cn.iocoder.yudao.module.ai.dal.dataobject.image.AiImageDO; 11 import cn.iocoder.yudao.module.ai.dal.dataobject.image.AiImageDO;
@@ -71,11 +71,11 @@ public interface AiImageService { @@ -71,11 +71,11 @@ public interface AiImageService {
71 PageResult<AiImageDO> getImagePage(AiImagePageReqVO pageReqVO); 71 PageResult<AiImageDO> getImagePage(AiImagePageReqVO pageReqVO);
72 72
73 /** 73 /**
74 - * 更新绘画发布状态 74 + * 更新绘画
75 * 75 *
76 * @param updateReqVO 更新信息 76 * @param updateReqVO 更新信息
77 */ 77 */
78 - void updateImagePublicStatus(@Valid AiImageUpdatePublicStatusReqVO updateReqVO); 78 + void updateImage(@Valid AiImageUpdateReqVO updateReqVO);
79 79
80 /** 80 /**
81 * 删除绘画 81 * 删除绘画
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/image/AiImageServiceImpl.java
@@ -15,7 +15,7 @@ import cn.iocoder.yudao.framework.common.pojo.PageResult; @@ -15,7 +15,7 @@ import cn.iocoder.yudao.framework.common.pojo.PageResult;
15 import cn.iocoder.yudao.framework.common.util.object.BeanUtils; 15 import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
16 import cn.iocoder.yudao.module.ai.controller.admin.image.vo.AiImageDrawReqVO; 16 import cn.iocoder.yudao.module.ai.controller.admin.image.vo.AiImageDrawReqVO;
17 import cn.iocoder.yudao.module.ai.controller.admin.image.vo.AiImagePageReqVO; 17 import cn.iocoder.yudao.module.ai.controller.admin.image.vo.AiImagePageReqVO;
18 -import cn.iocoder.yudao.module.ai.controller.admin.image.vo.AiImageUpdatePublicStatusReqVO; 18 +import cn.iocoder.yudao.module.ai.controller.admin.image.vo.AiImageUpdateReqVO;
19 import cn.iocoder.yudao.module.ai.controller.admin.image.vo.midjourney.AiMidjourneyActionReqVO; 19 import cn.iocoder.yudao.module.ai.controller.admin.image.vo.midjourney.AiMidjourneyActionReqVO;
20 import cn.iocoder.yudao.module.ai.controller.admin.image.vo.midjourney.AiMidjourneyImagineReqVO; 20 import cn.iocoder.yudao.module.ai.controller.admin.image.vo.midjourney.AiMidjourneyImagineReqVO;
21 import cn.iocoder.yudao.module.ai.dal.dataobject.image.AiImageDO; 21 import cn.iocoder.yudao.module.ai.dal.dataobject.image.AiImageDO;
@@ -62,9 +62,6 @@ public class AiImageServiceImpl implements AiImageService { @@ -62,9 +62,6 @@ public class AiImageServiceImpl implements AiImageService {
62 @Resource 62 @Resource
63 private AiApiKeyService apiKeyService; 63 private AiApiKeyService apiKeyService;
64 64
65 - @Resource  
66 - private MidjourneyApi midjourneyApi;  
67 -  
68 @Override 65 @Override
69 public PageResult<AiImageDO> getImagePageMy(Long userId, PageParam pageReqVO) { 66 public PageResult<AiImageDO> getImagePageMy(Long userId, PageParam pageReqVO) {
70 return imageMapper.selectPage(userId, pageReqVO); 67 return imageMapper.selectPage(userId, pageReqVO);
@@ -151,7 +148,7 @@ public class AiImageServiceImpl implements AiImageService { @@ -151,7 +148,7 @@ public class AiImageServiceImpl implements AiImageService {
151 } 148 }
152 149
153 @Override 150 @Override
154 - public void updateImagePublicStatus(AiImageUpdatePublicStatusReqVO updateReqVO) { 151 + public void updateImage(AiImageUpdateReqVO updateReqVO) {
155 // 1. 校验存在 152 // 1. 校验存在
156 validateImageExists(updateReqVO.getId()); 153 validateImageExists(updateReqVO.getId());
157 // 2. 更新发布状态 154 // 2. 更新发布状态
@@ -179,6 +176,7 @@ public class AiImageServiceImpl implements AiImageService { @@ -179,6 +176,7 @@ public class AiImageServiceImpl implements AiImageService {
179 @Override 176 @Override
180 @Transactional(rollbackFor = Exception.class) 177 @Transactional(rollbackFor = Exception.class)
181 public Long midjourneyImagine(Long userId, AiMidjourneyImagineReqVO reqVO) { 178 public Long midjourneyImagine(Long userId, AiMidjourneyImagineReqVO reqVO) {
  179 + MidjourneyApi midjourneyApi = apiKeyService.getMidjourneyApi();
182 // 1. 保存数据库 180 // 1. 保存数据库
183 AiImageDO image = BeanUtils.toBean(reqVO, AiImageDO.class).setUserId(userId).setPublicStatus(false) 181 AiImageDO image = BeanUtils.toBean(reqVO, AiImageDO.class).setUserId(userId).setPublicStatus(false)
184 .setStatus(AiImageStatusEnum.IN_PROGRESS.getStatus()) 182 .setStatus(AiImageStatusEnum.IN_PROGRESS.getStatus())
@@ -206,6 +204,7 @@ public class AiImageServiceImpl implements AiImageService { @@ -206,6 +204,7 @@ public class AiImageServiceImpl implements AiImageService {
206 204
207 @Override 205 @Override
208 public Integer midjourneySync() { 206 public Integer midjourneySync() {
  207 + MidjourneyApi midjourneyApi = apiKeyService.getMidjourneyApi();
209 // 1.1 获取 Midjourney 平台,状态在 “进行中” 的 image 208 // 1.1 获取 Midjourney 平台,状态在 “进行中” 的 image
210 List<AiImageDO> imageList = imageMapper.selectListByStatusAndPlatform( 209 List<AiImageDO> imageList = imageMapper.selectListByStatusAndPlatform(
211 AiImageStatusEnum.IN_PROGRESS.getStatus(), AiPlatformEnum.MIDJOURNEY.getPlatform()); 210 AiImageStatusEnum.IN_PROGRESS.getStatus(), AiPlatformEnum.MIDJOURNEY.getPlatform());
@@ -272,6 +271,7 @@ public class AiImageServiceImpl implements AiImageService { @@ -272,6 +271,7 @@ public class AiImageServiceImpl implements AiImageService {
272 271
273 @Override 272 @Override
274 public Long midjourneyAction(Long userId, AiMidjourneyActionReqVO reqVO) { 273 public Long midjourneyAction(Long userId, AiMidjourneyActionReqVO reqVO) {
  274 + MidjourneyApi midjourneyApi = apiKeyService.getMidjourneyApi();
275 // 1.1 检查 image 275 // 1.1 检查 image
276 AiImageDO image = validateImageExists(reqVO.getId()); 276 AiImageDO image = validateImageExists(reqVO.getId());
277 if (ObjUtil.notEqual(userId, image.getUserId())) { 277 if (ObjUtil.notEqual(userId, image.getUserId())) {
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/image/MidjourneyImageOptions.java deleted
1 -package cn.iocoder.yudao.module.ai.service.image;  
2 -  
3 -import lombok.Data;  
4 -import org.springframework.ai.image.ImageOptions;  
5 -  
6 -/**  
7 - * @author fansili  
8 - * @time 2024/6/5 10:34  
9 - * @since 1.0  
10 - */  
11 -@Data  
12 -public class MidjourneyImageOptions implements ImageOptions {  
13 - /**  
14 - * 模型  
15 - */  
16 - private String model;  
17 - /**  
18 - * 宽度  
19 - */  
20 - private Integer width;  
21 - /**  
22 - * 高度  
23 - */  
24 - private Integer height;  
25 - /**  
26 - * 版本  
27 - */  
28 - private String version;  
29 - /**  
30 - * 参数  
31 - */  
32 - private String state;  
33 -  
34 - @Override  
35 - public Integer getN() {  
36 - return 0;  
37 - }  
38 -  
39 - @Override  
40 - public String getModel() {  
41 - return model;  
42 - }  
43 -  
44 - @Override  
45 - public Integer getWidth() {  
46 - return width;  
47 - }  
48 -  
49 - @Override  
50 - public Integer getHeight() {  
51 - return height;  
52 - }  
53 -  
54 - @Override  
55 - public String getResponseFormat() {  
56 - return "";  
57 - }  
58 -}  
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/model/AiApiKeyService.java
1 package cn.iocoder.yudao.module.ai.service.model; 1 package cn.iocoder.yudao.module.ai.service.model;
2 2
3 import cn.iocoder.yudao.framework.ai.core.enums.AiPlatformEnum; 3 import cn.iocoder.yudao.framework.ai.core.enums.AiPlatformEnum;
  4 +import cn.iocoder.yudao.framework.ai.core.model.midjourney.api.MidjourneyApi;
4 import cn.iocoder.yudao.framework.ai.core.model.suno.api.SunoApi; 5 import cn.iocoder.yudao.framework.ai.core.model.suno.api.SunoApi;
5 import cn.iocoder.yudao.framework.common.pojo.PageResult; 6 import cn.iocoder.yudao.framework.common.pojo.PageResult;
6 import cn.iocoder.yudao.module.ai.controller.admin.model.vo.apikey.AiApiKeyPageReqVO; 7 import cn.iocoder.yudao.module.ai.controller.admin.model.vo.apikey.AiApiKeyPageReqVO;
@@ -93,6 +94,15 @@ public interface AiApiKeyService { @@ -93,6 +94,15 @@ public interface AiApiKeyService {
93 ImageClient getImageClient(AiPlatformEnum platform); 94 ImageClient getImageClient(AiPlatformEnum platform);
94 95
95 /** 96 /**
  97 + * 获得 MidjourneyApi 对象
  98 + *
  99 + * TODO 可优化点:目前默认获取 Midjourney 对应的第一个开启的配置用于绘画;后续可以支持配置选择
  100 + *
  101 + * @return MidjourneyApi 对象
  102 + */
  103 + MidjourneyApi getMidjourneyApi();
  104 +
  105 + /**
96 * 获得 SunoApi 对象 106 * 获得 SunoApi 对象
97 * 107 *
98 * TODO 可优化点:目前默认获取 Suno 对应的第一个开启的配置用于音乐;后续可以支持配置选择 108 * TODO 可优化点:目前默认获取 Suno 对应的第一个开启的配置用于音乐;后续可以支持配置选择
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/model/AiApiKeyServiceImpl.java
@@ -2,6 +2,7 @@ package cn.iocoder.yudao.module.ai.service.model; @@ -2,6 +2,7 @@ package cn.iocoder.yudao.module.ai.service.model;
2 2
3 import cn.iocoder.yudao.framework.ai.core.enums.AiPlatformEnum; 3 import cn.iocoder.yudao.framework.ai.core.enums.AiPlatformEnum;
4 import cn.iocoder.yudao.framework.ai.core.factory.AiClientFactory; 4 import cn.iocoder.yudao.framework.ai.core.factory.AiClientFactory;
  5 +import cn.iocoder.yudao.framework.ai.core.model.midjourney.api.MidjourneyApi;
5 import cn.iocoder.yudao.framework.ai.core.model.suno.api.SunoApi; 6 import cn.iocoder.yudao.framework.ai.core.model.suno.api.SunoApi;
6 import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; 7 import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
7 import cn.iocoder.yudao.framework.common.pojo.PageResult; 8 import cn.iocoder.yudao.framework.common.pojo.PageResult;
@@ -113,6 +114,16 @@ public class AiApiKeyServiceImpl implements AiApiKeyService { @@ -113,6 +114,16 @@ public class AiApiKeyServiceImpl implements AiApiKeyService {
113 } 114 }
114 115
115 @Override 116 @Override
  117 + public MidjourneyApi getMidjourneyApi() {
  118 + AiApiKeyDO apiKey = apiKeyMapper.selectFirstByPlatformAndStatus(
  119 + AiPlatformEnum.MIDJOURNEY.getPlatform(), CommonStatusEnum.ENABLE.getStatus());
  120 + if (apiKey == null) {
  121 + return null;
  122 + }
  123 + return clientFactory.getOrCreateMidjourneyApi(apiKey.getApiKey(), apiKey.getUrl());
  124 + }
  125 +
  126 + @Override
116 public SunoApi getSunoApi() { 127 public SunoApi getSunoApi() {
117 AiApiKeyDO apiKey = apiKeyMapper.selectFirstByPlatformAndStatus( 128 AiApiKeyDO apiKey = apiKeyMapper.selectFirstByPlatformAndStatus(
118 AiPlatformEnum.SUNO.getPlatform(), CommonStatusEnum.ENABLE.getStatus()); 129 AiPlatformEnum.SUNO.getPlatform(), CommonStatusEnum.ENABLE.getStatus());
yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/factory/AiClientFactory.java
1 package cn.iocoder.yudao.framework.ai.core.factory; 1 package cn.iocoder.yudao.framework.ai.core.factory;
2 2
3 import cn.iocoder.yudao.framework.ai.core.enums.AiPlatformEnum; 3 import cn.iocoder.yudao.framework.ai.core.enums.AiPlatformEnum;
  4 +import cn.iocoder.yudao.framework.ai.core.model.midjourney.api.MidjourneyApi;
4 import cn.iocoder.yudao.framework.ai.core.model.suno.api.SunoApi; 5 import cn.iocoder.yudao.framework.ai.core.model.suno.api.SunoApi;
5 import org.springframework.ai.chat.StreamingChatClient; 6 import org.springframework.ai.chat.StreamingChatClient;
6 import org.springframework.ai.image.ImageClient; 7 import org.springframework.ai.image.ImageClient;
@@ -57,6 +58,17 @@ public interface AiClientFactory { @@ -57,6 +58,17 @@ public interface AiClientFactory {
57 ImageClient getOrCreateImageClient(AiPlatformEnum platform, String apiKey, String url); 58 ImageClient getOrCreateImageClient(AiPlatformEnum platform, String apiKey, String url);
58 59
59 /** 60 /**
  61 + * 基于指定配置,获得 MidjourneyApi 对象
  62 + *
  63 + * 如果不存在,则进行创建
  64 + *
  65 + * @param apiKey API KEY
  66 + * @param url API URL
  67 + * @return MidjourneyApi 对象
  68 + */
  69 + MidjourneyApi getOrCreateMidjourneyApi(String apiKey, String url);
  70 +
  71 + /**
60 * 基于指定配置,获得 SunoApi 对象 72 * 基于指定配置,获得 SunoApi 对象
61 * 73 *
62 * 如果不存在,则进行创建 74 * 如果不存在,则进行创建
yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/factory/AiClientFactoryImpl.java
@@ -9,6 +9,7 @@ import cn.hutool.extra.spring.SpringUtil; @@ -9,6 +9,7 @@ import cn.hutool.extra.spring.SpringUtil;
9 import cn.iocoder.yudao.framework.ai.config.YudaoAiAutoConfiguration; 9 import cn.iocoder.yudao.framework.ai.config.YudaoAiAutoConfiguration;
10 import cn.iocoder.yudao.framework.ai.config.YudaoAiProperties; 10 import cn.iocoder.yudao.framework.ai.config.YudaoAiProperties;
11 import cn.iocoder.yudao.framework.ai.core.enums.AiPlatformEnum; 11 import cn.iocoder.yudao.framework.ai.core.enums.AiPlatformEnum;
  12 +import cn.iocoder.yudao.framework.ai.core.model.midjourney.api.MidjourneyApi;
12 import cn.iocoder.yudao.framework.ai.core.model.suno.api.SunoApi; 13 import cn.iocoder.yudao.framework.ai.core.model.suno.api.SunoApi;
13 import cn.iocoder.yudao.framework.ai.core.model.tongyi.QianWenChatClient; 14 import cn.iocoder.yudao.framework.ai.core.model.tongyi.QianWenChatClient;
14 import cn.iocoder.yudao.framework.ai.core.model.tongyi.QianWenChatModal; 15 import cn.iocoder.yudao.framework.ai.core.model.tongyi.QianWenChatModal;
@@ -111,8 +112,18 @@ public class AiClientFactoryImpl implements AiClientFactory { @@ -111,8 +112,18 @@ public class AiClientFactoryImpl implements AiClientFactory {
111 } 112 }
112 113
113 @Override 114 @Override
  115 + public MidjourneyApi getOrCreateMidjourneyApi(String apiKey, String url) {
  116 + String cacheKey = buildClientCacheKey(MidjourneyApi.class, AiPlatformEnum.MIDJOURNEY.getPlatform(), apiKey, url);
  117 + return Singleton.get(cacheKey, (Func0<MidjourneyApi>) () -> {
  118 + YudaoAiProperties.MidjourneyProperties properties = SpringUtil.getBean(YudaoAiProperties.class).getMidjourney();
  119 + return new MidjourneyApi(url, apiKey, properties.getNotifyUrl());
  120 + });
  121 + }
  122 +
  123 + @Override
114 public SunoApi getOrCreateSunoApi(String apiKey, String url) { 124 public SunoApi getOrCreateSunoApi(String apiKey, String url) {
115 - return new SunoApi(url); 125 + String cacheKey = buildClientCacheKey(SunoApi.class, AiPlatformEnum.SUNO.getPlatform(), apiKey, url);
  126 + return Singleton.get(cacheKey, (Func0<SunoApi>) () -> new SunoApi(url));
116 } 127 }
117 128
118 private static String buildClientCacheKey(Class<?> clazz, Object... params) { 129 private static String buildClientCacheKey(Class<?> clazz, Object... params) {