001/** 002 * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). 003 * <p> 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * <p> 008 * http://www.apache.org/licenses/LICENSE-2.0 009 * <p> 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016package io.jboot.apidoc; 017 018import com.alibaba.fastjson.JSONObject; 019import com.alibaba.fastjson.parser.Feature; 020import com.jfinal.core.Controller; 021import com.jfinal.kit.JsonKit; 022import com.jfinal.kit.Ret; 023import com.jfinal.kit.StrKit; 024import com.jfinal.plugin.activerecord.Page; 025import io.jboot.apidoc.annotation.Api; 026import io.jboot.apidoc.annotation.ApiOper; 027import io.jboot.apidoc.annotation.ApiResp; 028import io.jboot.apidoc.annotation.ApiResps; 029import io.jboot.utils.*; 030 031import java.io.File; 032import java.lang.reflect.Method; 033import java.lang.reflect.Modifier; 034import java.util.*; 035 036public class ApiDocManager { 037 038 private static final ApiDocManager me = new ApiDocManager(); 039 040 public static ApiDocManager me() { 041 return me; 042 } 043 044 //渲染器 045 private ApiDocRender render = ApiDocRender.MARKDOWN_RENDER; 046 047 //每个类对于的属性名称,一般支持从数据库读取 字段配置 填充,来源于 api-remarks.json 048 private Map<String, Map<String, String>> modelFieldRemarks = new HashMap<>(); 049 private Map<String, Map<String, String>> defaultModelFieldRemarks = new HashMap<>(); 050 051 //ClassType Mocks,来源于 api-mock.json 052 private Map<String, Object> classTypeMockDatas = new HashMap<>(); 053 private Map<Class<?>, ApiMockBuilder> classTypeMockBuilders = new HashMap<>(); 054 055 //ApiOperation 排序方式 056 private Comparator<ApiOperation> operationComparator; 057 058 private ApiDocManager() { 059 initDefaultClassTypeMockBuilder(); 060 } 061 062 private void initDefaultClassTypeMockBuilder() { 063 addClassTypeMockBuilders(Ret.class, ApiMockBuilders.retBuilder); 064 addClassTypeMockBuilders(Map.class, ApiMockBuilders.mapBuilder); 065 addClassTypeMockBuilders(List.class, ApiMockBuilders.listBuilder); 066 addClassTypeMockBuilders(Page.class, ApiMockBuilders.pageBuilder); 067 addClassTypeMockBuilders(String.class, ApiMockBuilders.stringBuilder); 068 } 069 070 public ApiDocRender getRender() { 071 return render; 072 } 073 074 public void setRender(ApiDocRender render) { 075 this.render = render; 076 } 077 078 public Map<String, Map<String, String>> getModelFieldRemarks() { 079 return modelFieldRemarks; 080 } 081 082 public void setModelFieldRemarks(Map<String, Map<String, String>> modelFieldRemarks) { 083 this.modelFieldRemarks = modelFieldRemarks; 084 } 085 086 public void addModelFieldRemarks(String classOrSimpleName, Map<String, String> fieldNames) { 087 this.modelFieldRemarks.put(classOrSimpleName, fieldNames); 088 } 089 090 public Map<String, Object> getClassTypeMockDatas() { 091 return classTypeMockDatas; 092 } 093 094 public void setClassTypeMockDatas(Map<String, Object> classTypeMockDatas) { 095 this.classTypeMockDatas = classTypeMockDatas; 096 } 097 098 public void addClassTypeMocks(String classType, Object mockData) { 099 this.classTypeMockDatas.put(classType, mockData); 100 } 101 102 public Object getClassTypeMockData(String classType) { 103 return this.classTypeMockDatas.get(classType); 104 } 105 106 public Comparator<ApiOperation> getOperationComparator() { 107 return operationComparator; 108 } 109 110 public void setOperationComparator(Comparator<ApiOperation> operationComparator) { 111 this.operationComparator = operationComparator; 112 } 113 114 public Map<Class<?>, ApiMockBuilder> getClassTypeMockBuilders() { 115 return classTypeMockBuilders; 116 } 117 118 public void setClassTypeMockBuilders(Map<Class<?>, ApiMockBuilder> classTypeMockBuilders) { 119 this.classTypeMockBuilders = classTypeMockBuilders; 120 } 121 122 public void addClassTypeMockBuilders(Class<?> forClass, ApiMockBuilder builder) { 123 this.classTypeMockBuilders.put(forClass, builder); 124 } 125 126 127 String buildMockJson(ClassType classType, Method method) { 128 return ApiDocUtil.prettyJson(JsonKit.toJson(doBuildMockObject(classType, method, 0))); 129 } 130 131 132 Object doBuildMockObject(ClassType classType, Method method, int level) { 133 Object retObject = getClassTypeMockData(classType.toString()); 134 135 if (retObject == null) { 136 retObject = getClassTypeMockData(classType.getMainClass().getName()); 137 } 138 139 if (retObject == null) { 140 retObject = getClassTypeMockData(StrKit.firstCharToLowerCase(classType.getMainClass().getSimpleName())); 141 } 142 143 if (retObject != null) { 144 return retObject; 145 } 146 147 for (Class<?> aClass : classTypeMockBuilders.keySet()) { 148 if (aClass.isAssignableFrom(classType.getMainClass())) { 149 Object object = classTypeMockBuilders.get(aClass).build(classType, method, level); 150 if (object != null) { 151 return object; 152 } 153 } 154 } 155 return null; 156 } 157 158 159 Map<String, List<ApiResponse>> buildRemarks(ClassType classType, Method method) { 160 Map<String, List<ApiResponse>> retMap = new LinkedHashMap<>(); 161 doBuildRemarks(retMap, classType, method, 0); 162 return retMap; 163 } 164 165 166 private void doBuildRemarks(Map<String, List<ApiResponse>> retMap, ClassType classType, Method method, int level) { 167 Class<?> mainClass = classType.getMainClass(); 168 169 Set<ApiResponse> apiResponses = new LinkedHashSet<>(); 170 171 //根据默认的配置构建 172 doBuildRemarksByDefault(apiResponses, classType, method); 173 174 List<Class<?>> dataTypeClasses = null; 175 176 //根据方法的 @Resp 来构建 177 if (level == 0) { 178 dataTypeClasses = doBuildRemarksByMethodAnnotation(apiResponses, method); 179 } 180 181 //根据配置文件来构建 182 doBuildRemarksByConfig(apiResponses, classType, method); 183 184 if (!apiResponses.isEmpty()) { 185 retMap.put(mainClass.getSimpleName(), new LinkedList<>(apiResponses)); 186 } 187 188 //必须执行在 retMap.put 之后,才能保证 remarks 表格在文档中的顺序 189 if (dataTypeClasses != null) { 190 for (Class<?> dataType : dataTypeClasses) { 191 doBuildRemarks(retMap, new ClassType(dataType), method, level + 1); 192 } 193 } 194 195 196 ClassType[] types = classType.getGenericTypes(); 197 if (types != null) { 198 for (ClassType type : types) { 199 doBuildRemarks(retMap, type, method, level + 1); 200 } 201 } 202 } 203 204 205 private void doBuildRemarksByDefault(Set<ApiResponse> apiResponses, ClassType classType, Method method1) { 206 Map<String, String> defaultModelRemarks = defaultModelFieldRemarks.get(classType.getMainClass().getName()); 207 if (defaultModelRemarks == null || defaultModelRemarks.isEmpty()) { 208 return; 209 } 210 211 List<Method> getterMethods = ReflectUtil.searchMethodList(classType.getMainClass(), m -> m.getParameterCount() == 0 212 && m.getReturnType() != void.class 213 && Modifier.isPublic(m.getModifiers()) 214 && (m.getName().startsWith("get") || m.getName().startsWith("is")) 215 && !"getClass".equals(m.getName()) 216 ); 217 218 Map<String, Method> filedAndMethodMap = new HashMap<>(); 219 for (Method getterMethod : getterMethods) { 220 filedAndMethodMap.put(ApiDocUtil.getterMethod2Field(getterMethod), getterMethod); 221 } 222 223 for (String key : defaultModelRemarks.keySet()) { 224 225 ApiResponse apiResponse = new ApiResponse(); 226 apiResponse.setField(key); 227 apiResponse.setRemarks(defaultModelRemarks.get(key)); 228 229 Method getterMethod = filedAndMethodMap.get(key); 230 if (getterMethod != null) { 231 apiResponse.setDataAndClassType(getterMethod.getReturnType()); 232 } 233 //若没有 getter 方法,一般情况下 map 或者 ret 等 234 //此时,需要通过 Mock 数据来对 key 的 dataType 进行推断 235 else { 236 Object object = doBuildMockObject(classType, method1, 0); 237 if (object instanceof Map) { 238 Object value = ((Map<?, ?>) object).get(key); 239 if (value != null) { 240 apiResponse.setDataAndClassType(value.getClass()); 241 } 242 } 243 } 244 245 apiResponses.add(apiResponse); 246 } 247 248 } 249 250 251 private List<Class<?>> doBuildRemarksByMethodAnnotation(Set<ApiResponse> apiResponses, Method method) { 252 253 List<ApiResponse> apiResponses1 = new LinkedList<>(); 254 List<Class<?>> dataTypes = new ArrayList<>(); 255 256 ApiResps apiResps = method.getAnnotation(ApiResps.class); 257 if (apiResps != null) { 258 for (ApiResp apiResp : apiResps.value()) { 259 apiResponses1.add(new ApiResponse(apiResp)); 260 dataTypes.add(apiResp.dataType()); 261 } 262 } 263 264 ApiResp apiResp = method.getAnnotation(ApiResp.class); 265 if (apiResp != null) { 266 apiResponses1.add(new ApiResponse(apiResp)); 267 dataTypes.add(apiResp.dataType()); 268 } 269 270 apiResponses.addAll(apiResponses1); 271 return dataTypes; 272 } 273 274 275 private void doBuildRemarksByConfig(Set<ApiResponse> apiResponses, ClassType classType, Method method1) { 276 Map<String, String> configRemarks = getConfigRemarks(classType.getMainClass()); 277 if (configRemarks == null || configRemarks.isEmpty()) { 278 return; 279 } 280 281 List<Method> getterMethods = ReflectUtil.searchMethodList(classType.getMainClass(), m -> m.getParameterCount() == 0 282 && m.getReturnType() != void.class 283 && Modifier.isPublic(m.getModifiers()) 284 && (m.getName().startsWith("get") || m.getName().startsWith("is")) 285 && !"getClass".equals(m.getName()) 286 ); 287 288 Map<String, Method> filedAndMethodMap = new HashMap<>(); 289 for (Method getterMethod : getterMethods) { 290 filedAndMethodMap.put(ApiDocUtil.getterMethod2Field(getterMethod), getterMethod); 291 } 292 293 294 for (String key : configRemarks.keySet()) { 295 296 ApiResponse apiResponse = new ApiResponse(); 297 apiResponse.setField(key); 298 apiResponse.setRemarks(configRemarks.get(key)); 299 300 Method getterMethod = filedAndMethodMap.get(key); 301 if (getterMethod != null) { 302 apiResponse.setDataAndClassType(getterMethod.getReturnType()); 303 } 304 //若没有 getter 方法,一般情况下 map 或者 ret 等 305 //此时,需要通过 Mock 数据来对 key 的 dataType 进行推断 306 else { 307 Object object = doBuildMockObject(classType, method1, 0); 308 if (object instanceof Map) { 309 Object value = ((Map<?, ?>) object).get(key); 310 if (value != null) { 311 apiResponse.setDataAndClassType(value.getClass()); 312 } 313 } 314 } 315 316 apiResponses.add(apiResponse); 317 } 318 } 319 320 private Map<String, String> getConfigRemarks(Class<?> clazz) { 321 Map<String, String> ret = modelFieldRemarks.get(clazz.getName()); 322 return ret != null ? ret : modelFieldRemarks.get(StrKit.firstCharToLowerCase(clazz.getSimpleName())); 323 } 324 325 326 /** 327 * 生成 API 文档 328 * 329 * @param config 330 */ 331 public void genDocs(ApiDocConfig config) { 332 List<Class> controllerClasses = ClassScanner.scanClass(aClass -> Controller.class.isAssignableFrom(aClass) && aClass.getAnnotation(Api.class) != null); 333 if (controllerClasses.isEmpty()) { 334 return; 335 } 336 337 initMockJson(config); 338 initModelRemarks(config); 339 340 List<ApiDocument> apiDocuments = new ArrayList<>(); 341 342 //所有 Controller 构建为同一个文档 343 if (config.isAllInOneEnable()) { 344 ApiDocument document = new ApiDocument(); 345 document.setValue(config.getAllInOneTitle()); 346 document.setNotes(config.getAllInOneNotes()); 347 document.setFilePath(config.getAllInOneFilePath()); 348 349 buildAllInOneDocument(document, controllerClasses, config); 350 351 apiDocuments.add(document); 352 } 353 //不同的 Controller 构建为不同的文档 354 else { 355 for (Class<?> controllerClass : controllerClasses) { 356 if (StrUtil.isNotBlank(config.getPackagePrefix())) { 357 if (controllerClass.getName().startsWith(config.getPackagePrefix())) { 358 apiDocuments.add(buildDocument(controllerClass, config)); 359 } 360 } else { 361 apiDocuments.add(buildDocument(controllerClass, config)); 362 } 363 } 364 } 365 366 if (render != null) { 367 render.render(apiDocuments, config); 368 } 369 } 370 371 372 private void initMockJson(ApiDocConfig config) { 373 File mockJsonFile = new File(config.getMockJsonPathAbsolute()); 374 if (mockJsonFile.exists()) { 375 String mockJsonString = FileUtil.readString(mockJsonFile); 376 JSONObject mockJsonObject = JSONObject.parseObject(mockJsonString, Feature.OrderedField); 377 for (String classTypeKey : mockJsonObject.keySet()) { 378 this.classTypeMockDatas.put(classTypeKey, mockJsonObject.get(classTypeKey)); 379 } 380 } 381 } 382 383 384 private void initModelRemarks(ApiDocConfig config) { 385 386 Map<String, String> pageRemarks = new LinkedHashMap<>(); 387 pageRemarks.put("totalRow", "总行数"); 388 pageRemarks.put("pageNumber", "当前页码"); 389 pageRemarks.put("firstPage", "是否是第一页"); 390 pageRemarks.put("lastPage", "是否是最后一页"); 391 pageRemarks.put("totalPage", "总页数"); 392 pageRemarks.put("pageSize", "每页数据量"); 393 pageRemarks.put("list", "数据列表"); 394 defaultModelFieldRemarks.put(Page.class.getName(), pageRemarks); 395 396 397 Map<String, String> retRemarks = new LinkedHashMap<>(); 398 retRemarks.put("state", "状态,成功 ok,失败 fail"); 399 defaultModelFieldRemarks.put(Ret.class.getName(), retRemarks); 400 401 Map<String, String> apiRetRemarks = new LinkedHashMap<>(); 402 apiRetRemarks.put("state", "状态,成功 ok,失败 fail"); 403 apiRetRemarks.put("errorCode", "错误码,可能返回 null"); 404 apiRetRemarks.put("message", "错误消息"); 405 apiRetRemarks.put("data", "数据"); 406 defaultModelFieldRemarks.put(ApiRet.class.getName(), apiRetRemarks); 407 408 409 File modelJsonFile = new File(config.getRemarksJsonPathAbsolute()); 410 if (modelJsonFile.exists()) { 411 String modelJsonString = FileUtil.readString(modelJsonFile); 412 JSONObject modelJsonObject = JSONObject.parseObject(modelJsonString, Feature.OrderedField); 413 for (String classOrSimpleName : modelJsonObject.keySet()) { 414 Map<String, String> remarks = new LinkedHashMap<>(); 415 JSONObject modelRemarks = modelJsonObject.getJSONObject(classOrSimpleName); 416 modelRemarks.forEach((k, v) -> remarks.put(k, String.valueOf(v))); 417 addModelFieldRemarks(classOrSimpleName, remarks); 418 } 419 } 420 } 421 422 423 private void buildAllInOneDocument(ApiDocument document, List<Class> controllerClasses, ApiDocConfig config) { 424 for (Class<?> controllerClass : controllerClasses) { 425 buildOperation(document, controllerClass, config); 426 } 427 428 List<ApiOperation> operations = document.getApiOperations(); 429 if (operations != null) { 430 if (operationComparator != null) { 431 operations.sort(operationComparator); 432 } else { 433 operations.sort(Comparator.comparing(ApiOperation::getActionKey)); 434 } 435 } 436 } 437 438 439 private ApiDocument buildDocument(Class<?> controllerClass, ApiDocConfig config) { 440 441 Api api = controllerClass.getAnnotation(Api.class); 442 ApiDocument document = new ApiDocument(); 443 document.setControllerClass(controllerClass); 444 document.setValue(api.value()); 445 document.setNotes(api.notes()); 446 document.setFilePathByControllerPath(ApiDocUtil.getControllerPath(controllerClass)); 447 448 449 String filePath = api.filePath(); 450 if (StrUtil.isNotBlank(filePath)) { 451 document.setFilePath(filePath); 452 } 453 454 buildOperation(document, controllerClass, config); 455 456 List<ApiOperation> operations = document.getApiOperations(); 457 if (operations != null) { 458 if (operationComparator != null) { 459 operations.sort(operationComparator); 460 } else { 461 operations.sort(new Comparator<ApiOperation>() { 462 @Override 463 public int compare(ApiOperation o1, ApiOperation o2) { 464 return o1.getOrderNo() == o2.getOrderNo() ? o1.getMethod().getName().compareTo(o2.getMethod().getName()) : o1.getOrderNo() - o2.getOrderNo(); 465 } 466 }); 467 } 468 } 469 470 return document; 471 } 472 473 474 private void buildOperation(ApiDocument document, Class<?> controllerClass, ApiDocConfig config) { 475 476 List<Method> methods = ReflectUtil.searchMethodList(controllerClass, 477 method -> method.getAnnotation(ApiOper.class) != null && Modifier.isPublic(method.getModifiers())); 478 479 String controllerPath = ApiDocUtil.getControllerPath(controllerClass); 480 HttpMethod defaultHttpMethod = ApiDocUtil.getControllerMethod(controllerClass); 481 482 for (Method method : methods) { 483 ApiOper apiOper = method.getAnnotation(ApiOper.class); 484 485 ApiOperation apiOperation = new ApiOperation(); 486 apiOperation.setControllerClass(controllerClass); 487 488 Class<?> containerClass = apiOper.containerClass() != void.class ? apiOper.containerClass() : config.getDefaultContainerClass(); 489 apiOperation.setMethodAndInfo(method, controllerPath, ApiDocUtil.getMethodHttpMethods(method, defaultHttpMethod), containerClass); 490 491 492 apiOperation.setValue(apiOper.value()); 493 apiOperation.setNotes(apiOper.notes()); 494 apiOperation.setParaNotes(apiOper.paraNotes()); 495 apiOperation.setOrderNo(apiOper.orderNo()); 496 apiOperation.setContentType(apiOper.contentType()); 497 498 document.addOperation(apiOperation); 499 } 500 501 502 Api api = controllerClass.getAnnotation(Api.class); 503 if (api != null) { 504 Class<?>[] collectClasses = api.collect(); 505 for (Class<?> collectClass : collectClasses) { 506 507 List<Method> collectMethods = ReflectUtil.searchMethodList(collectClass, 508 method -> method.getAnnotation(ApiOper.class) != null && Modifier.isPublic(method.getModifiers())); 509 510 String collectControllerPath = ApiDocUtil.getControllerPath(collectClass); 511 HttpMethod collectDefaultHttpMethod = ApiDocUtil.getControllerMethod(controllerClass); 512 513 for (Method method : collectMethods) { 514 ApiOper apiOper = method.getAnnotation(ApiOper.class); 515 516 ApiOperation apiOperation = new ApiOperation(); 517 apiOperation.setControllerClass(collectClass); 518 519 Class<?> containerClass = apiOper.containerClass() != void.class ? apiOper.containerClass() : config.getDefaultContainerClass(); 520 apiOperation.setMethodAndInfo(method, collectControllerPath, ApiDocUtil.getMethodHttpMethods(method, collectDefaultHttpMethod), containerClass); 521 522 523 apiOperation.setValue(apiOper.value()); 524 apiOperation.setNotes(apiOper.notes()); 525 apiOperation.setParaNotes(apiOper.paraNotes()); 526 apiOperation.setOrderNo(apiOper.orderNo()); 527 apiOperation.setContentType(apiOper.contentType()); 528 529 document.addOperation(apiOperation); 530 } 531 } 532 } 533 } 534 535 536}