- 参考
- 开发环境
- 测试表-t_rbt_test
- 组件引用说明
- 项目源代码
- 创建父模块-test-invoice-cloud
- 创建子模块-test-invoice-eureka
- 创建子模块-test-invoice-common
- 创建子模块-test-invoice-contract
- 创建子模块-test-invoice-service
- 创建子模块-test-invoice-web
- 项目开发过程中遇到的问题及解决
- 项目总类图
- 项目总结构图
- 扩展-负载均衡
- 项目调用关系图
参考
开发环境
名称 | 版本 |
---|---|
操作系统 | Windows 10 X64 |
JDK | JDK1.8(jdk-8u151-windows-x64) |
IntelliJ IDEA | IntelliJ IDEA 2018.3 |
Maven | Maven 3.6.0 |
测试表-t_rbt_test
位于本地 MySql
数据库 luoma_test
下
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
-- ----------------------------
-- Table structure for t_rbt_test
-- ----------------------------
DROP TABLE IF EXISTS `t_rbt_test`;
CREATE TABLE `t_rbt_test` (
`id` varchar(100) CHARACTER SET utf8 COLLATE utf8_unicode_ci NOT NULL COMMENT '主键',
`name` varchar(50) CHARACTER SET utf8 COLLATE utf8_unicode_ci NULL DEFAULT NULL COMMENT '名称',
`version` varchar(20) CHARACTER SET utf8 COLLATE utf8_unicode_ci NULL DEFAULT NULL COMMENT '版本号',
`create_by` varchar(50) CHARACTER SET utf8 COLLATE utf8_unicode_ci NULL DEFAULT NULL COMMENT '创建人',
`create_time` timestamp(0) NULL DEFAULT NULL COMMENT '创建时间',
`update_by` varchar(50) CHARACTER SET utf8 COLLATE utf8_unicode_ci NULL DEFAULT NULL COMMENT '修改人',
`update_time` timestamp(0) NULL DEFAULT NULL COMMENT '修改时间',
`is_delete` int(1) NULL DEFAULT NULL COMMENT '是否删除',
`biz_time` timestamp(0) NULL DEFAULT NULL COMMENT '系统时间戳',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_unicode_ci ROW_FORMAT = Dynamic;
SET FOREIGN_KEY_CHECKS = 1;
组件引用说明
由于篇幅有限,加上侧重点的问题。本文中引用的组件请参考
项目源代码
见附件
创建父模块-test-invoice-cloud
文件
-New
-项目
左侧面板选择 Maven
,不勾选 Create from archetype
选项,如下图,点击 下一个
即可。
依次补全下面的信息,点击 下一个
节点 | 描述 |
---|---|
groupId | 代表组织和整个项目的唯一标志,是项目组织唯一的知标识符,实际对应 JAVA 的包的结构 |
artifactId | 项目的唯一的标识符,实际对应项目的名称 |
version | 用于说明目前项目的版本 |
输入项目名称 test-invoice-cloud
,我们主要是在这个项目下创建我们的子模块
这样我们就创建好了一个普通项目,因为该项目是作为一个解决方案存在的,可以直接删除 src
文件夹
创建好的项目如下
项目文件名称及含义
目录/文件 | 含义 |
---|---|
.idea | 创建项目的时候自动创建一个 .idea 的项目配置目录来保存项目的配置信息。这是默认选项。 子目录包含一系列XML文件,包括:compiler.xml、encodings.xml、modules.xml等。这些文件记录工程本身的核心信息,包括:模块组件的名称和位置、编译器设置等,可以存放到VCS。一个例外是workspace.xml,该文件存储个人设置(例如窗口位置)以及其它附属于开发环境的信息,不应该存放到VCS |
pom.xml | Maven 项目配置文件。POM( Project Object Model,项目对象模型 ) 是 Maven 工程的基本工作单元,是一个XML文件,包含了项目的基本信息,用于描述项目如何构建,声明项目依赖,等等。执行任务或目标时,Maven 会在当前目录中查找 POM。它读取 POM,获取所需的配置信息,然后执行目标。 |
.iml | 模块是工程中一个可以独立编译、运行、调试、测试的单元。模块的配置信息默认存放在其内容根目录(Content root folder)下的 .iml 文件中,该文件一般存放到VCS。 |
设置 Maven 引用地址
注意,这里的设置是必须的。相关组件的下载就需要这里的配置。
pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<!--父级:Spring Boot-->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<!--<version>2.0.1.RELEASE</version>-->
<!--<version>2.0.3.RELEASE</version>--><!--解决循环引用对象问题mybatis https://github.com/heikehuan/springboot-multiple-dataSources/issues/2-->
<version>2.0.9.RELEASE</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<groupId>com-test-invoice</groupId>
<artifactId>test-invoice-cloud</artifactId>
<version>1.0-SNAPSHOT</version>
<name>test-invoice-cloud</name>
<packaging>pom</packaging>
<modules>
<module>test-invoice-common</module>
<module>test-invoice-contract</module>
<module>test-invoice-service</module>
<module>test-invoice-web</module>
<module>test-invoice-eureka</module>
</modules>
<properties>
<springCloud.version>Finchley.RELEASE</springCloud.version>
<fastjson.version>1.2.38</fastjson.version>
<swagger.version>2.7.0</swagger.version>
<lombok.version>1.18.6</lombok.version>
<HikariCP.version>3.3.1</HikariCP.version>
<mybatis-plus-spring-boot-starter.version>3.1.0</mybatis-plus-spring-boot-starter.version>
<swagger-ui.version>2.7.0</swagger-ui.version>
</properties>
<!--Spring Cloud 版本序列配置-->
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${springCloud.version}</version>
<type>pom</type>
<scope>import</scope>
<exclusions>
<exclusion>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<!--Spring Boot 执行器组件-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<!--Spring Cloud 基础-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter</artifactId>
</dependency>
<!--Spring Cloud 服务注册组件-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<!--此处的依赖是SpringBoot2.0以后专用的,如果您使用的SpringBoot版本低于2.0请使用spring-cloud-starter-eureka-server-->
<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>
<!--Spring Boot 测试组件-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!--Spring Boot Web组件-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<!-- 从依赖信息里移除 Tomcat配置 -->
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- 参数校验信息 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<!-- 添加 Undertow依赖 start-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-undertow</artifactId>
</dependency>
<!-- 添加 Undertow依赖 end-->
<!-- spring aop start -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<!-- spring aop end -->
<!--lombok start-->
<!-- https://mvnrepository.com/artifact/org.projectlombok/lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
</dependency>
<!--lombok end-->
<!--fastJson start-->
<!--Fastjson是一个Java语言编写的高性能功能完善的JSON库。它采用一种“假定有序快速匹配”的算法,把JSON Parse的性能提升到极致,是目前Java语言中最快的JSON库-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>${fastjson.version}</version>
</dependency>
<!--fastJson end-->
<!--swagger start-->
<!--springfox是通过注解的形式自动生成API文档,利用它,可以很方便的书写restful API,swagger主要用于展示springfox生成的API文档-->
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>${swagger.version}</version>
</dependency>
<!--swagger end-->
</dependencies>
<!--注意:这里必须要添加,否则各种依赖有问题-->
<repositories>
<repository>
<id>spring-milestones</id>
<name>Spring Milestones</name>
<url>https://repo.spring.io/libs-milestone</url>
<snapshots>
<enabled>false</enabled>
</snapshots>
</repository>
</repositories>
<build>
<plugins>
<plugin>
<!-- 指定maven编译的jdk版本,如果不指定,maven3默认用jdk 1.5 maven2默认用jdk1.3 -->
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.1</version>
<configuration>
<!-- 一般而言,target与source是保持一致的,但是,有时候为了让程序能在其他版本的jdk中运行(对于低版本目标jdk,源代码中不能使用低版本jdk中不支持的语法),会存在target不同于source的情况 -->
<source>1.8</source> <!-- 源代码使用的JDK版本 -->
<target>1.8</target> <!-- 需要生成的目标class文件的编译版本 -->
<encoding>UTF-8</encoding><!-- 字符集编码 -->
<!--<skipTests>true</skipTests><!– 跳过测试 –>
<verbose>true</verbose>
<showWarnings>true</showWarnings>
<fork>true</fork><!– 要使compilerVersion标签生效,还需要将fork设为true,用于明确表示编译版本配置的可用 –>
<executable><!– path-to-javac –></executable><!– 使用指定的javac命令,例如:<executable>${JAVA_1_4_HOME}/bin/javac</executable> –>
<compilerVersion>1.3</compilerVersion><!– 指定插件将使用的编译器的版本 –>
<meminitial>128m</meminitial><!– 编译器使用的初始内存 –>
<maxmem>512m</maxmem><!– 编译器使用的最大内存 –>
<compilerArgument>-verbose -bootclasspath ${java.home}\lib\rt.jar</compilerArgument>--><!-- 这个选项用来传递编译器自身不包含但是却支持的参数选项 -->
</configuration>
</plugin>
<plugin>
<groupId>org.sonarsource.scanner.maven</groupId>
<artifactId>sonar-maven-plugin</artifactId>
<version>3.5.0.1254</version>
</plugin>
</plugins>
</build>
</project>
创建子模块-test-invoice-eureka
test-invoice-cloud
上右键,新建模块
左侧面板选择 Maven
,不勾选 Create from archetype
选项,如下图,点击 下一个
即可。
artifactId test-invoice-eureka
模块名称 test-invoice-eureka
创建类和文件
模块简介
这个模块对应的是 Eureka Server
Eureka Server 提供服务注册服务,各个节点启动后,会在 Eureka Server 中进行注册,这样 EurekaServer 中的服务注册表中将会存储所有可用服务节点的信息,服务节点的信息可以在界面中直观的看到。
这里只是作为项目中的一个模块,详细的了解可参考下面的内容
pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>test-invoice-cloud</artifactId>
<groupId>com-test-invoice</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>test-invoice-eureka</artifactId>
<name>test-invoice-eureka</name>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-netflix-eureka-server</artifactId>
<version>2.0.0.M7</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<finalName>${project.artifactId}</finalName>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
<resources>
<resource>
<directory>src/main/resources</directory>
<includes>
<include>**/*</include>
</includes>
</resource>
</resources>
</build>
</project>
yml
application.yml
spring:
# 节点1的标签
profiles:
active: dev
activiti:
check-process-definitions: false #校验流程文件,默认校验resources下的processes文件夹里的流程文件
# 服务名保持一致
application:
name: eureka-ha
application-dev.yml
server:
port: 8772
# 配置Eureka Server 信息
eureka:
client:
# 表示是否将自己注册到Eureka Server,不进行注册(当服务注册中心是单点而非高可用时的配置方式)
registerWithEureka: false
# 表示是否从Eureka Server获取注册信息,不获取注册信息(当服务注册中心是单点而非高可用时的配置方式)
fetchRegistry: false
#设置与Eureka Server交互的地址,查询服务和注册服务都需要依赖这个地址。默认是http://localhost:8761/eureka ;多个地址可使用 , 分隔
service-url:
defaultZone: http://localhost:${server.port}/eureka/
# 自定义实例编号
instance:
instance-id: ${spring.application.name}:${server.port}
# 配置使用主机名注册服务
#hostname: peer1
# 优先使用IP地址方式进行注册服务
prefer-ip-address: true
# 配置使用指定IP
ip-address: 127.0.0.1
入口类-EurekaHaApplication
package com.test.eurekaha;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration;
import org.springframework.boot.autoconfigure.data.redis.RedisRepositoriesAutoConfiguration;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
import org.springframework.boot.autoconfigure.jdbc.DataSourceTransactionManagerAutoConfiguration;
import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;
/**
* EurekaHaApplication
*
* @author:
* @version:
* @date: 2019-08-13 15:26
*/
//排除掉数据库配置项目
@SpringBootApplication( exclude = {
DataSourceAutoConfiguration.class,
DataSourceTransactionManagerAutoConfiguration.class,
RedisAutoConfiguration.class,
RedisRepositoriesAutoConfiguration.class
})
@EnableEurekaServer
public class EurekaHaApplication {
public static void main(String[] args) {
SpringApplication.run(EurekaHaApplication.class, args);
}
}
测试
创建成功后,启动项目,访问 http://localhost:8772/
创建子模块-test-invoice-common
test-invoice-cloud
上右键,新建模块
左侧面板选择 Maven
,不勾选 Create from archetype
选项,如下图,点击 下一个
即可。
artifactId test-invoice-common
模块名称 test-invoice-common
模块简介
test-invoice-common
这个模块主要是创建一些公共类,实体提供给其它模块使用
新建测试类-TRbtTestData
新建包 com.test.invoice
再新建类 TRbtTestData
属性私有,行为公有
所以这里我们把属性设置为 private
,通过 get
和 set
来获取设置值
package com.test.invoice.data;
import com.alibaba.fastjson.annotation.JSONField;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.experimental.Accessors;
@Data
@Accessors(chain = true)
public class TRbtTestData {
@ApiModelProperty("名称")
@JSONField(name = "name")
private String name;
@ApiModelProperty("版本号")
@JSONField(name = "version")
private String version;
}
这个类引入了很多包,需要在 pom.xml
引入
父模块 pom.xml 引入包子模块 pom.xml 无法解析问题
问题描述
TRbtTestData
类需要引入一些包,我们再最外层 test-invoice-cloud
的 pom.xml
中引入包
这个 pom.xml
中使用到了自定义变量,可以参考
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<!--父级:Spring Boot-->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<!--<version>2.0.1.RELEASE</version>-->
<!--<version>2.0.3.RELEASE</version>--><!--解决循环引用对象问题mybatis https://github.com/heikehuan/springboot-multiple-dataSources/issues/2-->
<version>2.0.9.RELEASE</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<groupId>com-test-invoice</groupId>
<artifactId>test-invoice-cloud</artifactId>
<packaging>pom</packaging>
<version>1.0-SNAPSHOT</version>
<name>test-invoice-cloud</name>
<modules>
<module>test-invoice-common</module>
</modules>
<properties>
<fastjson.version>1.2.38</fastjson.version>
<swagger.version>2.7.0</swagger.version>
<lombok.version>1.18.6</lombok.version>
</properties>
<dependencies>
<!--Spring Boot 执行器组件-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<!--Spring Cloud 基础-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter</artifactId>
</dependency>
<!--Spring Cloud 服务注册组件-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<!--此处的依赖是SpringBoot2.0以后专用的,如果您使用的SpringBoot版本低于2.0请使用spring-cloud-starter-eureka-server-->
<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>
<!--Spring Boot 测试组件-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!--Spring Boot Web组件-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<!-- 从依赖信息里移除 Tomcat配置 -->
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- spring aop start -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<!-- spring aop end -->
<!--fastJson start-->
<!--Fastjson是一个Java语言编写的高性能功能完善的JSON库。它采用一种“假定有序快速匹配”的算法,把JSON Parse的性能提升到极致,是目前Java语言中最快的JSON库-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>${fastjson.version}</version>
</dependency>
<!--fastJson end-->
<!--swagger start-->
<!--springfox是通过注解的形式自动生成API文档,利用它,可以很方便的书写restful API,swagger主要用于展示springfox生成的API文档-->
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>${swagger.version}</version>
</dependency>
<!--swagger end-->
<!--lombok start-->
<!-- https://mvnrepository.com/artifact/org.projectlombok/lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
</dependency>
<!--lombok end-->
</dependencies>
</project>
test-invoice-common
的 pom.xml
中不引入,因为是父子模块,子模块就不用再引用了
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>test-invoice-cloud</artifactId>
<groupId>com-test-invoice</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>test-invoice-common</artifactId>
<name>test-invoice-common</name>
</project>
构建,报下面的错误
程序包 xx 不存在
问题解决
原因是子模块没有引入对应包内容
打开右边的 Maven
选项卡,发现 test-invoice-common
没有 Dependencies
内容
在 test-invoice-common
的 pom.xml
中加入下面的内容
<properties>
<mybatisplus.version>3.1.0</mybatisplus.version>
</properties>
<dependencies>
<!--MyBatis-Plus-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus</artifactId>
<version>${mybatisplus.version}</version>
</dependency>
</dependencies>
完整 pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>test-invoice-cloud</artifactId>
<groupId>com-test-invoice</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>test-invoice-common</artifactId>
<name>test-invoice-common</name>
<properties>
<mybatisplus.version>3.1.0</mybatisplus.version>
</properties>
<dependencies>
<!--MyBatis-Plus-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus</artifactId>
<version>${mybatisplus.version}</version>
</dependency>
</dependencies>
</project>
重新导入
导入成功后重新构建项目,成功
打开右边的 Maven
选项卡,Dependencies
内容出现
问题原因
去掉 在 test-invoice-common
的 pom.xml
中的内容
<properties>
<mybatisplus.version>3.1.0</mybatisplus.version>
</properties>
<dependencies>
<!--MyBatis-Plus-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus</artifactId>
<version>${mybatisplus.version}</version>
</dependency>
</dependencies>
重新构建项目,也成功。
说明这个问题的原因是 IDE 不够智能引起的,有时候会加载不到对应的包引用,需要再重新构建才可以。
创建下图中的类和文件
类图
pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>test-invoice-cloud</artifactId>
<groupId>com-test-invoice</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>test-invoice-common</artifactId>
<name>test-invoice-common</name>
<properties>
<mybatisplus.version>3.1.0</mybatisplus.version>
</properties>
<dependencies>
<!--MyBatis-Plus (简称 MP)是一个 MyBatis 的增强工具,在 MyBatis 的基础上只做增强不做改变-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus</artifactId>
<version>${mybatisplus.version}</version>
</dependency>
<!--Dozer是一个JavaBean映射工具库。它支持简单的属性映射,复杂类型映射,双向映射,隐式显式的映射,以及递归映射-->
<dependency>
<groupId>com.github.dozermapper</groupId>
<artifactId>dozer-core</artifactId>
<version>6.1.0</version>
</dependency>
<!--JUnit 是一个Java编程语言的单元测试框架-->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<scope>test</scope>
</dependency>
<!--bouncy castle(轻量级密码术包)是一种用于 Java 平台的开放源码的轻量级密码术包;它支持大量的密码术算法,并提供JCE 1.2.1的实现-->
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcprov-jdk15on</artifactId>
<version>1.55</version>
</dependency>
<!--密码加密框架-->
<dependency>
<groupId>com.madgag.spongycastle</groupId>
<artifactId>core</artifactId>
<version>1.54.0.0</version>
</dependency>
</dependencies>
</project>
基础实体类-SuperEntity
package com.test.invoice.model.base;
import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.extension.activerecord.Model;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;
import java.io.Serializable;
import java.util.Date;
/**
* 基础实体类
*
* @author:
* @version:
* @date: 2019-08-09 14:26
*/
@EqualsAndHashCode(callSuper = true)
@Data
@Accessors(chain = true)
public class SuperEntity<T extends Model> extends Model<T> {
@TableId(type = IdType.UUID)
@ApiModelProperty(value = "主键")
private String id;
@ApiModelProperty(value="创建时间",hidden = true)
@TableField(value="create_time",fill = FieldFill.INSERT)
@JsonFormat(timezone = "GMT+8",pattern = "yyyy-MM-dd HH:mm:ss")
private Date createTime;
@TableField(value = "update_time", fill = FieldFill.INSERT_UPDATE)
@ApiModelProperty(value = "修改时间", hidden = true)
@JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss")
private Date updateTime;
@ApiModelProperty(value = "创建人", hidden = true)
@TableField(value = "create_by", fill = FieldFill.INSERT)
private String createBy;
@ApiModelProperty(value = "修改人", hidden = true)
@TableField(value = "update_by", fill = FieldFill.INSERT_UPDATE)
private String updateBy;
@ApiModelProperty(value = "是否删除(0:未删除;1:已删除)", hidden = true)
@TableField(value = "is_delete", fill = FieldFill.INSERT)
private Integer isDelete;
@ApiModelProperty(value = "系统时间戳", hidden = true)
@TableField(value = "biz_time", fill = FieldFill.INSERT_UPDATE)
@JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss")
private Date bizTime;
@Override
protected Serializable pkVal() { return this.id; }
}
t_rbt_test 表对应实体类-TRbtTestEntity
package com.test.invoice.model;
import com.baomidou.mybatisplus.annotation.TableName;
import com.test.invoice.model.base.SuperEntity;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.ToString;
import lombok.experimental.Accessors;
/**
* t_rbt_test 表对应实体类-TRbtTestEntity
*
* @author:
* @version:
* @date: 2019-08-09 14:25
*/
@Data
@EqualsAndHashCode(callSuper = true)
@Accessors(chain = true)
@ToString
@TableName("t_rbt_test")
public class TRbtTestEntity extends SuperEntity {
@ApiModelProperty("名称")
private String name;
@ApiModelProperty("版本号")
private String version;
}
TRbtTestData 参数类
package com.test.invoice.data;
import com.alibaba.fastjson.annotation.JSONField;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.experimental.Accessors;
/**
* TRbtTestData 参数类
*
* @author:
* @version:
* @date: 2019-08-09 08:57
*/
@Data
@Accessors(chain = true) //@Accessors 注解用来配置lombok如何产生和显示getters和setters的方法。
public class TRbtTestData {
@ApiModelProperty("id")
@JSONField(name = "id")
private String id;
@ApiModelProperty("名称")
@JSONField(name = "name")
private String name;
@ApiModelProperty("版本号")
@JSONField(name = "version")
private String version;
}
TRbtTestData 分页对象类
package com.test.invoice.data;
import com.alibaba.fastjson.annotation.JSONField;
import com.test.invoice.model.base.SuperEntity;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.experimental.Accessors;
/**
* TRbtTestData 分页对象类
*
* @author:
* @version:
* @date: 2019-08-15 16:49
*/
@Data
@Accessors(chain = true) //@Accessors 注解用来配置lombok如何产生和显示getters和setters的方法。
public class TRbtTestDataParam extends SuperEntity {
@ApiModelProperty("当前页")
@JSONField(name = "page")
private Integer page;
@ApiModelProperty("页面大小")
@JSONField(name = "pageSize")
private Integer pageSize;
@ApiModelProperty("名称")
@JSONField(name = "name")
private String name;
}
响应码枚举对象-ResponseCode
package com.test.invoice.enums;
/**
* 响应码枚举对象。
*
* @author
*/
public enum ResponseCode {
//通用
OK(2000, "Success"),
INSERT_ERROR(-108, "新增失败"),
DELETE_ERROR(-109, "数据删除失败,请刷新重试"),
UPDATE_ERROR(-107, "更新失败,请刷新重试"),
OPERATE_ERROR(5500, "操作失败"),
PARAM_INVALID(1000, "缺失参数或无效"),
THIRD_PARTY_EXCEPTION(3000, "第三方接口异常"),
SYSTEM_EXCEPTION(5000, "系统异常"),
SYSTEM_EXCEPTION_4(8004, "解析回调的xml出现异常"),
SYSTEM_EXCEPTION_8(8008, "JSON转换错误"),
SYSTEM_EXCEPTION_9(8009, "HTTP请求异常"),
SYSTEM_EXCEPTION_10(8010, "用户权限不够"),
SYSTEM_EXCEPTION_11(8011, "获取用户信息失败"),
SYSTEM_EXCEPTION_13(8013, "查询用户出现异常"),
SYSTEM_EXCEPTION_14(8014, "获取用户信息失败,请核实用户id"),
SYSTEM_EXCEPTION_16(8888, "获取用户信息失败,请核实用户id"),
SYSTEM_EXCEPTION_18(8888, "用户权限不够"),
SYSTEM_EXCEPTION_20(8888, "获取用户信息失败"),
SYSTEM_EXCEPTION_21(8888, "调用失败"),
ACCESS_TOKEN_EXCEPTION(9000, "获取token失败"),
AUTH_INVALID(4000, "身份验证失败"),
TOKEN_MISSING(4001, "token缺失"),
TOKEN_INVALID(4002, "token Fail"),
;
private final int value;
private final String description;
ResponseCode(int value, String description) {
this.value = value;
this.description = description;
}
public int value() {
return this.value;
}
public String getDescription() {
return this.description;
}
public static ResponseCode valueOf(int code) {
for (ResponseCode responseCode : values()) {
if (responseCode.value == code) {
return responseCode;
}
}
throw new IllegalArgumentException("No matching constant for [" + code + "]");
}
@Override
public String toString() {
return Integer.toString(this.value);
}
}
服务异常类-ServiceException
package com.test.invoice.excepiton;
import com.test.invoice.enums.ResponseCode;
/**
* 服务异常类
*
* @author:
* @version:
* @date: 2019-08-09 10:28
*/
public class ServiceException extends RuntimeException {
private int code;
public ServiceException(ResponseCode responseCode,Throwable cause){
super(responseCode.getDescription(),cause);
this.code = responseCode.value();
}
public ServiceException(ResponseCode responseCode){
super(responseCode.getDescription());
this.code = responseCode.value();
}
public int getCode() { return code; }
public void setCode(int code) { this.code = code; }
}
响应实体-ResponseVO
VO:value object 值对象/ view object 表现层对象
1.主要对应页面显示(web 页面 / swt 、swing 界面)的数据对象
2.可以和表对应,也可以不对应,根据业务需要。
package com.test.invoice.vo;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.test.invoice.enums.ResponseCode;
import com.test.invoice.excepiton.ServiceException;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.Accessors;
import java.io.Serializable;
/**
* 响应实体
*
* @author:
* @version:
* @date: 2019-08-09 10:40
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
@JsonInclude(JsonInclude.Include.NON_NULL)
@ApiModel("响应实体")
@Accessors(chain = true)
public class ResponseVO<T> implements Serializable {
private static final long serialVersionUID = 1L;
@ApiModelProperty("编码 200:成功,其他异常")
private Integer code;
@ApiModelProperty("消息")
private String msg;
@ApiModelProperty("数据结果集")
private T data;
@ApiModelProperty("服务器时间戳")
private Long timestamp;
public ResponseVO(ResponseCode responseCode){
this.code = responseCode.value();
this.msg = responseCode.getDescription();
this.timestamp = System.currentTimeMillis();
}
public ResponseVO(ResponseCode responseCode,T data){
this.code = responseCode.value();
this.msg = responseCode.getDescription();
this.timestamp = System.currentTimeMillis();
this.data = data;
}
public ResponseVO(ServiceException e){
this.code = e.getCode();
this.msg = e.getMessage();
}
public ResponseVO(int code, String msg){
this.code = code;
this.msg = msg;
this.timestamp = System.currentTimeMillis();
}
}
VO:value object 值对象/ view object 表现层对象
1.主要对应页面显示(web 页面 / swt 、swing 界面)的数据对象
2.可以和表对应,也可以不对应,根据业务需要。
package com.test.invoice.vo;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.test.invoice.enums.ResponseCode;
import com.test.invoice.excepiton.ServiceException;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.Accessors;
import java.io.Serializable;
/**
* 响应实体
*
* @author:
* @version:
* @date: 2019-08-09 10:40
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
@JsonInclude(JsonInclude.Include.NON_NULL)
@ApiModel("响应实体")
@Accessors(chain = true)
public class ResponseVO<T> implements Serializable {
private static final long serialVersionUID = 1L;
@ApiModelProperty("编码 200:成功,其他异常")
private Integer code;
@ApiModelProperty("消息")
private String msg;
@ApiModelProperty("数据结果集")
private T data;
@ApiModelProperty("服务器时间戳")
private Long timestamp;
public ResponseVO(ResponseCode responseCode){
this.code = responseCode.value();
this.msg = responseCode.getDescription();
this.timestamp = System.currentTimeMillis();
}
public ResponseVO(ResponseCode responseCode,T data){
this.code = responseCode.value();
this.msg = responseCode.getDescription();
this.timestamp = System.currentTimeMillis();
this.data = data;
}
public ResponseVO(ServiceException e){
this.code = e.getCode();
this.msg = e.getMessage();
}
public ResponseVO(int code, String msg){
this.code = code;
this.msg = msg;
this.timestamp = System.currentTimeMillis();
}
}
创建子模块-test-invoice-contract
test-invoice-cloud
上右键,新建模块
左侧面板选择 Maven
,不勾选 Create from archetype
选项,如下图,点击 下一个
即可。
artifactId test-invoice-contract
模块名称 test-invoice-contract
创建包重复问题
新建包 com.test.invoice.service
再建一个子包 db
再建一个和 db
包同级的包 test
这时候发现并不同级
解决
新建好包 com.test.invoice.service
之后创建类 Test
,再创建包
新建类-TRbtTestConsumer
package com.test.invoice.service.consumer;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.*;
import com.test.invoice.data.TRbtTestData;
/**
* TRbtTestConsumer
*
* @author:
* @version:
* @date: 2019-08-08 14:45
*/
@FeignClient(value = "service-producer")
public class TRbtTestConsumer {
public void Test()
{
TRbtTestData data = new TRbtTestData();
}
}
pom.xml 引用
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>test-invoice-cloud</artifactId>
<groupId>com-test-invoice</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>test-invoice-contract</artifactId>
<name>test-invoice-contract</name>
<dependencies>
<dependency>
<groupId>com.test.invoice</groupId>
<artifactId>test-invoice-common</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<!--feign start-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<!--spring boot 2.0.3版本解决方案:spring-cloud-starter-feign-->
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<!--feign end-->
</dependencies>
<build>
</build>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
</project>
报错-Dependency ‘xxx’ not found 及解决
Dependency 'com.test.invoice:test-invoice-common:1.0-SNAPSHOT' not found
参考文档
我对比三个 pom.xml 文件
test-invoice-cloud
pom.xml
<modelVersion>4.0.0</modelVersion>
<groupId>com-test-invoice</groupId>
<artifactId>test-invoice-cloud</artifactId>
<version>1.0-SNAPSHOT</version>
<name>test-invoice-cloud</name>
<packaging>pom</packaging>
test-invoice-common
pom.xml
<parent>
<artifactId>test-invoice-cloud</artifactId>
<groupId>com-test-invoice</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>test-invoice-common</artifactId>
<name>test-invoice-common</name>
test-invoice-contract
pom.xml
<parent>
<artifactId>test-invoice-cloud</artifactId>
<groupId>com-test-invoice</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>test-invoice-contract</artifactId>
<name>test-invoice-contract</name>
<dependency>
<groupId>com.test.invoice</groupId>
<artifactId>test-invoice-common</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
问题原因
<dependency>
<groupId>com.test.invoice</groupId>
问题解决,修改为
<dependency>
<groupId>com-test-invoice</groupId>
创建下图中的类和文件
模块简介
主要用于提供接口,还有 Feign-api 接口
类图
pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>test-invoice-cloud</artifactId>
<groupId>com-test-invoice</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>test-invoice-contract</artifactId>
<name>test-invoice-contract</name>
<dependencies>
<dependency>
<groupId>com-test-invoice</groupId>
<artifactId>test-invoice-common</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<!--feign start-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<!--spring boot 2.0.3版本解决方案:spring-cloud-starter-feign-->
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<!--feign end-->
</dependencies>
<build>
</build>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
</project>
接口定义-ITRbtTestService
package com.test.invoice.service.producer;
import com.test.invoice.data.TRbtTestData;
import com.test.invoice.data.TRbtTestDataParam;
import com.test.invoice.vo.ResponseVO;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.*;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
/**
* 接口定义-ITRbtTestService
*/
public interface ITRbtTestService {
ResponseVO<String> Add(@RequestBody TRbtTestData data);
ResponseVO<Boolean> Update(@RequestBody TRbtTestData data);
ResponseVO<Boolean> Del(@RequestParam(value="name",required = true) String name,@RequestParam(value="version",required = true) String version);
ResponseVO<TRbtTestData> Get(@RequestBody TRbtTestData data);
ResponseVO<Page<TRbtTestData>> Query(@RequestBody TRbtTestDataParam param);
}
Feign-api 接口定义——TRbtTestConsumer
package com.test.invoice.service.consumer;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.test.invoice.vo.ResponseVO;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.*;
import com.test.invoice.data.TRbtTestData;
import com.test.invoice.data.TRbtTestDataParam;
/**
* Feign-api 接口定义——TRbtTestConsumer
*
* @author:
* @version:
* @date: 2019-08-08 14:45
*/
@FeignClient(value = "service-producer")
public interface TRbtTestConsumer {
@PostMapping("/Test/Add")
ResponseVO<String> Add(@RequestBody TRbtTestData data);
@PostMapping("Test/Update")
ResponseVO<Boolean> Update(@RequestBody TRbtTestData data);
@RequestMapping(value="Test/Del",method=RequestMethod.GET)
ResponseVO<Boolean> Del(@RequestParam(value="name",required = true) String name,@RequestParam(value="version",required = true) String version);
@PostMapping("/Test/Get")
ResponseVO<TRbtTestData> Get(@RequestBody TRbtTestData data);
@PostMapping("/Test/Query")
ResponseVO<Page<TRbtTestData>> Query(@RequestBody TRbtTestDataParam param);
}
创建子模块-test-invoice-service
test-invoice-cloud
上右键,新建模块
左侧面板选择 Maven
,不勾选 Create from archetype
选项,如下图,点击 下一个
即可。
artifactId test-invoice-service
模块名称 test-invoice-service
创建下图中的类和文件
模块简介
作为一个 Eureka Client 模块
实现 test-invoice-contract
项目中定义的接口
类图
pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>test-invoice-cloud</artifactId>
<groupId>com-test-invoice</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>test-invoice-service</artifactId>
<name>test-invoice-service</name>
<dependencies>
<dependency>
<groupId>com-test-invoice</groupId>
<artifactId>test-invoice-contract</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<!--Apache PDFbox 是一个开源的、基于 Java 的、支持 PDF 文档生成的工具库-->
<dependency>
<groupId>org.apache.pdfbox</groupId>
<artifactId>fontbox</artifactId>
<version>2.0.1</version>
</dependency>
<dependency>
<groupId>org.apache.pdfbox</groupId>
<artifactId>pdfbox</artifactId>
<version>2.0.4</version>
</dependency>
<!--iText是一个能够快速产生PDF文件的java类库-->
<dependency>
<groupId>com.lowagie</groupId>
<artifactId>itext</artifactId>
<version>2.1.7</version>
</dependency>
<!--mysql start-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<!--mysql end-->
<!--保存到数据库需要如下依赖-->
<!--<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>-->
<!--db start-->
<!--号称性能最好的JDBC连接池-->
<dependency>
<groupId>com.zaxxer</groupId>
<artifactId>HikariCP</artifactId>
<version>${HikariCP.version}</version>
</dependency>
<!--db end-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>${mybatis-plus-spring-boot-starter.version}</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
<!--redis start-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!--redis end-->
<!--分布式链路追踪三个依赖-->
<!--<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-sleuth</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-zipkin</artifactId>
</dependency>
<!--activiti start-->
<dependency>
<groupId>org.activiti</groupId>
<artifactId>activiti-spring-boot-starter-basic</artifactId>
<version>6.0.0</version>
</dependency>
<!--activiti end-->
</dependencies>
<build>
<finalName>${project.artifactId}</finalName>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<fork>true</fork>
</configuration>
<!--如果要访问info接口想获取maven中的属性内容请记得添加如下内容-->
<executions>
<execution>
<goals>
<goal>build-info</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
<resources>
<resource>
<directory>src/main/resources</directory>
<includes>
<include>**/*</include>
</includes>
</resource>
</resources>
</build>
</project>
Resources
自定义分页映射xml-TRbtTestMapper.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.test.invoice.mapper.TRbtTestRepository">
<select id="selectPageVO" resultType="com.test.invoice.data.TRbtTestData"
parameterType="com.test.invoice.data.TRbtTestDataParam">
select id,name,version from t_rbt_test where name = #{param.name}
</select>
</mapper>
application.yml
server:
port: 18800
spring:
profiles:
active: dev # 指定配置文件
http:
encoding:
force: true # 默认编码,即 utf8
activiti:
check-process-definitions: false # 校验流程文件,默认校验resources下的processes文件夹里的流程文件
# 服务名称,即serviceId
application:
name: service-producer
sleuth:
web:
client:
enabled: true # 启用为分布式web client 客户端
sampler:
probability: 1.0 # 将采样比例设置为 1.0,也就是全部都需要。默认是 0.1
application-dev.yml
spring:
datasource:
type: com.zaxxer.hikari.HikariDataSource #数据源类型
url: jdbc:mysql://localhost:3306/luoma_test?serverTimezone=UTC&characterEncoding=utf-8&useSSL=false #数据库url
username: root #数据库用户名
password: luoma_LUOMA #数据库密码
driver-class-name: com.mysql.jdbc.Driver #数据库驱动
hikari:
pool-name: pool-vscloud
connectionTestQuery: SELECT 1
maximum-pool-size: 50
minimum-idle: 10
#mybatis
mybatis-plus:
mapper-locations: classpath:/mapper/*Mapper.xml #mybatis-plus mapper xml 文件地址
#实体扫描,多个package用逗号或者分号分隔
typeAliasesPackage:
typeEnumsPackage:
global-config:
id-type: 4
#字段策略 0:"忽略判断",1:"非 NULL 判断"),2:"非空判断"
field-strategy: 2
#驼峰下划线转换
db-column-underline: true
#刷新mapper 调试神器
refresh-mapper: true
#数据库大写下划线转换
#capital-mode: true
#序列接口实现类配置
#key-generator:
#逻辑删除配置
logic-delete-value: 0
logic-not-delete-value: 1
configuration:
map-underscore-to-camel-case: true
cache-enabled: false
# 服务注册与发现相关配置
eureka:
#自定义实例编号
instance:
instance-id: ${spring.application.name}:${server.port}
# 优先使用IP地址方式进行注册服务
prefer-ip-address: true
# 配置使用指定IP
client:
# 服务注册地址
serviceUrl:
defaultZone: http://127.0.0.1:8772/eureka/
application-test.yml
#spring
spring:
datasource:
type: com.zaxxer.hikari.HikariDataSource
url: jdbc:mysql://localhost:3306/luoma_test?serverTimezone=UTC&characterEncoding=utf-8&useSSL=false
username: root
password: luoma_ai_LEIMU
driver-class-name: com.mysql.jdbc.Driver
hikari:
pool-name: pool-vscloud
connectionTestQuery: SELECT 1
maximum-pool-size: 50
minimum-idle: 10
#mybatis
mybatis-plus:
mapper-locations: classpath:/mapper/*Mapper.xml
#实体扫描,多个package用逗号或者分号分隔
typeAliasesPackage:
typeEnumsPackage:
global-config:
#主键类型 0:"数据库ID自增", 1:"用户输入ID",2:"全局唯一ID (数字类型唯一ID)", 3:"全局唯一ID UUID";
id-type: 0
#字段策略 0:"忽略判断",1:"非 NULL 判断"),2:"非空判断"
field-strategy: 2
#驼峰下划线转换
db-column-underline: true
#刷新mapper 调试神器
refresh-mapper: true
#数据库大写下划线转换
#capital-mode: true
#序列接口实现类配置
#key-generator:
#逻辑删除配置
logic-delete-value: 0
logic-not-delete-value: 1
# #自定义填充策略接口实现
# meta-object-handler:
configuration:
map-underscore-to-camel-case: true
cache-enabled: false
bootstrap.properties
#由于 logback-spring.xml的加载在 application.properties之前,所以之前的配置 logback-spring.xml无法获取到 spring.application.name属性,因此这里将该属性移动到最先加载的 bootstrap.properties配置文件中。
#https://cloud.tencent.com/developer/article/1067431
spring.application.name=service-producer
MybatisPlus 配置类-MybatisPlusConfig
package com.test.invoice.config;
import com.baomidou.mybatisplus.extension.plugins.PaginationInterceptor;
import com.baomidou.mybatisplus.extension.plugins.PerformanceInterceptor;
import org.mybatis.spring.mapper.MapperScannerConfigurer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;
/**
* 从Spring3.0,@Configuration用于定义配置类,可替换xml配置文件,被注解的类内部包含有一个或多个被@Bean注解的方法,这些方法将会被AnnotationConfigApplicationContext或AnnotationConfigWebApplicationContext类进行扫描,并用于构建bean定义,初始化Spring容器。
*/
@Configuration
public class MybatisPlusConfig {
/**
* mybatis-plus SQL执行效率插件【生产环境可以关闭】
*/
@Bean
//@Profile({"dev", "test"})// 设置 dev test 环境开启
@Profile({"dev"})// 设置 dev test 环境开启
public PerformanceInterceptor performanceInterceptor() {
return new PerformanceInterceptor();
}
/**
* mybatis-plus分页插件<br>
* 文档:http://mp.baomidou.com<br>
*/
@Bean
public PaginationInterceptor paginationInterceptor() {
PaginationInterceptor paginationInterceptor = new PaginationInterceptor();
// paginationInterceptor.setLimit(你的最大单页限制数量,默认 500 条,小于 0 如 -1 不受限制);
return paginationInterceptor;
}
/**
* 相当于顶部的:
* {@code @MapperScan("com.baomidou.springboot.mapper*")}
* 这里可以扩展,比如使用配置文件来配置扫描Mapper的路径
*/
@Bean
public MapperScannerConfigurer mapperScannerConfigurer() {
MapperScannerConfigurer scannerConfigurer = new MapperScannerConfigurer();
scannerConfigurer.setBasePackage("com.test.invoice.mapper*");
return scannerConfigurer;
}
}
MybatisPlus 配置类-MyMetaObjectHandler
package com.test.invoice.config;
import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.reflection.MetaObject;
import org.springframework.stereotype.Component;
import java.util.Date;
/**
* mybatis公共字段自动填充
*
*/
@Slf4j
@Component
public class MyMetaObjectHandler implements MetaObjectHandler {
@Override
public void insertFill(MetaObject metaObject) {
String user="luoma";
try {
log.info("【填充数据 --开始】");
this.setFieldValByName("createTime", new Date(), metaObject);
this.setFieldValByName("updateTime", new Date(), metaObject);
this.setFieldValByName("bizTime", new Date(), metaObject);
this.setFieldValByName("isDelete", 0, metaObject);
this.setFieldValByName("createBy", user, metaObject);
this.setFieldValByName("updateBy", user, metaObject);
log.info("【填充数据 --完毕】");
} catch (Exception e) {
log.info("填充错误" + e.getMessage());
}
}
/**
* description 更新自动填充
*/
@Override
public void updateFill(MetaObject metaObject) {
String user="luoma";
this.setFieldValByName("updateTime", new Date(), metaObject);
this.setFieldValByName("bizTime", new Date(), metaObject);
this.setFieldValByName("updateBy", user, metaObject);
log.info("【填充数据 --完毕】");
}
}
自定义接口-TRbtTestRepository
package com.test.invoice.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.test.invoice.data.TRbtTestData;
import com.test.invoice.data.TRbtTestDataParam;
import com.test.invoice.model.TRbtTestEntity;
import org.springframework.stereotype.Repository;
@Repository
public interface TRbtTestRepository extends BaseMapper<TRbtTestEntity> {
IPage<TRbtTestData> selectPageVO(Page<TRbtTestData> page, TRbtTestDataParam param);
}
接口实现-TRbtTestServiceImpl
package com.test.invoice.service.impl;
import com.test.invoice.data.TRbtTestDataParam;
import com.test.invoice.enums.ResponseCode;
import com.test.invoice.vo.ResponseVO;
import com.test.invoice.data.TRbtTestData;
import com.test.invoice.mapper.TRbtTestRepository;
import com.test.invoice.model.TRbtTestEntity;
import com.test.invoice.service.producer.ITRbtTestService;
import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestParam;
import java.util.List;
/**
* TRbtTestServiceImpl
*
* @author:
* @version:
* @date: 2019-08-12 10:23
*/
@Slf4j
@Service
public class TRbtTestServiceImpl extends ServiceImpl<TRbtTestRepository, TRbtTestEntity> implements ITRbtTestService {
@Override
public ResponseVO<String> Add(@RequestBody TRbtTestData data){
TRbtTestEntity entity = new TRbtTestEntity();
entity.setName(data.getName()).setVersion(data.getVersion());
if(baseMapper.insert(entity) > 0){
return new ResponseVO<>(ResponseCode.OK);
}
return new ResponseVO<>(ResponseCode.OPERATE_ERROR,"ERROR");
}
@Override
public ResponseVO<Boolean> Update(@RequestBody TRbtTestData data){
UpdateWrapper<TRbtTestEntity> wrapper = new UpdateWrapper<>();
wrapper.set("name",data.getName()).set("version",data.getVersion());
wrapper.eq("id",data.getId());
return new ResponseVO<>(ResponseCode.OK,update(wrapper));
}
@Override
public ResponseVO<Boolean> Del(@RequestParam(value="name",required = true) String name, @RequestParam(value="version",required = true) String version){
UpdateWrapper<TRbtTestEntity> wrapper = new UpdateWrapper<>();
wrapper.eq("name",name);
wrapper.eq("version",version);
return new ResponseVO<>(ResponseCode.OK, baseMapper.delete(wrapper) > 0);
}
@Override
public ResponseVO<TRbtTestData> Get(@RequestBody TRbtTestData data){
List<TRbtTestEntity> list = baseMapper.selectList(Wrappers.<TRbtTestEntity>lambdaQuery().eq(TRbtTestEntity::getName, data.getName()));
if(!list.isEmpty()){
TRbtTestEntity entity = list.get(0);
data.setName(entity.getName());
data.setVersion(entity.getVersion());
return new ResponseVO<>(ResponseCode.OK, data);
}else{
return new ResponseVO<>(ResponseCode.PARAM_INVALID, data);
}
}
@Override
public ResponseVO<Page<TRbtTestData>> Query(@RequestBody TRbtTestDataParam param){
Page<TRbtTestData> page = new Page<>();
page.setCurrent(param.getPage());
page.setSize(param.getPageSize());
//return new ResponseVO<>(ResponseCode.OK, baseMapper.selectPageVO(page, param));
IPage<TRbtTestData> ipageData = baseMapper.selectPageVO(page, param);
//IPage<TRbtTestData> 转换为 Page<TRbtTestData>
Page<TRbtTestData> pageData = new Page<>();
pageData.setRecords(ipageData.getRecords());
pageData.setCurrent(ipageData.getCurrent());
pageData.setSize(ipageData.getSize());
pageData.setTotal(ipageData.getTotal());
return new ResponseVO<>(ResponseCode.OK, pageData);
}
}
生产者:业务微服务-测试框架系统控制类-TRbtTestController
package com.test.invoice.contorller;
import com.test.invoice.data.TRbtTestData;
import com.test.invoice.data.TRbtTestDataParam;
import com.test.invoice.service.producer.ITRbtTestService;
import com.test.invoice.vo.ResponseVO;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.web.bind.annotation.*;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import javax.annotation.Resource;
/**
*
* 生产者:业务微服务-测试框架系统控制类
*
* @author:
* @version:
* @date: 2019-08-12 09:53
*/
@RestController
@Api(description = "测试框架系统控制类")
public class TRbtTestController {
@Resource
private ITRbtTestService testService;
@PostMapping("/Test/Add")
@ApiOperation(value="系统框架测试-新增数据",httpMethod = "POST",response = ResponseVO.class,notes = "系统框架测试-新增数据")
public ResponseVO Add(@RequestBody TRbtTestData data){
return testService.Add(data);
}
@PostMapping("Test/Update")
@ApiOperation(value="系统框架测试-修改数据",httpMethod = "POST",response = ResponseVO.class,notes = "系统框架测试-修改数据")
public ResponseVO<Boolean> Update(@RequestBody TRbtTestData data){
return testService.Update(data);
}
@RequestMapping(value="Test/Del",method = RequestMethod.GET)
@ApiOperation(value="系统框架测试-删除数据",httpMethod = "GET",response = ResponseVO.class,notes = "系统框架测试-删除数据")
public ResponseVO<Boolean> Del(@RequestParam(value="name",required = true) String name,@RequestParam(value="version",required = true) String version)
{
return testService.Del(name,version);
}
@PostMapping("/Test/Get")
@ApiOperation(value="系统框架测试-获取单个数据",httpMethod = "POST",response = ResponseVO.class,notes = "系统框架测试-获取单个数据")
public ResponseVO<TRbtTestData> Get(@RequestBody TRbtTestData data){
return testService.Get(data);
}
@PostMapping("/Test/Query")
@ApiOperation(value="系统框架测试-查询分页数据",httpMethod = "POST",response = ResponseVO.class,notes = "系统框架测试-查询分页数据")
public ResponseVO<Page<TRbtTestData>> Query(@RequestBody TRbtTestDataParam param){
return testService.Query(param);
}
}
入口类-ServerApplication
package com.test.invoice;
import org.activiti.spring.boot.SecurityAutoConfiguration;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
import org.springframework.web.bind.annotation.RestController;
/**
* 启动类
*
* @author:
* @version:
* @date: 2019-08-12 09:53
*/
@SpringBootApplication(exclude = SecurityAutoConfiguration.class) //禁用 security 验证
@RestController
@EnableEurekaClient //Eureka Client
public class ServerApplication {
public static void main(String[] args){
SpringApplication.run(ServerApplication.class,args);
}
}
测试结果
启动项目 test-invoice-eureka
启动项目 test-invoice-service
启动多个项目参考
首先访问 http://localhost:8772/ ,看 Instances currently registered with Eureka
是否能看到 test-invoice-service
项目
接着使用 Postman
调用 test-invoice-service
接口 Add
结果
创建子模块-test-invoice-web
test-invoice-cloud
上右键,新建模块
左侧面板选择 Maven
,不勾选 Create from archetype
选项,如下图,点击 下一个
即可。
artifactId test-invoice-web
模块名称 test-invoice-web
创建下图中的类和文件
模块简介
作为一个 Eureka 客户端,提供调用接口
类图
pom.xml
server:
port: 8080
servlet:
context-path: /enta/api
spring:
application:
# 服务名,即serviceId
name: service-feign-web
sleuth:
web:
client:
enabled: true # 启用为分布式web client 客户端
sampler:
probability: 1.0 # 将采样比例设置为 1.0,也就是全部都需要。默认是 0.1
# 服务注册与发现相关配置
eureka:
#自定义实例编号
instance:
instance-id: ${spring.application.name}:${server.port}
# 优先使用IP地址方式进行注册服务
prefer-ip-address: true
# 配置使用指定IP
# ip-address: 127.0.0.1
client:
# 服务注册地址
serviceUrl:
# defaultZone: http://127.0.0.1:8772/eureka/
defaultZone: http://127.0.0.1:8772/eureka/
#配置actuator的相关配置
# 描述信息
info:
blog-url: http://luoma.pro
author: luoma
# 加载所有的端点/默认只加载了 info / health
management:
endpoints:
web:
exposure:
include: "*"
endpoint:
health:
show-details: always
shutdown:
enabled: false # 可以关闭制定的端点
bootstrap.properties
#由于 logback-spring.xml的加载在 application.properties之前,所以之前的配置 logback-spring.xml无法获取到 spring.application.name属性,因此这里将该属性移动到最先加载的 bootstrap.properties配置文件中。
#https://cloud.tencent.com/developer/article/1067431
spring.application.name=service-feign-web
消费者,通过 Feign-api 调用-TRbtTestController
package com.test.invoice.controller;
import com.test.invoice.data.TRbtTestData;
import com.test.invoice.data.TRbtTestDataParam;
import com.test.invoice.service.consumer.TRbtTestConsumer;
import com.test.invoice.vo.ResponseVO;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.web.bind.annotation.*;
import org.springframework.beans.factory.annotation.Autowired;
/**
* 消费者,通过 Feign-api 调用
*
* @author:
* @version:
* @date: 2019-08-12 14:49
*/
@RestController
@Api(description = "测试框架系统控制类")
@RequestMapping("/Inv/Api")
public class TRbtTestController {
//@Resource
@Autowired
private TRbtTestConsumer testConsumer;
@PostMapping("Test/Add")
@ApiOperation(value = "系统框架测试-新增数据", httpMethod = "POST", response = ResponseVO.class, notes = "系统框架测试-新增数据")
public ResponseVO Add(@RequestBody TRbtTestData data){
return testConsumer.Add(data);
}
@PostMapping("Test/Update")
@ApiOperation(value = "系统框架测试-修改数据", httpMethod = "POST", response = ResponseVO.class, notes = "系统框架测试-修改数据")
public ResponseVO Update(@RequestBody TRbtTestData data){
return testConsumer.Update(data);
}
@RequestMapping(value="Test/Del",method=RequestMethod.GET)
@ApiOperation(value = "系统框架测试-删除数据", httpMethod = "POST", response = ResponseVO.class, notes = "系统框架测试-删除数据")
public ResponseVO Del(@RequestParam(value="name",required = true) String name,@RequestParam(value="version",required = true) String version){
return testConsumer.Del(name,version);
}
@PostMapping("/Test/Get")
@ApiOperation(value = "系统框架测试-获取单个数据", httpMethod = "POST", response = ResponseVO.class, notes = "系统框架测试-获取单个数据")
public ResponseVO Get(@RequestBody TRbtTestData data){
return testConsumer.Get(data);
}
@PostMapping("/Test/Query")
@ApiOperation(value = "系统框架测试-获取分页数据", httpMethod = "POST", response = ResponseVO.class, notes = "系统框架测试-获取分页数据")
public ResponseVO Query(@RequestBody TRbtTestDataParam param){
return testConsumer.Query(param);
}
}
启动类-WebApplication
package com.test.invoice;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
import org.springframework.cloud.openfeign.EnableFeignClients;
/**
* 启动类
*
* @author:
* @version:
* @date: 2019-08-12 14:43
*/
//禁用 security 验证
@SpringBootApplication(exclude = org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration.class)
@EnableFeignClients //使用feign客户端,https://blog.csdn.net/andy_zhang2007/article/details/86680622
@EnableEurekaClient //启用服务注册与发现
public class WebApplication { //extends SpringBootServletInitializer
public static void main(String[] args){
SpringApplication.run(WebApplication.class,args);
}
}
测试
启动项目 test-invoice-eureka
启动项目 test-invoice-service
启动项目 test-invoice-web
启动多个项目参考
首先访问 http://localhost:8772/ ,看 Instances currently registered with Eureka
是否能看到
test-invoice-service
项目test-invoice-web
项目
测试-Add
使用 Postman
访问 http://localhost:8080/enta/api/Inv/Api/Test/Add
参数
{"id":"","name":"luoma","version":"1.0.6"}
结果
测试-Update
使用 Postman
访问 http://localhost:8080/enta/api/Inv/Api/Test/Update
参数
{"id":"11d1dd6b2bbe1d9a729986f5c2f62b42","name":"测试数据-菲克-1","version":"1.0.1"}
结果
测试-Del
使用 Postman
访问 http://localhost:8080/enta/api/Inv/Api/Test/Del?name=luoma&version=1.0.1
结果
测试-Get
使用 Postman
访问 http://localhost:8080/enta/api/Inv/Api/Test/Get
参数
{"id":"","name":"测试数据-菲克-1","version":""}
测试-Query
使用 Postman
访问 http://localhost:8080/enta/api/Inv/Api/Test/Query
参数
{"page":2,"pageSize":3,"name":"luoma"}
项目开发过程中遇到的问题及解决
父模块 pom.xml 引入包子模块 pom.xml 无法解析问题
访问接口-提示 Unsupported Media Type
项目总类图
项目总结构图
扩展-负载均衡
创建子模块-test-invoice-service-1
test-invoice-cloud
上右键,新建模块
左侧面板选择 Maven
,不勾选 Create from archetype
选项,如下图,点击 下一个
即可。
artifactId test-invoice-service-1
模块名称 test-invoice-service-1
复制 test-invoice-service 模块的所有内容到 test-invoice-service-1
修改启动类为 ServerApplication_1
package com.test.invoice;
import org.activiti.spring.boot.SecurityAutoConfiguration;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
import org.springframework.web.bind.annotation.RestController;
/**
* 启动类
*
* @author:
* @version:
* @date: 2019-08-12 09:53
*/
@SpringBootApplication(exclude = SecurityAutoConfiguration.class) //禁用 security 验证
@RestController
@EnableEurekaClient //Eureka Client
public class ServerApplication_1 {
public static void main(String[] args){
SpringApplication.run(ServerApplication_1.class,args);
}
}
修改 TRbtTestServiceImpl 的 Add 方法
原 Add
方法添加代码 data.setName(data.getName()+"_18801");
以示区分
@Override
public ResponseVO<String> Add(@RequestBody TRbtTestData data){
data.setName(data.getName()+"_18801");
TRbtTestEntity entity = new TRbtTestEntity();
entity.setName(data.getName()).setVersion(data.getVersion());
if(baseMapper.insert(entity) > 0){
return new ResponseVO<>(ResponseCode.OK);
}
return new ResponseVO<>(ResponseCode.OPERATE_ERROR,"ERROR");
}
修改 application.yml 端口号
端口号从 18800
修改为 18801
,其它不变
server:
port: 18801
spring:
profiles:
active: dev # 指定配置文件
http:
encoding:
force: true # 默认编码,即 utf8
activiti:
check-process-definitions: false # 校验流程文件,默认校验resources下的processes文件夹里的流程文件
# 服务名称,即serviceId
application:
name: service-producer
sleuth:
web:
client:
enabled: true # 启用为分布式web client 客户端
sampler:
probability: 1.0 # 将采样比例设置为 1.0,也就是全部都需要。默认是 0.1
启动项目
依次启动
启动项目 `test-invoice-eureka`
启动项目 `test-invoice-service`
启动项目 `test-invoice-service-1`
启动项目 `test-invoice-web`
查看 Eureka 是否注册
使用 Postman 调用接口
快速的多次点击添加
URL:http://localhost:8080/enta/api/Inv/Api/Test/Add
参数:{"id":"","name":"五哥","version":"1.0.6"}
数据库查询结果
SELECT * FROM `t_rbt_test`
where name like '五哥%'
可以看到有些是正常的数据,有些是带后缀 _18801
的数据
说明两个服务中心 test-invoice-service
,test-invoice-service-1
自动提供了服务均衡负载的功能
项目调用关系图