# uaa-behind-zuul-sample **Repository Path**: superch/uaa-behind-zuul-sample ## Basic Information - **Project Name**: uaa-behind-zuul-sample - **Description**: No description available - **Primary Language**: Java - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 3 - **Forks**: 0 - **Created**: 2018-03-22 - **Last Updated**: 2020-12-19 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # 架构说明 ## 工程使用 ### 启动&关闭 #### 1. 从IDEA启动 1.对root工程执行`clean,compile`;如果有Class对QCtiy之类的引用报错,则在对api-domain工程执行一次`clean,compile`,目的是执行APT插件生成QueryDSL所需代码。 2.启动`service-gegistry` 3.启动`uaa-service`,`dummy-service`,`api-gateway` ![run-dashboard](x-readme-imgs/run-dashboard.png) #### 2. 通过`docker-compose`启动 进入工程根目录 * 启动 * 编译打包 * `mvn clean package -Pdocker -Dmaven.test.skip=true` * docker启动 * `docker-compose up -d` * 查看实时日志 * `docker-compose logs -f` * 关闭 * `docker-compose stop` * 清除容器 * `docker-compose rm -f` ### 数据初始化 `http://localhost:8765/dummy/init-data` ### 动作确认 1) 访问:`http://localhost:8765/dummy/swagger-ui.html` 2) 点开要测试的API 3) 点击[Try it out!] ### [`UAA`]登录用户信息 ``` 普通用户(USER):user/password 管理员(ADMIN):admin/admin ``` ### [`UAA`]客户端注册信息 ``` clientId: acme clientSecret: acmesecret authorizedGrantTypes: authorization_code, refresh_token, password, client_credentials scopes: openid ``` ## 技术选型 * `Spring Cloud Netflix(Eureka, Hystrix, Zuul, Archaius, etc.).` * `Spring Cloud Security` * `Spring Data Jpa` * `QueryDSL` * `Swagger2` * `JWT(JSON Web Tokens)` * `H2Database/MySQL` * `Redis` ## 分层设计 ``` 表现层(PC/移动端/React.js) ↓ 调度层(Spring Cloud) ↓ 业务层(业务逻辑处理/数据处理/Spring全家桶) ↓ 数据层(Redis/MySQL) ``` ## 业务架构设计 ``` +---------------+ │ User Request │ +-------+-------+ │ │ +-------v-------+ │ ApiGateway │ +-------+-------+ │ │ +-----------------------------------v---------------------------------------+ │ +--------------+ +--------------+ +---------------+ │ │ │ micro │ │ micro │ │ micro │ │ │ │ service1 │ │ service2 │ ...... │ serviceN │ │ │ +--------------+ +--------------+ +---------------+ │ +---------------------------------------------------------------------------+ │ │ │ │ +------------v--------------+ +------------v-----------+ │Common biz services │ │Infrastructure services │ │ +-------------+ │ │ +--------------------+ │ │ │ service │ │ │ │ UAA service │ │ │ +-------------+ │ │ +--------------------+ │ │ │ │ │ +--------------------+ │ │ │ │ │ │ Service │ │ │ +------v----------------+ │ │ │ Registration │ │ │ │ repository(dsl) │ │ │ │ and Discovery │ │ │ +------+----------------+ │ │ │ │ │ │ │ │ │ +--------------------+ │ │ │ │ │ +--------------------+ │ │ +------v----------------+ │ │ │ Common Components │ │ │ │ model(entity) │ │ │ │ │ │ │ +-----------------------+ │ │ │ │ │ │ │ │ +--------------------+ │ +---------------------------+ +------------------------+ ``` ## API的认证和授权 所有服务(包括认证授权服务)都被放置在API网关(ZUUL)的后面,API网关聚合所有微服务,统一对外暴露接口。 API网关的认证和授权采用OAuth2.0协议。浏览器层面的请求走授权码认证流程,客户端、APP层面的请求走客户端授权认证或者密码授权认证流程。 ### 浏览器层面的认证和授权 #### 单点登录 1.Login画面 ![single-sing-on-form](x-readme-imgs/single-sing-on-form.png) 2.授权画面 ![single-sing-on-confirm](x-readme-imgs/single-sing-on-confirm.png) #### API网关的安全配置 ```yml security: oauth2: sso: loginPath: /login client: accessTokenUri: http://uaa-service/uaa/oauth/token userAuthorizationUri: /uaa/oauth/authorize clientId: acme clientSecret: acmesecret ``` 知识点: `security.oauth2.client.userAuthorizationUri`是通过浏览器发起的请求。请求路径为:`Browser --> Zuul --> UAA` `security.oauth2.client.accessTokenUri`是通过`RestTemplate`发起的请求。请求路径为:`RestTemplate --> UAA` `userAuthorizationUri`由于没有经过`Zuul`代理,所以没有被负载均衡。 扩展点: 给`RestTemplate`增加负载均衡能力 ```java @Bean UserInfoRestTemplateCustomizer userInfoRestTemplateCustomizer(SpringClientFactory springClientFactory) { return template -> { AccessTokenProviderChain accessTokenProviderChain = Stream .of(new AuthorizationCodeAccessTokenProvider(), new ImplicitAccessTokenProvider(), new ResourceOwnerPasswordAccessTokenProvider(), new ClientCredentialsAccessTokenProvider()) .peek(tp -> tp.setRequestFactory(new RibbonClientHttpRequestFactory(springClientFactory))) .collect(Collectors.collectingAndThen(Collectors.toList(), AccessTokenProviderChain::new)); template.setAccessTokenProvider(accessTokenProviderChain); }; } ``` 扩展后基于Form登录的授权码模式认证流程如下: ``` Browser Zuul UAA │ /dummy │ │ ├────────────────────────────────>│ │ │ Location:http://ZUUL/login │ │ │<┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┤ │ │ /login │ │ ├────────────────────────────────>│ │ │ Location:/uaa/oauth/authorize │ │ │<┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┤ │ │ /uaa/oauth/authorize │ │ ├────────────────────────────────>│ │ │ │ /uaa/oauth/authorize │ │ ├┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄>│ │ │ ├──┐ │ │ │ │ Not authorize │ │ │<─┘ │ │ Location:http://ZUUL/uaa/login │ │ │<┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┤ │ │ │ │ Location:http://ZUUL/uaa/login │ │ │<┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┤ │ │ /uaa/login │ │ ├────────────────────────────────>│ │ │ │ /uaa/login │ │ ├┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄>│ │ │ LOGIN FORM │ │ │<┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┤ │ LOGIN FORM │ │ │<────────────────────────────────┤ │ ``` #### [`Zuul`] Clear `sensitiveHeaders` lists for `AuthorizationServer` route By default it filters: - `Cookie` - `Set-Cookie` - `Authorization` But we need that `AuthorizationServer` could create cookies so we must clear list ```yml zuul: routes: uaa-service: sensitiveHeaders: path: /uaa/** stripPrefix: false ``` #### [`Zuul`] Disable `XSRF` at gateway level for `AuthorizationServer` `AuthorizationServer` has it own `XSRF` protection so we must disable at `Zuul` level ```java private RequestMatcher csrfRequestMatcher() { return new RequestMatcher() { // Always allow the HTTP GET method private final Pattern allowedMethods = Pattern.compile("^(GET│HEAD│OPTIONS│TRACE)$"); // Disable CSFR protection on the following urls: private final AntPathRequestMatcher[] requestMatchers = { new AntPathRequestMatcher("/uaa/**") }; @Override public boolean matches(HttpServletRequest request) { if (allowedMethods.matcher(request.getMethod()).matches()) { return false; } for (AntPathRequestMatcher matcher : requestMatchers) { if (matcher.matches(request)) { return false; } } return true; } }; } ``` #### [`Zuul`] Authorize request to `AuthorizationServer` ```java http.authorizeRequests().antMatchers("/uaa/**", "/login").permitAll() ``` **ATTENTION** do not use `"/uaa/**"` authorize only necessary API (I was to lazy) #### [`UAA`] Deploy `AuthorizationServer` on isolated `context-path` `Zuul` and `AuthorizationServer` have to manage their own session! So both have to write two `JSESSIONID` cookies. You must isolate `AuthorizationServer` on other context-path `server.context-path = /uaa` to avoid any cookies collision. **ALTERNATIVE** we can check if `server.session.cookie.path` or `server.session.cookie.name` is not sufficient, I did not test it. ### [`UAA`] Enable `server.use-forward-headers` Does not work without. I will not explain why, please look about `X-Forwarded-*` headers for more information. ### 客户端、APP层面的认证和授权 #### OAuth 2.0 密码授权模式 Get token: ``` curl -v -u acme:acmesecret \ http://localhost:8765/uaa/oauth/token \ -d grant_type=password \ -d username=user \ -d password=password \ -d scope=openid ``` Resource access: ``` curl -v -H 'Authorization: Bearer ' \ http://localhost:8765/dummy ``` #### OAuth 2.0 客户端授权模式 Get token: ``` curl -v -u acme:acmesecret \ http://localhost:8765/uaa/oauth/token \ -d grant_type=client_credentials \ -d scope=openid ``` Resource access: ``` curl -v -H 'Authorization: Bearer ' \ http://localhost:8765/dummy ``` ## 开发步骤 ### ApiGateway追加路由规则 微服务要想对外公开必须要经过ApiGateway的路由。 比如`dummy-service`,所有到达ApiGateway的请求,如果uri符合`/dummy/**`规则,都会被路由到`dummy-service`进行处理。 ```yaml zuul: routes: dummy-service: /dummy/** uaa-service: sensitiveHeaders: path: /uaa/** stripPrefix: false add-proxy-headers: true ``` ### 微服务开发 #### 设定服务发现客户端注解`@EnableDiscoveryClient`和资源服务器`@EnableResourceServer`注解 ```java @SpringBootApplication @EnableResourceServer @EnableDiscoveryClient @EnableGlobalMethodSecurity(prePostEnabled = true) @EnableConfigurationProperties public class DummyServiceApplication extends WebSecurityConfigurerAdapter { public static void main(String[] args) { SpringApplication.run(DummyServiceApplication.class, args); } @Override public void configure(HttpSecurity http) throws Exception { http.authorizeRequests().antMatchers( "/**/v2/api-docs").permitAll(); } } ``` #### API返回结果规定 API的返回结果统一封装在`ApiResource`中,`ApiResource`提供了一个构建器,可以下面这样使用: ```java return new ApiResource.Builder().coce("OK").result(cityResourceList).build(); ``` 所有API的返回结果统一为如下格式: ```json { "code": "string", "message": "string", "result": {} } ``` * `code`和`message`是控制字段,可以存放状态值,错误码,错误信息等。 * `result`中存放的是具体的返回结果,可以是String,List,Map,Object等。如果是Object类型的对象,必须定义在 `api-interface`模块下面,作为后续开发SDK(限java)时的接口定义JAR独立使用。原则上接口DTO和Entity必须独立 定义,Entity反映的的是数据库定义,有些字段可能不方便对外公开。只把必须的字段公开给用户,可以保证接口的简洁性, 防止冗余字段对用户造成困扰。 #### API Swagger2注解定义 参照下面的代码配置接口文档注解 * `@Api` * `@ApiOperation` * `@ApiParam` * `@ApiModel` * `@ApiModelProperty` ```java @Api(value = "/", tags = "测试接口") @RestController @RequestMapping("/") public class DummyController { ... @ApiOperation(value = "获取城市信息") @GetMapping("echo-city") public ApiResource> echoCity( @ApiParam(value = "城市代码") @RequestParam(required = false) String cityCode, @ApiParam(value = "分页检索偏移量", defaultValue = "0") @RequestParam(required = false) Long offset, @ApiParam(value = "分页检索件数", defaultValue = "5") @RequestParam(required = false) Long limit) { QCity city = QCity.city; BooleanExpression whereExpression = city.id.eq(city.id); if (cityCode != null) { whereExpression = whereExpression.and(city.cityCode.eq(cityCode)); } List cityResource = queryFactory .select(Projections.bean(CityDto.class, city.id, city.cityCode, city.cityName, city.cityDes)) // 装配结果集 .from(city) .where(whereExpression) .orderBy(city.cityCode.asc()) .offset(offset == null ? 0 : offset) .limit(limit == null ? 5 : limit) .fetch(); ApiResource apiResource = new ApiResource.Builder().result(cityResource).build(); return apiResource; } ... } ``` ```java @ApiModel(value = "CityDto", description = "城市接口类") public class CityDto implements Serializable { private static final long serialVersionUID = 1L; @ApiModelProperty("唯一ID") private int id; @ApiModelProperty("城市编码") private String cityCode; @ApiModelProperty("城市名称") private String cityName; @ApiModelProperty("城市描述") private String cityDes; ...... } ``` #### 领域层调用 * `插入`操作时使用`RepositoryDsl`,如果Entity定义了级联操作,可以一次性级联插入。 * `更新`,`删除`和`检索`时使用`queryFactory`,`QueryDSL`可以简化检索条件的构建。 * 如果时通用业务逻辑可以封装在`api-domian`模块的`service`包里面。 ```java @Api(value = "/", tags = "测试接口") @RestController @RequestMapping("/") public class DummyController { @Autowired CityRepositoryDsl cityRepositoryDsl; @Autowired JPAQueryFactory queryFactory; @ApiOperation(value = "创建城市", notes = "如果数据库中存在区号为022的城市则更新城市描述,否则新建城市。") @GetMapping("add-city") @Transactional public ApiResource> addCity() { QCity city = QCity.city; // 件数检查 long count = queryFactory.from(city).where(city.cityCode.eq("022")).fetchCount(); if (count == 0) { // 执行插入操作 City newCity = new City(); newCity.setCityCode("022"); newCity.setCityName("天津"); newCity.setCityDes("直辖市"); cityRepositoryDsl.save(newCity); //1 // 执行更新操作 } else { queryFactory .update(city) .set(city.cityDes, "直辖市" + UUID.randomUUID().toString()) .where(city.cityCode.eq("022")) .execute(); //2 } // 执行再检索 ApiResource> apiResource = echoCity("022", null, null); return apiResource; } @ApiOperation(value = "获取城市信息") @GetMapping("echo-city") public ApiResource> echoCity( @ApiParam(value = "城市代码") @RequestParam(required = false) String cityCode, @ApiParam(value = "分页检索偏移量", defaultValue = "0") @RequestParam(required = false) Long offset, @ApiParam(value = "分页检索件数", defaultValue = "5") @RequestParam(required = false) Long limit) { QCity city = QCity.city; BooleanExpression whereExpression = city.id.eq(city.id); if (cityCode != null) { whereExpression = whereExpression.and(city.cityCode.eq(cityCode)); } List cityResource = queryFactory .select(Projections.bean(CityDto.class, city.id, city.cityCode, city.cityName, city.cityDes)) // 装配结果集 .from(city) .where(whereExpression) .orderBy(city.cityCode.asc()) .offset(offset == null ? 0 : offset) .limit(limit == null ? 5 : limit) .fetch(); //3 ApiResource apiResource = new ApiResource.Builder().result(cityResource).build(); return apiResource; } @ApiOperation(value = "查询订单", notes = "一对多动态条件查询,并且条件限定出现在多的一端的情况的示例。" + "[订单编号]是订单表的字段,[商品名称]时订单条目表的字段,订单和订单条目时一对多的关系。") @GetMapping("search-order") public ApiResource> searchOrder( @ApiParam(value = "订单编号") @RequestParam(required = false) String orderNo, @ApiParam(value = "商品名称") @RequestParam(required = false) String productName, @ApiParam(value = "分页检索偏移量", defaultValue = "0") @RequestParam(required = false) Long offset, @ApiParam(value = "分页检索件数", defaultValue = "5") @RequestParam(required = false) Long limit) { QOrder order = QOrder.order; QOrderItem orderItem = QOrderItem.orderItem; // 构建动态Where条件 BooleanExpression whereExpression = order.commonId.eq(order.commonId); if (orderNo != null) { whereExpression = whereExpression.and(order.orderNo.eq(orderNo)); } if (productName != null) { whereExpression = whereExpression.and(orderItem.productName.eq(productName)); } // 执行检索 List orders = queryFactory .selectFrom(order) // 注意:只有用selectFrom才会自动将结果集组装Order实体 .leftJoin(order.orderItems, orderItem) // 注意:连接的写法,第二个参数很重要 .where(whereExpression) // whereExpression==null时自动不拼接where语句 .offset(offset == null ? 0 : offset) .limit(limit == null ? 5 : limit) .distinct() .fetch(); //4 return new ApiResource.Builder().result(orders).build(); } } ``` 标注说明: 1. DB插入操作 2. DB更新操作 3. 单表查询 4. 多表结合查询 #### 领域层定义 ##### Model定义 使用`javax.persistence.*`包下的注解定义Model。 ```java @Entity @Table(name = "city") public class City implements Serializable { private static final long serialVersionUID = 1L; @Id @GeneratedValue(strategy = GenerationType.AUTO) private int id; private String cityCode; private String cityName; private String cityDes; ...... } ``` 一对多关系定义示例(一张订单可以有多个订单条目) ```java @Data @Entity @Table(name = "t_order") public class Order extends BaseEntity { /** * 订单单号 */ private String orderNo; /** * 店铺 */ @JsonIgnore //1 @ManyToOne @JoinColumn(name = "store_id") private Store store; /** * 订单条目 */ @OneToMany(mappedBy = "order", cascade = CascadeType.ALL, orphanRemoval = true) //2 private List orderItems = new ArrayList<>(); ... ... } ``` 1. @JsonIgnore --> 转换为JSON格式是忽略该字段,防止死循环。 2. 多的一端在一的一端的实体里以List的形式存在,mappedBy = "order"是指把级联关系的管理委托给Order实体来管理。 ```java @Data @Entity @Table(name = "t_order_item") public class OrderItem extends BaseEntity { /** * 订单 */ @JsonIgnore //1 @ManyToOne @JoinColumn(name = "order_id") //2 private Order order; /** * 商品编号 */ private Long productId; /** * 商品名称 */ private String productName; ... ... } ``` 1. @JsonIgnore --> 转换为JSON格式是忽略该字段,防止死循环。 2. @JoinColumn(name = "order_id")是指在当前实体对应的表中增加一个名字叫order_id的新字段,这个字段映射的是Order实体。 一对一关系示例(店铺和店铺详情是一对一关系) ```java @Data @Entity @Table(name = "t_store") public class Store extends BaseEntity { public Store() { } public Store(String storeNo, String storeName) { this.storeNo = storeNo; this.storeName = storeName; } public void setStoreDetails(StoreDetails storeDetails) { this.storeDetails = storeDetails; this.storeDetails.setStore(this); } /** * 店铺编号 */ private String storeNo; /** * 店铺名称 */ private String storeName; /** * 店铺详情 */ @OneToOne(mappedBy = "store", cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.LAZY) private StoreDetails storeDetails; } ``` ```java @Data @Entity @Table(name = "t_store_details") public class StoreDetails extends BaseEntity { public StoreDetails() { } public StoreDetails(String storeRegistrant, String storeRegistrantPhone) { this.storeRegistrant = storeRegistrant; this.storeRegistrantPhone = storeRegistrantPhone; } /** * 店铺 */ @JsonIgnore @OneToOne(fetch = FetchType.LAZY) @JoinColumn(name = "store_id") private Store store; /** * 店铺注册人 */ private String storeRegistrant; /** * 店铺注册人电话 */ private String storeRegistrantPhone; } ``` 关系定义模板代码汇总: ```java 一对一: 店铺表 Store(主): /** * 店铺详情 */ @OneToOne(mappedBy = "store", cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.LAZY) private StoreDetails storeDetails; // 约定:从表名首字母小写 店铺详情表 StoreDetails(从): /** * 店铺 */ @JsonIgnore @OneToOne(fetch = FetchType.LAZY) @JoinColumn(name = "store_id") private Store store; // 约定:主表名首字母小写 一对多: 订单表 Order(主): /** * 订单条目 */ @OneToMany(mappedBy = "order", cascade = CascadeType.ALL, orphanRemoval = true) private List orderItems = new ArrayList<>(); // 约定:从表名首字母小写+s 订单条目表 OrderItem(从): /** * 订单 */ @JsonIgnore @ManyToOne @JoinColumn(name = "order_id") private Order order; // 约定:主表名首字母小写 多对多: 员工表 Employee(主): /** * 项目 */ @ManyToMany @JoinTable( name="t_employee_project", joinColumns=@JoinColumn(name="employee_id", referencedColumnName="id"), inverseJoinColumns=@JoinColumn(name="project_id", referencedColumnName="id")) private List projects; // 约定:从表名首字母小写+s 项目表 Project(从): /** * 员工 */ @ManyToMany(mappedBy="projects", fetch = FetchType.LAZY) private List employees; // 约定:主表名首字母小写+s ``` ##### Repository定义 继承下面两个接口 * `org.springframework.data.jpa.repository.JpaRepository` * `org.springframework.data.querydsl.QueryDslPredicateExecutor` ```java public interface CityRepositoryDsl extends JpaRepository,QueryDslPredicateExecutor { } ``` ## API 文档生成 ### 在线调试文档(限开发环境) 例:dummy-server 配置`application.yml` ```yaml # Define the swagger show swagger: enabled: true ``` 访问URL: `http://localhost:8765/dummy/swagger-ui.html` ![swagger-ui](x-readme-imgs/swagger-ui.png) ![swagger-ui2](x-readme-imgs/swagger-ui2.png) 获取接口信息JSON文档 访问URL: `http://localhost:8765/dummy/v2/api-docs` 结果: ```json { "swagger": "2.0", "info": { "version": "1.0", "title": "Dummy-service RESTful APIs" }, "host": "localhost:8765", "basePath": "/dummy/", "tags": [{ "name": "测试接口", "description": "Dummy Controller" }], "paths": { "/": { "get": { "tags": ["测试接口"], "summary": "helloWorld", "operationId": "helloWorldUsingGET", "consumes": ["application/json"], "produces": ["*/*"], "responses": { "200": { "description": "OK", "schema": { "type": "string" } }, "401": { "description": "Unauthorized" }, "403": { "description": "Forbidden" }, "404": { "description": "Not Found" } } } }, "/add-city": { "get": { "tags": ["测试接口"], "summary": "创建城市", "description": "如果数据库中存在区号为022的城市则更新城市描述,否则新建城市。", "operationId": "addCityUsingGET", "consumes": ["application/json"], "produces": ["*/*"], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/ApiResource«List«CityDto»»" } }, "401": { "description": "Unauthorized" }, "403": { "description": "Forbidden" }, "404": { "description": "Not Found" } } } }, "/echo-city": { "get": { "tags": ["测试接口"], "summary": "获取城市信息", "operationId": "echoCityUsingGET", "consumes": ["application/json"], "produces": ["*/*"], "parameters": [{ "name": "cityCode", "in": "query", "description": "城市代码", "required": false, "type": "string" }, { "name": "offset", "in": "query", "description": "分页检索偏移量", "required": false, "type": "integer", "default": 0, "format": "int64" }, { "name": "limit", "in": "query", "description": "分页检索件数", "required": false, "type": "integer", "default": 5, "format": "int64" }], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/ApiResource«List«CityDto»»" } }, "401": { "description": "Unauthorized" }, "403": { "description": "Forbidden" }, "404": { "description": "Not Found" } } } }, "/init-data": { "get": { "tags": ["测试接口"], "summary": "初始化城市数据", "description": "生成4个直辖市和随机8个行政省的Dummy数据。", "operationId": "initDummyDataUsingGET", "consumes": ["application/json"], "produces": ["*/*"], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/ApiResource«List«CityDto»»" } }, "401": { "description": "Unauthorized" }, "403": { "description": "Forbidden" }, "404": { "description": "Not Found" } } } }, "/secret": { "get": { "tags": ["测试接口"], "summary": "helloSecret", "operationId": "helloSecretUsingGET", "consumes": ["application/json"], "produces": ["*/*"], "responses": { "200": { "description": "OK", "schema": { "type": "string" } }, "401": { "description": "Unauthorized" }, "403": { "description": "Forbidden" }, "404": { "description": "Not Found" } } } } }, "definitions": { "ApiResource«List«CityDto»»": { "type": "object", "properties": { "code": { "type": "string", "description": "返回代码" }, "message": { "type": "string", "description": "返回消息" }, "result": { "type": "object", "description": "返回结果内容" } }, "description": "Api资源统一封装类" }, "CityDto": { "type": "object", "properties": { "cityCode": { "type": "string", "description": "城市编码" }, "cityDes": { "type": "string", "description": "城市描述" }, "cityName": { "type": "string", "description": "城市名称" }, "id": { "type": "integer", "format": "int32", "description": "唯一ID" } }, "description": "城市接口类" } } } ``` ### 离线文档生成和导出 1.执行maven插件生成asciidoc格式的文档 ![convertSwagger2markup](x-readme-imgs/convertSwagger2markup.png) 2.查看中间文档 ![asciidoc](x-readme-imgs/asciidoc.png) 3.通过asciidocFX编辑器打开中间文档,可以把目标文档导出为HTML,PDF等格式 ![asciidocFX](x-readme-imgs/asciidocFX.png)