最新 Springboot +Dubbo 分布式服务搭建

本贴最后更新于 2232 天前,其中的信息可能已经时移世易

摘要:
本文中涉及的代码在 github 均可找到 https://github.com/G-
little/priest
,本人从最早的 dubbo 项目开源,便一直是 dubbo 的忠实粉丝,后来 dubbo 项目被 apache 作为顶级开源项目引入也是欢欣鼓舞,作为忠实粉丝也希望为 dubbo 的推广尽一份绵薄之力。


dubbo 作为分布式系统的教科书式开源项目,独立使用起来是非常简单的,但在与新版的 springboot 及其他开源项目的整合使用过程中,因为不同版本开源项目的兼容问题,还是会产生各种匪夷所思的问题。作为开发界的老司机有时也被这些问题折磨的焦头烂额。本文将最新版 dubbo 与 springboot ,mybatis 构建分布式项目的过程详细记录,希望能将大家从项目构建过程的版本坑中解救出来。


另外也把自己开源的良心项目诚心推荐给大家 priest
整个项目采用最新版 springboot+dubbo+mybatis3+springdata-redis 架构,将传统的 rest 接口开发,用户 token 认证,及管理后台项目打包开源,所有代码均可通过插件自动生成,将开发人员从 996 的加班节奏中解救出来。开源不易,感兴趣的同学不妨留下你的小心心 ❤️

概述

写程序我想最讲究的就是知其然,知其所以然,提起项目整合,首先不得不说的就是 dubbo 的设计架构。

![image.png](https://b3logfile.com/file/2019/04/image-de29c20e.png)

首先我们看一下 dubbo 的调用流程这里主要涉及 4 个模块:

  • Registry: 注册中心,dubbo 的服务注册,服务发现均通过该服务作为桥梁。

  • Provider: 服务提供者,开发者实现的具体接口逻辑

  • Consumer: 消费者,Provider 接口的订阅方

  • Monitor: dubbo 服务的调用次数,调用时间监控中心。

    从图中我们可以了解到整个 RPC 服务调用过程为:

  1. 服务提供者将服务注册到服务中心
  2. 消费者在注册中心中订阅服务
  3. 消费者调用已经在注册中心注册的服务

项目构建

一 基础服务

透过 dubbo 的架构我们可以梳理出整个项目依赖的基础服务和技术

  • dubbo 分布式服务
  • spring-boot 基于 spring 的依赖管理
  • jdk 1.8
  • zookeeper dubbo 依赖的注册中心
  • mybatis mysql orm 框架
  • mysql 数据库

二 项目结构规划

一个好的项目规划,一定要考虑到项目的层次划分明确,未来扩展,开发敏捷等等方面。 对于 springboot+dubbo 的项目特点我们不难想到,web 项目
需要远程调用 dubbo 分布式服务 ,dubbo 项目需对外提供 api 服务,因此我们得出如下项目结构:

  • 将 dubbo 项目分为 dubbo-service,和 dubbo-api 部分,方便项目依赖和调用。

  • dao 层逻辑的独立性,我们将 dao 层单独拆分项目(清晰而已,不必要)

  • web 层负责对代码参数的校验及 dubbo 层业务逻辑的对外输出单独一个项目

  • dubbo 项目代码的扩展项目

  • 项目间公共代码项目

最终得到项目结构如下:

├── dubbo // dubbo 打包部署 │   ├── assembly // dubbo assembly 打包配置 │   └── bin // dubbo 启动相关脚本 ├── dubbo-extend // dubbo 项目扩展,可基于dubbo spi机制,对dubbo 进行扩展 │   └── src ├── plugin-test // 插件测试项目,用于代码生成插件的测试(作者开源项目,普通项目构建可以忽略) │   └── src ├── priest-common // 项目共用代码 │   └── src ├── priest-common-web // web 项目共用代码 │   └── src ├── priest-demo // demo 项目 │   ├── priest-demo-api // demo api 项目 │   ├── priest-demo-dao // demo dao 项目 │   ├── priest-demo-http // demo http 项目 │   └── priest-demo-service //demo service 项目 └── priest-generator // 代码生成插件 (作者开源项目,普通项目构建可以忽略)

三 父项目 pom 配置说明

maven 的父子项目与 java 中的继承关系非常的相似,子项目继承父项目的属性,依赖版本,插件配置等,又可以根据子项目的特点和需求,重写项目配置。总结来说父项目的职责如下:

  • 全局版本控制,对于经常出现版本冲突问题的童鞋这一点至关重要,将所有项目版本的控制全部交由父项目控制,当项目出现版本冲突问题时,父级项目便可通过依赖的全局版本控制,轻松解决依赖冲突。

image.png

  • 项目公共 properties 配置 例如 jdk 最低版本,项目源代码编码格式等。

image.png

  • 多环境 profile 切换 ,例如开发环境,线上环境不同的注册中心配置,redis 配置

image.png

  • repositories 私有仓库配置 ,多人协同开发,api 打包发布私有仓库

  • 插件管理 类似版本依赖

image.png

下面粘贴顶级项目的 pom 配置(含注释):

<?xml version="1.0"?> <project xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd" xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> <modelVersion>4.0.0</modelVersion> <groupId>com.little.g</groupId> <artifactId>priest</artifactId> <version>0.0.1-SNAPSHOT</version> <packaging>pom</packaging> <name>priest</name> <url>http://maven.apache.org</url> <modules> <!-- 项目模块管理 --> <module>dubbo-extend</module> <module>priest-demo</module> <module>priest-common</module> <module>priest-common-web</module> <module>priest-generator</module> <module>plugin-test</module> </modules> <!-- 全局属性配置 --> <properties> <failOnMissingWebXml>false</failOnMissingWebXml> <resource.delimiter>${}</resource.delimiter> <java.version>1.8</java.version> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <mysql-connector-java.version>8.0.13</mysql-connector-java.version> <tk.mapper.version>4.1.5</tk.mapper.version> <dubbo.version>2.7.0</dubbo.version> </properties> <!-- 打包配置信息 --> <profiles> <profile> <!-- 开发环境 --> <id>develop</id> <!-- 默认 --> <activation> <activeByDefault>true</activeByDefault> </activation> <properties> <!-- 日志 --> <priest.log.level>DEBUG</priest.log.level> <priest.log.path>/data/logs</priest.log.path> <!--打包编码 --> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <priest.dubbo.zk.url>zookeeper://127.0.0.1:2181</priest.dubbo.zk.url> <priest.redis.nodes>127.0.0.1:6379</priest.redis.nodes> <priest.redis.password>123456</priest.redis.password> <priest.online>false</priest.online> </properties> </profile> <profile> <!-- 线上环境 --> <id>online</id> <activation> <activeByDefault>false</activeByDefault> </activation> <properties> <!-- 日志 --> <priest.log.level>DEBUG</priest.log.level> <priest.log.path>/data/logs</priest.log.path> <!--打包编码 --> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <priest.dubbo.zk.url>zookeeper://192.168.0.195:2181</priest.dubbo.zk.url> </properties> </profile> </profiles> <!--开发者信息--> <developers> <developer> <name>llg.java</name> <id>ligang</id> <email>llg.java@gmail.com</email> <organization>xiaogang.org.cn</organization> <roles> <role>Java Developer</role> </roles> </developer> </developers> <!-- <distributionManagement> <repository> <id>nexus-releases</id> <name>Nexus Release Repository</name> <url>http://nexus.shuzijiayuan.com/content/repositories/releases/</url> </repository> <snapshotRepository> <id>nexus-snapshots</id> <name>Nexus Snapshot Repository</name> <url>http://nexus.shuzijiayuan.com/content/repositories/snapshots/</url> </snapshotRepository> </distributionManagement> --> <!-- 仓库配置 --> <repositories> <repository> <id>apache.snapshots.https</id> <name>Apache Development Snapshot Repository</name> <url>https://repository.apache.org/content/repositories/snapshots</url> <releases> <enabled>false</enabled> </releases> <snapshots> <enabled>true</enabled> </snapshots> </repository> </repositories> <!-- 依赖管理 --> <dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.1.3.RELEASE</version> <type>pom</type> <scope>import</scope> </dependency> <!--本系统依赖管理 start--> <dependency> <groupId>com.little.g</groupId> <artifactId>priest-admin-common</artifactId> <version>0.0.1-SNAPSHOT</version> </dependency> <dependency> <groupId>com.little.g</groupId> <artifactId>priest-admin-api</artifactId> <version>0.0.1-SNAPSHOT</version> </dependency> <dependency> <groupId>com.little.g</groupId> <artifactId>priest-admin-dao</artifactId> <version>0.0.1-SNAPSHOT</version> </dependency> <dependency> <groupId>com.little.g</groupId> <artifactId>priest-user-token</artifactId> <version>0.0.1-SNAPSHOT</version> </dependency> <dependency> <groupId>com.little.g</groupId> <artifactId>priest-common</artifactId> <version>0.0.1-SNAPSHOT</version> </dependency> <dependency> <groupId>com.little.g</groupId> <artifactId>priest-user-api</artifactId> <version>0.0.1-SNAPSHOT</version> </dependency> <dependency> <groupId>com.little.g</groupId> <artifactId>priest-user-dao</artifactId> <version>0.0.1-SNAPSHOT</version> </dependency> <dependency> <groupId>com.little.g</groupId> <artifactId>dubbo-extend</artifactId> <version>0.0.1-SNAPSHOT</version> </dependency> <dependency> <groupId>com.little.g</groupId> <artifactId>priest-common-web</artifactId> <version>0.0.1-SNAPSHOT</version> </dependency> <dependency> <groupId>com.little.g</groupId> <artifactId>priest-demo-api</artifactId> <version>0.0.1-SNAPSHOT</version> </dependency> <dependency> <groupId>com.little.g</groupId> <artifactId>priest-demo-dao</artifactId> <version>0.0.1-SNAPSHOT</version> </dependency> <!--本系统依赖管理 end--> <dependency> <groupId>com.github.qcloudsms</groupId> <artifactId>qcloudsms</artifactId> <version>1.0.5</version> </dependency> <dependency> <groupId>commons-fileupload</groupId> <artifactId>commons-fileupload</artifactId> <version>[1.3.3)</version> </dependency> <dependency> <groupId>javax.servlet</groupId> <artifactId>servlet-api</artifactId> <version>2.5</version> <scope>provided</scope> </dependency> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> <version>[2.9.8,)</version> </dependency> <dependency> <groupId>com.fasterxml.jackson.module</groupId> <artifactId>jackson-module-afterburner</artifactId> <version>2.9.6</version> </dependency> <dependency> <groupId>org.reflections</groupId> <artifactId>reflections</artifactId> <version>0.9.10</version> </dependency> <dependency> <groupId>org.jodd</groupId> <artifactId>jodd-props</artifactId> <version>3.6.1</version> </dependency> <dependency> <groupId>com.github.pagehelper</groupId> <artifactId>pagehelper</artifactId> <version>RELEASE</version> </dependency> <dependency> <groupId>joda-time</groupId> <artifactId>joda-time</artifactId> <version>RELEASE</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-freemarker</artifactId> <version>2.1.3.RELEASE</version> </dependency> <!-- Apache Dubbo --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-framework-bom</artifactId> <version>5.1.5.RELEASE</version> <type>pom</type> <scope>import</scope> </dependency> <dependency> <groupId>org.apache.dubbo</groupId> <artifactId>dubbo-dependencies-bom</artifactId> <version>${dubbo.version}</version> <type>pom</type> <scope>import</scope> </dependency> <dependency> <groupId>org.apache.dubbo</groupId> <artifactId>dubbo</artifactId> <version>${dubbo.version}</version> <exclusions> <exclusion> <groupId>org.springframework</groupId> <artifactId>spring</artifactId> </exclusion> <exclusion> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> </exclusion> <exclusion> <groupId>javax.servlet</groupId> <artifactId>servlet-api</artifactId> </exclusion> <exclusion> <groupId>log4j</groupId> <artifactId>log4j</artifactId> </exclusion> </exclusions> </dependency> <dependency> <groupId>com.google.guava</groupId> <artifactId>guava</artifactId> <version>20.0</version> </dependency> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-lang3</artifactId> <version>3.8.1</version> </dependency> <dependency> <groupId>commons-lang</groupId> <artifactId>commons-lang</artifactId> <version>2.6</version> </dependency> <dependency> <groupId>javax.validation</groupId> <artifactId>validation-api</artifactId> <version>2.0.1.Final</version> </dependency> <dependency> <groupId>org.hibernate</groupId> <artifactId>hibernate-validator</artifactId> <version>6.0.10.Final</version> </dependency> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>2.0.0</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>${mysql-connector-java.version}</version> </dependency> <dependency> <groupId>tk.mybatis</groupId> <artifactId>mapper-spring-boot-starter</artifactId> <version>2.1.5</version> </dependency> <dependency> <groupId>tk.mybatis</groupId> <artifactId>mapper</artifactId> <version>${tk.mapper.version}</version> </dependency> <dependency> <groupId>tk.mybatis</groupId> <artifactId>mapper-generator</artifactId> <version>1.1.5</version> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid</artifactId> <version>1.1.10</version> </dependency> <dependency> <groupId>com.googlecode.libphonenumber</groupId> <artifactId>libphonenumber</artifactId> <version>7.0.6</version> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>[1.2.31,)</version> </dependency> </dependencies> </dependencyManagement> <!-- 公共构建属性及插件配置 --> <build> <resources> <resource> <directory>src/main/resources</directory> <filtering>true</filtering> </resource> <resource> <directory>src/main/conf</directory> <filtering>true</filtering> <targetPath>conf</targetPath> </resource> </resources> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> </plugin> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-jar-plugin</artifactId> </plugin> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-source-plugin</artifactId> </plugin> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-dependency-plugin</artifactId> </plugin> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-surefire-plugin</artifactId> </plugin> </plugins> <!-- 插件版本管理 --> <pluginManagement> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <version>3.5</version> <configuration> <source>${java.version}</source> <target>${java.version}</target> <encoding>${project.build.sourceEncoding}</encoding> </configuration> </plugin> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-jar-plugin</artifactId> <version>2.3.2</version> <configuration> <archive> <index>true</index> </archive> </configuration> </plugin> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-source-plugin</artifactId> <version>2.4</version> <executions> <execution> <id>attach-sources</id> <goals> <goal>jar-no-fork</goal> </goals> </execution> </executions> </plugin> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-dependency-plugin</artifactId> <version>2.10</version> </plugin> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-surefire-plugin</artifactId> <version>2.19.1</version> <configuration> <skipTests>true</skipTests> </configuration> </plugin> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-release-plugin</artifactId> <version>2.5.3</version> <configuration> <autoVersionSubmodules>true</autoVersionSubmodules> </configuration> </plugin> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-assembly-plugin</artifactId> <version>2.6</version> </plugin> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-deploy-plugin</artifactId> <version>2.8.2</version> </plugin> <plugin> <groupId>org.mybatis.generator</groupId> <artifactId>mybatis-generator-maven-plugin</artifactId> <version>1.3.7</version> <configuration> <verbose>true</verbose> <overwrite>true</overwrite> <configurationFile>${project.basedir}/src/test/resources/generatorConfig.xml</configurationFile> </configuration> <dependencies> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>${mysql-connector-java.version}</version> </dependency> <dependency> <groupId>tk.mybatis</groupId> <artifactId>mapper</artifactId> <version>${tk.mapper.version}</version> </dependency> </dependencies> </plugin> <plugin> <groupId>com.little.g</groupId> <artifactId>generator-maven-plugin</artifactId> <version>0.0.1-SNAPSHOT</version> <configuration> <configurationFile>${project.basedir}/src/main/conf/GenerateConfig.xml</configurationFile> </configuration> </plugin> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <version>2.1.3.RELEASE</version> </plugin> </plugins> </pluginManagement> </build> </project>

四 dao 项目配置

mybatis3 + mybatis-generator 组合可以称得上是 orm 界的瑞士军刀,mybatis 本身轻量,灵活,性能高但不具备敏捷开发的能力,mybatis-generator 插件则完美填补了 mybatis 本身的缺陷,下面让我们一睹他们的风采。

pom 依赖配置

没什么可说的直接贴带注释的 pom 配置:

<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> <groupId>com.little.g</groupId> <artifactId>priest-demo</artifactId> <version>0.0.1-SNAPSHOT</version> </parent> <modelVersion>4.0.0</modelVersion> <artifactId>priest-demo-dao</artifactId> <packaging>jar</packaging> <name>priest-demo-dao</name> <url>http://maven.apache.org</url> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.basedir>${project.basedir}</project.basedir> </properties> <profiles> <profile> <!-- 开发环境 --> <id>develop</id> <!-- 默认 --> <activation> <activeByDefault>true</activeByDefault> </activation> <properties> <!-- 数据库配置 --> <priest.jdbc.driver>com.mysql.jdbc.Driver</priest.jdbc.driver> <priest.jdbc.url>jdbc:mysql://192.168.2.101:3306/little_g?useUnicode=true&amp;characterEncoding=UTF-8&amp;autoReconnect=true</priest.jdbc.url> <priest.jdbc.username>priest</priest.jdbc.username> <priest.jdbc.password>priest</priest.jdbc.password> </properties> </profile> <profile> <!-- 线上环境 TODO --> <id>online</id> <activation> <activeByDefault>false</activeByDefault> </activation> <properties> <!-- 数据库 --> <priest.jdbc.driver>com.mysql.jdbc.Driver</priest.jdbc.driver> <priest.jdbc.url>jdbc:mysql://192.168.2.101:3306/little_g?useUnicode=true&amp;characterEncoding=UTF-8&amp;autoReconnect=true</priest.jdbc.url> <priest.jdbc.username>priest</priest.jdbc.username> <priest.jdbc.password>priest</priest.jdbc.password> </properties> </profile> </profiles> <dependencies> <dependency> <groupId>tk.mybatis</groupId> <artifactId>mapper-spring-boot-starter</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> </dependency> <!-- 带监控的阿里数据库连接池 --> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid</artifactId> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-aspects</artifactId> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.mybatis.generator</groupId> <artifactId>mybatis-generator-maven-plugin</artifactId> <configuration> <verbose>true</verbose> <overwrite>true</overwrite> <configurationFile>${project.basedir}/src/test/resources/generatorConfig.xml</configurationFile> </configuration> <dependencies> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> </dependency> <dependency> <groupId>tk.mybatis</groupId> <artifactId>mapper</artifactId> </dependency> </dependencies> </plugin> </plugins> </build> </project>

spring 及 mybatis-generator 配置

为了让 mybatis-generator 和 spring 的数据库连接配置能够共享,我们对数据库配置配置信息进行了单独文件抽取 jdbc.properties

jdbc.driverClass=com.mysql.jdbc.Driver jdbc.url=${priest.jdbc.url} jdbc.user=${priest.jdbc.username} jdbc.password=${priest.jdbc.password}

遵从 dubbo 的默认配置规范,我们将所有的 spring 配置文件放在
META-INF/spring 目录下 , 下面是 applicationContext-dao.xml 的配置:

<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:tx="http://www.springframework.org/schema/tx" xmlns:context="http://www.springframework.org/schema/context" xsi:schemaLocation="http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.0.xsd http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-3.0.xsd"> <context:property-placeholder order="21" location="classpath*:jdbc.properties" file-encoding="UTF-8" ignore-unresolvable="true"/> <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource" init-method="init" destroy-method="close"> <property name="url" value="${jdbc.url}" /> <property name="username" value="${jdbc.user}" /> <property name="password" value="${jdbc.password}" /> <property name="filters" value="stat" /> <property name="maxActive" value="20" /> <property name="initialSize" value="1" /> <property name="maxWait" value="60000" /> <property name="minIdle" value="1" /> <property name="timeBetweenEvictionRunsMillis" value="60000" /> <property name="minEvictableIdleTimeMillis" value="300000" /> <property name="testWhileIdle" value="true" /> <property name="testOnBorrow" value="false" /> <property name="testOnReturn" value="false" /> <property name="poolPreparedStatements" value="true" /> <property name="maxOpenPreparedStatements" value="20" /> <property name="asyncInit" value="true" /> </bean> <!-- transaction manager, use JtaTransactionManager for global tx --> <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"> <property name="dataSource" ref="dataSource" /> </bean> <!--事务模板 --> <bean id="transactionTemplate" class="org.springframework.transaction.support.TransactionTemplate"> <property name="transactionManager" ref="transactionManager" /> <!--ISOLATION_DEFAULT 表示由使用的数据库决定 --> <property name="isolationLevelName" value="ISOLATION_READ_COMMITTED"/> <property name="propagationBehaviorName" value="PROPAGATION_REQUIRED"/> <!-- <property name="timeout" value="30"/> --> </bean> <!-- enable autowire --> <context:annotation-config/> <!-- enable transaction demarcation with annotations --> <tx:annotation-driven mode="aspectj" transaction-manager="transactionManager" /> <!-- define the SqlSessionFactory --> <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean"> <property name="dataSource" ref="dataSource" /> <!--要映射类的包路径,即POJO对象的路径--> <property name="typeAliasesPackage" value="com.little.g.*.model" /> <!--扫描mapper文件,否则如果Mapper接口文件改名的话,就会出现找不到对应的Mapper中方法的错误--> <property name="mapperLocations" value="classpath*:com/little/g/*/mapper/*.xml"/> </bean> <bean id="sqlSessionTemplate" class="org.mybatis.spring.SqlSessionTemplate"> <constructor-arg index="0" ref="sqlSessionFactory"></constructor-arg> </bean> <!-- scan for mappers and let them be autowired --> <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer"> <!--扫描Mapper类并使它们自动装载--> <property name="basePackage" value="com.little.g.**.mapper" /> </bean> </beans>

mybatis-generator 代码生成配置 generatorConfig.xml:

<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE generatorConfiguration PUBLIC "-//mybatis.org//DTD MyBatis Generator Configuration 1.0//EN" "http://mybatis.org/dtd/mybatis-generator-config_1_0.dtd"> <generatorConfiguration> <properties url="file://${project.basedir}/target/classes/jdbc.properties"/> <!--<context id="Mysql" targetRuntime="MyBatis3Simple" defaultModelType="flat">--> <context id="DB2Tables" targetRuntime="MyBatis3"> <!-- 自动识别数据库关键字,默认false --> <property name="autoDelimitKeywords" value="true" /> <!--可以使用``包括字段名,避免字段名与sql保留字冲突报错 --> <property name="beginningDelimiter" value="`" /> <property name="endingDelimiter" value="`" /> <plugin type="org.mybatis.generator.plugins.SerializablePlugin"></plugin> <plugin type="org.mybatis.generator.plugins.CaseInsensitiveLikePlugin"></plugin> <!-- caseSensitive默认false,当数据库表名区分大小写时,可以将该属性设置为true --> <!-- 支持该模式 <plugin type="tk.mybatis.mapper.generator.MapperPlugin"> <property name="mappers" value="tk.mybatis.mapper.common.Mapper"/> <property name="caseSensitive" value="true"/> </plugin> --> <commentGenerator> <property name="suppressDate" value="true" /> <property name="suppressAllComments" value="true" /> </commentGenerator> <jdbcConnection driverClass="${jdbc.driverClass}" connectionURL="${jdbc.url}" userId="${jdbc.user}" password="${jdbc.password}"> <property name="nullCatalogMeansCurrent" value="true" /> </jdbcConnection> <javaTypeResolver> <property name="forceBigDecimals" value="false" /> </javaTypeResolver> <javaModelGenerator targetPackage="com.little.g.demo.model" targetProject="${project.basedir}/src/main/java"> <property name="enableSubPackages" value="true" /> <property name="trimStrings" value="true" /> </javaModelGenerator> <sqlMapGenerator targetPackage="com.little.g.demo.mapper" targetProject="${project.basedir}/src/main/resources"> <property name="enableSubPackages" value="true" /> </sqlMapGenerator> <javaClientGenerator type="XMLMAPPER" targetPackage="com.little.g.demo.mapper" targetProject="${project.basedir}/src/main/java"> <property name="enableSubPackages" value="true" /> </javaClientGenerator> <table tableName="user"> <generatedKey column="id" sqlStatement="JDBC"/> </table> </context> </generatorConfiguration>

数据库建表

demo 测试 User 表建表 sql:

CREATE TABLE `user` ( `id` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '唯一标识', `my_name` varchar(30) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '名字', `age` int(11) DEFAULT NULL COMMENT '年龄', `mobile` varchar(20) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '手机号', `create_time` bigint(20) DEFAULT NULL COMMENT '创建时间', PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=25 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

代码生成

命令行执行:

mvn clean install cd priest/priest-demo/priest-demo-dao/ mvn mybatis-generator:generate

输出生成文件名即可运行代码测试了

dao 测试

创建测试代码,运行 junit 测试

package com.little.g.common.web; import com.little.g.demo.mapper.UserMapper; import com.little.g.demo.model.User; import org.junit.Assert; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; import javax.annotation.Resource; /** * Created by lengligang on 2019/3/9. */ @RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration(locations = "classpath*:/META-INF/spring/*.xml") public class UserMapperTest { @Resource private UserMapper userMapper; @Test public void testAdd(){ User user = new User(); user.setMyName("张三"); int r=userMapper.insert(user); Assert.assertTrue(r>0); } }

五 api 项目配置

api 项目为 web 层 调用 dubbo 的接口项目即接口规范,依照项目分层的逻辑,dao 层的 pojo 不可以被 api 依赖,api 层需要有自己的 pojo 即 dto:

创建 dto

package com.little.g.demo.dto; import java.io.Serializable; public class UserDTO implements Serializable { private Integer id; private String myName; private Integer age; private String mobile; private Long createTime; private static final long serialVersionUID = 1L; public Integer getId() { return id; } public void setId(Integer id) { this.id = id; } public String getMyName() { return myName; } public void setMyName(String myName) { this.myName = myName == null ? null : myName.trim(); } public Integer getAge() { return age; } public void setAge(Integer age) { this.age = age; } public String getMobile() { return mobile; } public void setMobile(String mobile) { this.mobile = mobile == null ? null : mobile.trim(); } public Long getCreateTime() { return createTime; } public void setCreateTime(Long createTime) { this.createTime = createTime; } }

创建 UserService

package com.little.g.demo.api; import com.little.g.common.dto.ListResultDTO; import com.little.g.common.params.TimeQueryParam; import com.little.g.demo.dto.UserDTO; /** * Created by lengligang on 2019/3/9. */ public interface UserService { /** * 添加 * @param entity * @return */ boolean add(UserDTO entity); /** * 根据id获取 * @param id * @return */ UserDTO get(Integer id); /** * 更新 * @param entity * @return */ boolean update(UserDTO entity); /** * 删除 * @param id * @return */ boolean delete(Integer id); /** * 增量查询 * @param param * @return */ ListResultDTO<UserDTO> list(TimeQueryParam param); }

六 service 项目配置

配置依赖

将 priest-demo-api ,priest-demo-dao 加入 service 项目依赖最终 pom.xml 配置如下 :



com.little.g
priest-demo
0.0.1-SNAPSHOT

4.0.0

<artifactId>priest-demo-service</artifactId> <packaging>jar</packaging> <name>priest-demo-service</name> <url>http://maven.apache.org</url> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> </properties> <profiles> <profile> <!-- 开发环境 --> <id>develop</id> <!-- 默认 --> <activation> <activeByDefault>true</activeByDefault> </activation> <properties> </properties> </profile> <profile> <!-- 线上环境 --> <id>online</id> <activation> <activeByDefault>false</activeByDefault> </activation> <properties> </properties> </profile> </profiles> <dependencies> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-devtools</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <!-- dubbo 依赖 --> <dependency> <groupId>com.little.g</groupId> <artifactId>dubbo-extend</artifactId> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-core</artifactId> </dependency> <dependency> <groupId>org.apache.dubbo</groupId> <artifactId>dubbo</artifactId> </dependency> <dependency> <groupId>io.netty</groupId> <artifactId>netty-all</artifactId> </dependency> <dependency> <groupId>org.apache.curator</groupId> <artifactId>curator-framework</artifactId> </dependency> <dependency> <groupId>org.apache.curator</groupId> <artifactId>curator-recipes</artifactId> </dependency> <dependency> <groupId>org.apache.zookeeper</groupId> <artifactId>zookeeper</artifactId> </dependency> <!-- dubbo 依赖 --> <dependency> <groupId>com.little.g</groupId> <artifactId>priest-demo-api</artifactId> </dependency> <dependency> <groupId>com.little.g</groupId> <artifactId>priest-demo-dao</artifactId> </dependency> </dependencies> <build> <plugins> <!-- assembly 打包 maven dubbo 部署包配置 --> <plugin> <artifactId>maven-assembly-plugin</artifactId> <configuration> <finalName>${project.name}</finalName> <descriptor>${project.parent.parent.basedir}/dubbo/assembly/assembly.xml</descriptor> </configuration> <executions> <execution> <id>make-assembly</id> <phase>package</phase> <goals> <goal>single</goal> </goals> </execution> </executions> </plugin> </plugins> </build>

spring 配置

config.properties 配置:

# 读取 parent zookeeper 配置 zookeeper.url=${priest.dubbo.zk.url}

META-INF/spring/spring-config.xml 配置:

<beans xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context" xmlns="http://www.springframework.org/schema/beans" xmlns:tx="http://www.springframework.org/schema/tx" xmlns:util="http://www.springframework.org/schema/util" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util.xsd"> <context:property-placeholder location="classpath:config.properties" ignore-unresolvable="true" file-encoding="UTF-8" /> <!-- service 自动扫描--> <context:component-scan base-package="com.little.g.**.service"/> </beans>

userService 代码实现

package com.little.g.demo.service; import com.little.g.common.dto.ListResultDTO; import com.little.g.common.params.TimeQueryParam; import com.little.g.demo.api.UserService; import com.little.g.demo.dto.UserDTO; import com.little.g.demo.mapper.UserMapper; import com.little.g.demo.model.User; import com.little.g.demo.model.UserExample; import org.springframework.beans.BeanUtils; import org.springframework.stereotype.Service; import org.springframework.util.CollectionUtils; import javax.annotation.Resource; import java.util.List; import java.util.stream.Collectors; /** * Created by lengligang on 2019/3/9. */ @Service("userService") public class UserServiceImpl implements UserService { @Resource private UserMapper userMapper; @Override public boolean add(UserDTO entity) { User user=new User(); BeanUtils.copyProperties(entity,user); return userMapper.insertSelective(user)>0; } @Override public UserDTO get(Integer id) { User user=userMapper.selectByPrimaryKey(id); if(user == null){ return null; } UserDTO dto=new UserDTO(); BeanUtils.copyProperties(user,dto); return dto; } @Override public boolean update(UserDTO entity) { if(entity.getId() == null) return false; User user=new User(); BeanUtils.copyProperties(entity,user); return userMapper.updateByPrimaryKeySelective(user)>0; } @Override public boolean delete(Integer id) { return userMapper.deleteByPrimaryKey(id)>0; } @Override public ListResultDTO<UserDTO> list(TimeQueryParam param) { ListResultDTO<UserDTO> result=param.getResult(ListResultDTO.class); UserExample example = new UserExample(); example.or().andCreateTimeLessThan(param.getLast()); example.setOrderByClause(String.format("create_time desc limit %d",result.getLimit())); List<User> list= userMapper.selectByExample(example); if(CollectionUtils.isEmpty(list)){ return result; } result.setLast(list.get(list.size()-1).getCreateTime()); result.setList(list.stream().map(entity->{ UserDTO dto=new UserDTO(); BeanUtils.copyProperties(entity,dto); return dto; }).collect(Collectors.toList())); return result; } }

dubbo 服务配置

META-INF/spring/dubbo-config.xml:

<?xml version="1.0" encoding="UTF-8"?> <beans xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:dubbo="http://dubbo.apache.org/schema/dubbo" xmlns="http://www.springframework.org/schema/beans" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd http://dubbo.apache.org/schema/dubbo http://dubbo.apache.org/schema/dubbo/dubbo.xsd"> <dubbo:application name="com.little.g.demo" logger="slf4j"/> <dubbo:protocol name="dubbo"/> <dubbo:registry address="${zookeeper.url}"/> <dubbo:service interface="com.little.g.demo.api.UserService" ref="userService"/> </beans>

dubbo 启动测试

package com.little.g.demo; import org.apache.dubbo.container.Main; /** * Created by lengligang on 2019/3/9. */ public class TestDubbo { public static void main(String[] args) { Main.main(args); } }

dubbo junit 测试

package com.little.g.demo.service; import com.little.g.demo.api.UserService; import com.little.g.demo.dto.UserDTO; import org.junit.Assert; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; import javax.annotation.Resource; /** * Created by lengligang on 2019/3/9. */ @RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration(locations = "classpath*:/META-INF/spring/*.xml") public class UserServiceTest{ @Resource private UserService userService; @Test public void testAdd(){ UserDTO dto= new UserDTO(); dto.setCreateTime(System.currentTimeMillis()); Assert.assertTrue(userService.add(dto)); } }

七 web 项目配置

项目依赖配置

web 层增加 priest-demo-api 及 dubbo 依赖 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"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>com.little.g</groupId> <artifactId>priest-demo</artifactId> <version>0.0.1-SNAPSHOT</version> </parent> <groupId>com.little.g.demo</groupId> <artifactId>priest-demo-http</artifactId> <version>0.0.1-SNAPSHOT</version> <packaging>war</packaging> <name>priest-demo-http</name> <description>Demo project for Spring Boot</description> <properties> <java.version>1.8</java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-tomcat</artifactId> <scope>provided</scope> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>org.apache.dubbo</groupId> <artifactId>dubbo</artifactId> </dependency> <dependency> <groupId>io.netty</groupId> <artifactId>netty-all</artifactId> </dependency> <dependency> <groupId>org.apache.curator</groupId> <artifactId>curator-framework</artifactId> </dependency> <dependency> <groupId>org.apache.curator</groupId> <artifactId>curator-recipes</artifactId> </dependency> <dependency> <groupId>org.apache.zookeeper</groupId> <artifactId>zookeeper</artifactId> </dependency> <dependency> <groupId>com.little.g</groupId> <artifactId>priest-common-web</artifactId> </dependency> <dependency> <groupId>com.little.g</groupId> <artifactId>priest-demo-api</artifactId> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <executions> <execution> <goals> <goal>repackage</goal> </goals> </execution> </executions> </plugin> </plugins> </build> </project>

demo springboot 启动类编写

package com.little.g.demo.web; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.context.annotation.ComponentScan; import org.springframework.web.servlet.config.annotation.EnableWebMvc; @SpringBootApplication public class PriestDemoHttpApplication { public static void main(String[] args) { SpringApplication.run(PriestDemoHttpApplication.class, args); } }

springboot 配置

package com.little.g.common.web.config; import com.little.g.common.web.exception.GlobalExceptionHandler; import com.little.g.common.web.utils.ReloadableResourceBundleMessageSource; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.web.servlet.error.ErrorAttributes; import org.springframework.context.MessageSource; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.ImportResource; import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean; import org.springframework.web.servlet.LocaleResolver; import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; import org.springframework.web.servlet.i18n.LocaleChangeInterceptor; import org.springframework.web.servlet.i18n.SessionLocaleResolver; import java.util.Locale; /** * Created by lengligang on 2019/3/12. */ @ImportResource(locations = {"classpath:META-INF/spring/dubbo-consume.xml"}) //dubbo 配置引用 @Configuration public class AppConfig { /** * 国际化配置文件 */ @Value("${spring.messages.basename}") private String baseName; /** * 异常统一处理 * @return */ @Bean GlobalExceptionHandler exceptionHandler(){ return new GlobalExceptionHandler(); } /** * 自定义 rest 协议格式 * @return */ @Bean public ErrorAttributes errorAttributes() { return new PriestErrorAttributes(); } /** * 默认解析器 其中locale表示默认语言 */ @Bean public LocaleResolver localeResolver() { SessionLocaleResolver localeResolver = new SessionLocaleResolver(); localeResolver.setDefaultLocale(Locale.CHINA); return localeResolver; } /** * 默认拦截器 其中lang表示切换语言的参数名 */ @Bean public WebMvcConfigurer localeInterceptor() { return new WebMvcConfigurer() { @Override public void addInterceptors(InterceptorRegistry registry) { LocaleChangeInterceptor localeInterceptor = new LocaleChangeInterceptor(); localeInterceptor.setParamName("lang"); registry.addInterceptor(localeInterceptor); } }; } @Bean public MessageSource messageSource(){ ReloadableResourceBundleMessageSource messageSource=new ReloadableResourceBundleMessageSource(); messageSource.setBasename(baseName); messageSource.setCacheSeconds(3600); return messageSource; } /** * 国际化配置 * @return */ @Bean public LocalValidatorFactoryBean getValidator() { LocalValidatorFactoryBean bean = new LocalValidatorFactoryBean(); bean.setValidationMessageSource(messageSource()); return bean; } @Bean public MessageSourceUtil messageSourceUtil(){ return new MessageSourceUtil(); } }

application.properties 配置:

#启动端口 server.port=8888 #国际化 spring.messages.basename=classpath*:i18n/messages # dubbo 注册中心 zookeeper.url=${priest.dubbo.zk.url}

dubbo 服务引用

META-INF/spring/dubbo-consume.xml:

<?xml version="1.0" encoding="UTF-8"?> <beans xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:dubbo="http://dubbo.apache.org/schema/dubbo" xmlns="http://www.springframework.org/schema/beans" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd http://dubbo.apache.org/schema/dubbo http://dubbo.apache.org/schema/dubbo/dubbo.xsd"> <dubbo:application name="com.little.g.demo.web" logger="slf4j"/> <dubbo:protocol name="dubbo"/> <dubbo:registry address="${zookeeper.url}"/> <dubbo:reference id="userService" interface="com.little.g.demo.api.UserService" /> </beans>

UserController.java 编写

package com.little.g.demo.web; import com.little.g.common.ResultJson; import com.little.g.common.dto.ListResultDTO; import com.little.g.common.params.TimeQueryParam; import com.little.g.demo.api.UserService; import com.little.g.demo.dto.UserDTO; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import javax.annotation.Resource; import javax.validation.Valid; /** * Created by lengligang on 2019/3/12. */ @RequestMapping("/user") @RestController public class UserController { @Resource private UserService userService; @RequestMapping(value = "/add",method = RequestMethod.POST) public ResultJson add(@Valid UserDTO params){ ResultJson r=new ResultJson(); if(userService.add(params)){ return r; } r.setC(ResultJson.SYSTEM_UNKNOWN_EXCEPTION); return r; } @RequestMapping(value = "/del",method = RequestMethod.GET) public ResultJson del(@RequestParam Integer id){ ResultJson r=new ResultJson(); if(userService.delete(id)){ return r; } r.setC(ResultJson.SYSTEM_UNKNOWN_EXCEPTION); return r; } @RequestMapping(value = "/update",method = RequestMethod.POST) public ResultJson update(@Valid UserDTO params){ ResultJson r=new ResultJson(); if(userService.update(params)){ return r; } r.setC(ResultJson.SYSTEM_UNKNOWN_EXCEPTION); return r; } @RequestMapping(value = "/list",method = RequestMethod.GET) public ResultJson list(@Valid TimeQueryParam params){ ResultJson r=new ResultJson(); ListResultDTO<UserDTO> list= userService.list(params); r.setData(list); return r; } }

八 测试

启动 dubbo

TestDubbo main 方法启动 dubbo

spring-boot 插件运行 spring-boot:run 启动 web 项目

cd priest/priest-demo/priest-demo-web mvn spring-boot:run

相关链接

项目源代码 https://github.com/G-little/priest
[springboot] (https://docs.spring.io/spring-boot/docs/2.1.4.RELEASE/reference/htmlsingle/)
dubbo
mybatis3
mybatis-generator

  • Spring

    Spring 是一个开源框架,是于 2003 年兴起的一个轻量级的 Java 开发框架,由 Rod Johnson 在其著作《Expert One-On-One J2EE Development and Design》中阐述的部分理念和原型衍生而来。它是为了解决企业应用开发的复杂性而创建的。框架的主要优势之一就是其分层架构,分层架构允许使用者选择使用哪一个组件,同时为 JavaEE 应用程序开发提供集成的框架。

    949 引用 • 1460 回帖
  • Dubbo

    Dubbo 是一个分布式服务框架,致力于提供高性能和透明化的 RPC 远程服务调用方案,是 [阿里巴巴] SOA 服务化治理方案的核心框架,每天为 2,000+ 个服务提供 3,000,000,000+ 次访问量支持,并被广泛应用于阿里巴巴集团的各成员站点。

    60 引用 • 82 回帖 • 615 关注
  • MyBatis

    MyBatis 本是 Apache 软件基金会 的一个开源项目 iBatis,2010 年这个项目由 Apache 软件基金会迁移到了 google code,并且改名为 MyBatis ,2013 年 11 月再次迁移到了 GitHub。

    173 引用 • 414 回帖 • 364 关注

相关帖子

欢迎来到这里!

我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。

注册 关于
请输入回帖内容 ...