1 二次开发现状分析
1.1 目前二次开发的痛点
过于依赖Ecology的JDK及Jar包,对于JDK及新版本工具jar包的使用具有局限性。
模块耦合度太高,代码关联性太强,变量修改容易造成全局污染,class代码升级需要停ecology服务,正式环境影响用户使用,测试环境影响项目及开发的工作效率。
二次开发的代码和标准产品代码的粘合度太高,无法提取独立的二次开发代码,不利于知识沉淀和后期复用,之前我们的代码复用大部分是类级复用而非模块级复用。
问题排查困难,由于代码和标准产品代码掺杂在一起,日志无法切割,排查困难。
对比产品开发,二次开发往往未经过针对性的压力或系统测试,二次开发引发的宕机往往会波及整个EC服务。
1.2 微服务的优势
不依赖于Ecology,JDK、Jar包版本无限制,利用Maven导入Java依赖,易于和第三方应用集成,支持使用不同的开发语言,允许整合最新的技术。
微服务是松耦合的,无论是开发阶段还是部署阶段都是独立的。
能够快速响应,局部修改容易,一个服务出现问题不会影响整个应用。
Springboot有成熟的整合方案,对于主流的开发框架无配置集成。
内嵌Servlet容器,无需单独安装容器即可独立运行项目(默认是使用tomcat,整体以jar包的形式运行)。
利用SpringBoot整合Mybatis实现Sql统一管理和优化,简化Dao层代码编写,同时支持SQL热部署。
微服务可以使不同的团队专注于更小范围的工作职责、使用独立的技术、更安全更频繁地部署。
1.3 待解决问题
微服务代码版本管理及备案问题
微服务代码部署问题(每次都打一个比较大的jar包)
微服务环境启动问题(每次需要启动很多个应用服务和基础设施服务,初步考虑单独写启动脚本)
Ecology的无侵入整合。
2 二次开发微服务架构说明
2.1 微服务二次开发架构图
必读:二次开发的微服务架构因项目而异,对于大型的项目可以按照功能模块拆分每个服务,只要涉及到多个微服务的情况必须搭建nacos和SpringCloudGetWay基础设施来保障EC到微服务、微服务到微服务之前的通信,反之项目体量不大、需求量不多则可以考虑抛弃nacos和springCloudGetWay(可以跳过本文章的2.4\2.5\2.6\2.7),只部署单体的SpringBoot服务即可。
[图片上传失败...(image-a737a4-1593312061480)]
2.2 后端代码结构分层
[图片上传失败...(image-4680b4-1593312061480)]
2.2.1 结构****说明
|
包名
|
说明
|
|
entity
|
实体类层。与需要操作的表结构一致,生成set和get方法,使用Lomok减少大量的模板代码(IDE需要单独安装LomokPlugin)。(必须)
|
|
dao
|
数据库操作层。每个数据表的操作放在一个dao的中。Dao层的每个方法完成一个完整的数据库操作;如果操作包含多条sql,需要放在一个方法中,可以根据实际情况决定是否采用“事务”处理,该层只有方法名,方法名和Mapper.xml做一一对应。(必须)
|
|
service
|
业务服务层,调用Dao层。可以通过调用多个Dao类,组合业务操作的结果。Service层提供“方法代理”。(必须)
|
|
controller
|
控制层,用于和EC或View层通信。(必须)
|
|
aop
|
切面层,用于定义切面类,方便针对于二次开发代码进行无侵入修改。(非必须)
|
|
util
|
工具类。非必须,如果有需要独立编写的工具类,需要放在此处。(非必须)
|
2.3 构建第一个SpringBoot工程
2.3.1 创建工程
直接在Idea中Create New Project --> Spring Initializr --> 填写group、artifact -->钩上SpringWeb(SpringBoot版本建议选择2.3.0) --> 点下一步
[图片上传失败...(image-fc9aca-1593312061480)]
[图片上传失败...(image-e06e54-1593312061480)]
[图片上传失败...(image-5220ff-1593312061480)]
填写”ProjectName”后点击”Finish”,完成项目创建
[图片上传失败...(image-a78128-1593312061480)]
2.3.2 工程目录结构
|
-src
-main
-java
-package
#主函数,启动类,运行它如果运行了 Tomcat、Jetty、Undertow 等容器
-SpringbootApplication
-resouces
#存放静态资源 js/css/images 等
- statics
#存放 html 模板文件
- templates
#主要的配置文件,SpringBoot启动时候会自动加载application.yml/application.properties
- application.properties
#测试文件存放目录
-test
pom.xml 文件是Maven构建的基础,里面包含了我们所依赖JAR和Plugin的信息
- pom.xml
|
2.3.3 调整工程目录
在src目录下新建包com.weavernorth.模块名
将IDE生成的SpringbootApplication.java文件放置在模块包的根目录下(注:由于Controller层、Dao层要和启动类有共同的父包,否则会出现扫描不到的问题,如果启动类和 controller 没有共同的父包,则需要在启动上增加@ComponentScan注解)
File->Project Structure->Modules将src设置为”Sources”,删除src/main下面的java文件夹。
[图片上传失败...(image-7079b8-1593312061480)]
修改application.properties后缀,改为application.yml。
在新建的“com.weavernorth.模块名”包下按照<u>“后端代码分层</u><u>”</u><u>(2.2)</u>创建java分层路径,最终效果如下:
[图片上传失败...(image-9599d2-1593312061479)]
2.3.4 修改maven配置
IEDA中, File--->Settings--->Maven
查看当前的settings.xml文件路径。
[图片上传失败...(image-82ab57-1593312061479)]
打开settings.xml文件,将下图的maven源配置进settings.xml文件中
<mirrors>
<mirror>
<id>nexus-aliyun</id>
<mirrorOf>central</mirrorOf>
<name>Nexus aliyun</name>
<url>http://maven.aliyun.com/nexus/content/groups/public</url>
</mirror>
</mirrors>
2.3.5 pom.xml
在创建Springboot工程时自动生成好,不需要更改,以下为示例:
|
<?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/POM/4.0.0) [https://maven.apache.org/xsd/maven-4.0.0.xsd](https://maven.apache.org/xsd/maven-4.0.0.xsd)">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.0.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.weavernorth</groupId>
<artifactId>salary</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>salary</name>
<description>The First SpringBoot Project</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-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
|
2.3.6 创建实体类
com/weavernorth/salary/entity/Salary.java(建议使用Lomok生成setter/getter)
package com.weavernorth.salary.entity;
import org.springframework.stereotype.Component;
/**
- 工资单
*/
@Component
public class Salary {
private int Hrmid;
private String HrmName;
//实发工资
private Double doubleNetSalary;
//发放日期
private String strDate;
public int getHrmid() {
return Hrmid;
}
public void setHrmid(int hrmid) {
Hrmid = hrmid;
}
public String getHrmName() {
return HrmName;
}
public void setHrmName(String hrmName) {
HrmName = hrmName;
}
public Double getDoubleNetSalary() {
return doubleNetSalary;
}
public void setDoubleNetSalary(Double doubleNetSalary) {
this.doubleNetSalary = doubleNetSalary;
}
public String getStrDate() {
return strDate;
}
public void setStrDate(String strDate) {
this.strDate = strDate;
}
}
2.3.7 创建Controller
com/weavernorth/salary/controller/SalaryController.java
package com.weavernorth.salary.controller;
import com.weavernorth.salary.entity.Salary;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping(value = "/api/weavernorth/hrm")
public class SalaryController {
@Autowired
Salary salary;
/**
* 计算工资
* @param hrmid 人员id
* @param NetSalary 实发工资
* @param Date 发放日期
* @return
*/
@RequestMapping(value = "/salary/computeSalary",method= RequestMethod.GET)
public Salary computeSalary(@RequestParam int hrmid,@RequestParam Double NetSalary,@RequestParam String Date){
salary.setHrmid(hrmid);
salary.setDoubleNetSalary(NetSalary);
salary.setStrDate(Date);
return salary;
}
}
2.3.8 Maven打包
1.在IDE最右边的侧栏找到”Maven Projects”
[图片上传失败...(image-2b51f6-1593312061478)]
- 打包步骤clean->重新编译src->install,install成功后会在控制台中显示”BUILD SUCCESS”成功标志,在target目录下会生成一个[artifactId]+[version].jar格式的jar包,此jar包则为我们打包好的微服务应用。
[图片上传失败...(image-78c3fa-1593312061478)]
2.3.9 启动工程
2.3.9.1 本地启动
方法一:直接运行SpringBootFirstAppApplication中的main方法
|
package com.weavernorth.salary;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class SalaryApplication {
public static void main(String[] args) {
SpringApplication.run(SalaryApplication.class, args);
}
}
|
启动类的mian()方法中调用了Spring Boot的SpringApplication.run() 方法来启动一个应用。不同于以往的web应用,Spring Boot应用没有一行xml配置,也没有web.xml文件。
[图片上传失败...(image-2fc4f9-1593312061478)]
方法二:以java -jar的方式启动
maven打包完成后cd到target目录下执行以下命令,jar包名称视自己情况而定
java -jar salary-0.0.1-SNAPSHOT.jar
2.3.9.2 客户生成环境启动(linux)
windows系统启动参考 2.3.9.1
Linux环境:
(1) 确保打包无误后,放到指定目录(目录随意,不一定是lib)下
[图片上传失败...(image-1b1e40-1593312061478)]
(2) 进入打包目录下执行:
nohup java -jar loongson-0.0.1-SNAPSHOT.jar &
[图片上传失败...(image-72321b-1593312061478)]
21530是进程号
(3) 停止服务
运行成功后,查看进程 ps -aux | grep java
[图片上传失败...(image-1ecbea-1593312061478)]
杀掉进程 kill -9 21530(进程号)
2.3.10 访问工程
1.未在application.properties或application.yml设置指定端口号时,springboot应用默认端口为8080。
2.访问http://localhost:8080出现以下页面表示服务启动正常。
[图片上传失败...(image-b2e984-1593312061478)]
- 访问我们刚才发布的rest接口并传入参数(<u>http://localhost:8080/api/weavernorth/hrm/salary/computeSalary?hrmid=12&NetSalary=2000.0&Date=2020-05-02</u>),页面打印我们传入的参数表示接口测试成功。
[图片上传失败...(image-8439b1-1593312061478)]
2.4 SpringBoot整合Mybatis+Druid+log4j(必要)
2.4.1 添加依赖
1.在 pom.xml 中添加mybatis、Druid、sqlserver、log4j依赖
|
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>com.microsoft.sqlserver</groupId>
<artifactId>mssql-jdbc</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>1.3.2</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.1.10</version>
</dependency>
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.17</version>
</dependency>
|
2.4.2 application.yml相关配置
数据库信息
spring:
application:
name: salary
datasource:
url: jdbc:sqlserver://127.0.0.1:1433;DatabaseName=ecology2019
driverClassName: com.microsoft.sqlserver.jdbc.SQLServerDriver
username: sa
password: 123456
#采用Druid连接池
type: com.alibaba.druid.pool.DruidDataSource
log4j
logging:
path: ./log
mybatis
mybatis:
mapper扫描映射文件位置
mapper-locations: classpath:config/mappers/weavernorth//.xml
configuration:
log-impl: org.apache.ibatis.logging.log4j.Log4jImpl
注意:如果将mybatis.mapper-locations配置为classpath:config/mappers//.xml即mapper接口所在的包路径下,而Spring Boot默认只打入java package -> *.java,所以我们需要给pom.xml文件添加以下内容
<build>
<resources>
<resource>
<directory>src/main/resources</directory>
</resource>
<resource>
<directory>src/main/java</directory>
<includes>
<include>**/*.xml</include>
</includes>
<filtering>true</filtering>
</resource>
</resources>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<includeSystemScope>true</includeSystemScope>
</configuration>
</plugin>
</plugins>
</build>
2.4.3 设置log4j配置文件
在src/main/resource文件夹下新建log4j.properties文件复制以下代码
log4j.rootLogger=DEBUG,Console
log4j.appender.Console=org.apache.log4j.ConsoleAppender
log4j.appender.Console.layout=org.apache.log4j.PatternLayout
log4j.appender.Console.layout.ConversionPattern=%d [%t] %-5p [%c] - %m%n
log4j.logger.org.apache=INFO
log4j.logger.com.weavernorth.salary.dao=DEBUG
2.4.4 编码实战
2.4.4.1 需求简介
以Hrmresource为例,我们的目标是使用Mybatis查询出Hrmresource中Id、loginid、sex、lastname字段映射到我们的User对象中,并展示在View层。
2.4.4.2 表结构
[图片上传失败...(image-65ce67-1593312061477)]
2.4.4.3 创建实体类
com/weavernorth/Salary/entity/User.java(建议使用Lomok生成setter/getter)
package com.example.springbootmybatis.entity;
import lombok.Data;
import org.springframework.stereotype.Component;
import javax.persistence.Id;
@Component
@Data
public class User {
/** 用户id */
@Id
private Integer id;
/** 用户名 */
private String loginid;
/** 性别 */
private String sex;
/** 姓名 */
private String lastname;
}
2.4.4.4 创建Dao类
com/weavernorth/Salary/dao/IUserDaoService.java
|
package com.weavernorth.salary.dao;
import com.weavernorth.salary.entity.User;
import org.springframework.stereotype.Repository;
import java.util.List;
@Repository
public interface IUserDaoService {
List<User> findAllUser();
}
|
2.4.4.5 创建Mapper配置文件
创建mapper的xml配置文件,在/src/main/resources/config/mappers/weavernorth(所有的mapper都存放在该目录下,由于前面我们在application.yml中设置了mybatis.mapper-locations属性,springboot会自动注册我们设置路径下的mapper,如果不放到该路径下则mapper无法注册到Mybatis中,****必要)路径下创建Mapper文件:User.xml
Mapper中包含Dao类中所有需要执行的查询语句,命名空间要使用对应Dao类的java路径,sqlID和Dao类中的方法名需要一致。
注意:原则上每一个Dao类对应至少一个mapper文件,mapper的路径分层尽量和Dao类的包功能层一致。
例如****IUserDaoService****类存在于****”****src/com/****weavernorth****/****salary****/dao****”****下,则mapper则创建在****”****src/main/resources****/config/mapper****s****/weavernorth/****salary****”
<?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](http://mybatis.org/dtd/mybatis-3-mapper.dtd)">
<mapper namespace="com.weavernorth.salary.dao.IUserDaoService">
<select id="findAllUser" resultType="com.weavernorth.salary.entity.User" parameterType="string">
SELECT id,loginid,lastname,sex FROM HRMRESOURCE
</select>
</mapper>
2.4.4.6 创建JunitTest类测试
|
package com.weavernorth.salary.dao;
import com.weavernorth.salary.dao.IUserDaoService;
import com.weavernorth.salary.entity.User;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
import java.util.List;
@SpringBootTest
@RunWith(SpringRunner.class)
public class IUserMapperTest {
@Autowired
User user;
@Autowired
IUserDaoService iUserDaoService;
private static final Logger logger = LoggerFactory.getLogger(IUserMapperTest.class);
@Test
public void findAllUsers(){
List<User> users=iUserDaoService.findAllUser();
logger.info(users.toString());
}
}
|
2.4.4.7 执行结果查看
运行IUserMapperTest.java,查看Console中打印以下日志代表MyBatis整合成功。
[图片上传失败...(image-13ee3a-1593312061476)]
2.4.5 Mapper.xml的属性及标签说明
2.4.5.1 常见属性
|
属性
|
作用
|
|
namespace
|
Dao类对应接口的路径
|
|
id
|
表示此段sql执行语句的唯一标识,也是接口的方法名称【必须一致才能找到方法】
|
|
parameterType
|
表示该sql语句中需要传入的参数, 类型要与对应的接口方法的类型一致【可选】
|
|
resultMap
|
定义出参,调用已定义的映射管理器的id值
|
|
resultType
|
定义出参,匹配普通Java类型或自定义的pojo【出参类型若不指定,将为语句类型默认类型,如语句返回值为int】
|
2.4.5.2 ${}和#{}的区别
{}会自动在你要插入字段两端 加上引号。例如:你写的是order by #{username},传的是 liujun,那么会解析成order by “liujun”。
{user_id},如果传入的值是111,那么解析成sql时的值为order by 111 如果传入的值是id,则解析成的sql为order by id.
{}: 解析为一个 JDBC 预编译语句(prepared statement)的参数标记符,一个 #{ } 被解析为一个参数占位符 。
2.4.5.3 <sql>标签
该标签主要定义复用的sql语句片段,在执行的sql语句标签直接引用即可。可以提高编码效率、简化代码和提高可读性。
需要配置id熟悉,表示该sql片段的唯一标识。
引用:通过<include refid=" " />标签引用,refid的值就是<sql>的id属性的值。
<sql id="Base_Column_List">
id, question, answer
</sql>
<select id="selectByPrimaryKey" parameterType="java.lang.Long" resultMap="BaseResultMap">
select
**<include** refid="Base_Column_List" **/>**
from java
where id = #{id,jdbcType=BIGINT}
</select>
在<sql>中也可以include其他的<sql>标签,例如:
<sql id="sometable">
${prefix}Table
</sql>
<sql id="someinclude">
from
**<include** refid="${include_target}"**/>**
</sql>
<select id="select" resultType="map">
select
field1, field2, field3
**<include** refid="someinclude"**>**
**<property** name="prefix" value="Some"**/>**
**<property** name="include_target" value="sometable"**/>**
**</include>**
</select>
2.4.5.4 <where>和<if>标签
<where> : 主要用来替换sql语句中的where字段,他的作用主要是用来简化sql语句中where条件判断的书写的
<if>:条件判断标签,配置属性test=" 条件字符串 ",判断是否满足条件,满足则执行,不满足则跳过。
如果当id值为空时,此时打印的sql应是:select * from hrmresource where name=“xx” and age=“xx”
where 标签会自动将其后第一个条件的and或者是or给忽略掉
<select id="selectByParams" parameterType="map" resultType="user">
select * from hrmresource
<where>
<if test="id != null ">id=#{id}</if>
<if test="name != null and name.length()>0" >and name=#{name}</if>
<if test="age != null and age.length()>0">and age = #{age}</if>
</where>
</select>
2.4.5.5 <set>标签
<set> : 主要用来替换sql语句中的set字段,一般在update中使用。
在下述的代码片段当中,假如说现在三个字段都有值得话,那么上面打印的SQL语句如下:
update hrmresource set name=‘xxx’ , age=‘xx’ where id=‘x’
在上面age="xx"的后是没有逗号的,也就是说set标记已经自动帮助我们把最后一个逗号给去掉了
set 标记会自动将其后第一个条件后的逗号忽略掉
<update>
update hrmresource
<set>
**<if** test="name != null and name.length()>0"**>**name = #{name},**</if>**
**<if** test="age != null and age .length()>0"**>**age = #{age },**</if>**
</set>
where id = #{id}
</update>
2.4.5.6 <trim>标签
<trim> : 是一个格式化的标记,可以完成set或者是where标记的功能。
示例1:
select * from hrmresource
<trim prefix="WHERE" prefixoverride="AND |OR">
<if test="name != null and name.length()>0"> AND name=#{name}</if>
<if test="age != null and age.length()>0"> AND age=#{age}</if>
</trim>
假如说name和age的值都不为null的话打印的SQL为:select * from hrmresource where name = ‘xx’ and age = ‘xx’
在where的后面是不存在第一个and的,上面两个属性的意思如下:
prefix:前缀
prefixoverride:去掉第一个and或者是or
示例2:
update hrmresource
<trim prefix="set" suffixoverride="," suffix=" where id = #{id} ">
<if test="name != null and name.length()>0"> name=#{name} , </if>
<if test="age!= null and age.length()>0"> age=#{age} , </if>
</trim>
假如说name和age的值都不为null的话打印的SQL为:update hrmresource set name=‘xx’ , age=‘xx’ where id=‘x’
在age='xx’的后面不存在逗号,而且自动加了一个set前缀和where后缀,上面三个属性的意义如下,其中prefix意义如上:
suffixoverride:去掉最后一个逗号(也可以是其他的标记,就像是上面前缀中的and一样)
suffix:后缀
2.4.5.7 <choose>标签
<choose> : choose标签是按顺序判断其内部when标签中的test条件出否成立,如果有一个成立,则 choose 结束。当 choose 中所有 when 的条件都不满则时,则执行 otherwise 中的sql。类似于Java 的 switch 语句,choose 为 switch,when 为 case,otherwise 则为 default。
<select id="selectByParams" parameterType="map" resultType="user">
select * from hrmresource where 1 = 1
<choose>
**<when** test="id !=null "**>**
AND id = #{id}
**</when** **>**
**<when** test="username != null and username != '' "**>**
AND username = #{username}
**</when** **>**
**<when** test="age != null and age !=''"**>**
AND age = #{age}
**</when** **>**
**<otherwise>**
**</otherwise>**
**</choose>**
</select>
2.5 SpringBoot整合Nacos(非必要)
2.5.1 下载Nacos
https://github.com/alibaba/nacos/releases
[图片上传失败...(image-8eeae3-1593312061473)]
Linux版本: nacos-server-1.2.1.tar.gz
Windows版本: nacos-server-1.2.1.zip
2.5.2 启动nacos
解压后进入nacos/bin目录,执行startup.cmd
jdk必须是1.8以上,并且是 64位
如果启动成功,Nacos在8848端口绑定管理端Web应用程序,例如在本机运行则入口是http://localhost:8848/nacos/,默认用户名和密码都是nacos。
闪退情况:
右键用编辑器打开startup.cmd
[图片上传失败...(image-92ac45-1593312061473)]
[图片上传失败...(image-ec2f22-1593312061473)]
本地配置的环境变量JAVA_HOME,不能以bin目录结尾。
2.5.3 引入Nacos
本章将描述在nacos中如何注册服务、参数动态配置
2.5.3.1 Nacos配置中心
开发payment-service服务。
2.5.3.1.1 引入依赖
<properties>
<java.version>1.8</java.version>
<spring-cloud.version>Greenwich.SR1</spring-cloud.version>
<alibaba.version>0.9.0.RELEASE</alibaba.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
<version>${alibaba.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
2.5.3.1.2 编写Controller
@RestController
@RefreshScope
public class PaymentController {
@Value("${sleep:0}")
private int sleep;
final static Map<Integer, Balance> balanceMap = new HashMap() {{
put(1, new Balance(1, 10, 1000));
put(2, new Balance(2, 0, 10000));
put(3, new Balance(3, 100, 0));
}
};
@RequestMapping("/pay/balance")
public Balance getBalance(Integer id) {
System.out.println("request: /pay/balance?id=" + id + ", sleep: " + sleep);
if(sleep > 0) {
try {
Thread.sleep(sleep);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
if(id != null && balanceMap.containsKey(id)) {
return balanceMap.get(id);
}
return new Balance(0, 0, 0);
}
}
2.5.3.1.3 实体类
public class Balance {
private int id;
private int diamond;
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public int getDiamond() {
return diamond;
}
public void setDiamond(int diamond) {
this.diamond = diamond;
}
public int getTicket() {
return ticket;
}
public void setTicket(int ticket) {
this.ticket = ticket;
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
private int ticket;
private String message;
public Balance() {
}
public Balance(int id, int diamond, int ticket) {
this(id, diamond, ticket, "OK");
}
public Balance(int id, int diamond, int ticket, String message) {
this.id = id;
this.diamond = diamond;
this.ticket = ticket;
this.message = message;
}
}
2.5.3.1.4 启动类
@SpringBootApplication
public class SalaryApplication {
public static void main(String[] args) {
SpringApplication.run(SalaryApplication.class, args);
}
}
2.5.3.1.5 配置文件
application.yml文件中添加下图信息:
|
spring:
profiles:
active: dev
server:
port: 8082
sleep: 0
|
port:服务的端口号
创建bootstrap.yml文件,内容如下:
其中 server-addr即Nacos的IP和端口
spring: application: name: payment-service cloud: nacos: config: server-addr: 127.0.0.1:8848
Name:服务名称
Server-addr:监听nacos的ip及端口号
2.5.3.1.6 启动服务
日志中见如下信息,代表配置成功
[图片上传失败...(image-cb21a3-1593312061469)]
访问:http://localhost:8082/pay/balance?id=1
[图片上传失败...(image-59b850-1593312061469)]
2.5.3.1.7 创建nacos配置项动态修改配置信息
访问nacos:http://localhost:8848/nacos
[图片上传失败...(image-154fad-1593312061469)]
[图片上传失败...(image-22ca19-1593312061469)]
Data ID命名规则:
**application.name**-dev.properties
然后重新调用接口,查看输出日志
[图片上传失败...(image-ada85f-1593312061469)]
然后****将配置改为2000发布后再****调用接口,查看输出日志
[图片上传失败...(image-7d5ccd-1593312061469)]
[图片上传失败...(image-7d7c1f-1593312061469)]
[图片上传失败...(image-aca94b-1593312061469)]
[图片上传失败...(image-50215a-1593312061469)]
2.5.3.2 Nacos注册中心
将payment-service服务注册进nacos
2.5.3.2.1 添加依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
<version>${alibaba.version}</version>
</dependency>
2.5.3.2.2 修改bootstrap.yml
pring:
application:
name: payment-service
cloud:
nacos:
config:
server-addr: 127.0.0.1:8848
discovery:
server-addr: 127.0.0.1:8848
2.5.3.2.3 启动类上添加注解@EnableDiscoveryClient
@SpringBootApplication
@EnableDiscoveryClient
public class SalaryApplication {
public static void main(String[] args) {
SpringApplication.run(SalaryApplication.class, args);
}
}
2.5.3.2.4 启动服务并查看nacos服务列表
重启服务,查看输出日志:
[图片上传失败...(image-60c2f8-1593312061469)]
[图片上传失败...(image-3b5c77-1593312061469)]
2.5.3.2.5 执行mvc clean package把应用打成jar包并在8082和8083端口上启动两个实例
|
java -jar mynacosdemo-0.0.1-SNAPSHOT.jar --server.port=8082
java -jar mynacosdemo-0.0.1-SNAPSHOT.jar --server.port=8083
|
回到Nacos后台查看,可以看到两个实例信息
[图片上传失败...(image-336b81-1593312061469)]
点击详情:
[图片上传失败...(image-c1ddc7-1593312061469)]
停止payment-service服务后,健康实例数变为0
[图片上传失败...(image-1caf32-1593312061469)]
[图片上传失败...(image-731295-1593312061469)]
至此,服务Provider的工作完成
2.6 SpringBoot整合Feign(非必要)
本章将结合具体的应用场景描述如何实现服务之间的相互调用
场景:
payment-service服务(参照2.5.3.2):
此服务中存储了每个人对应的账户余额。
account-service服务:
此服务中存储了人员信息。
在调用account-service服务的接口时,要同时获取人员信息及每个人对应的账户余额。
也就是说,account-service服务要调用payment-service服务的接口(使用Feign)。
2.6.1 account-service服务
新建名称为account-service的SpringBoot应用,配置文件与payment-service基本相同,仅修改 以下两项,服务端口号和服务名称:
application.yml
server.port=8081
bootstrap.yml
spring.application.name=account-service
2.6.1.1 添加依赖
<properties>
<java.version>1.8</java.version>
<spring-cloud.version>Greenwich.SR1</spring-cloud.version>
<alibaba.version>0.9.0.RELEASE</alibaba.version>
<spring-cloud-netflix.version>2.1.1.RELEASE</spring-cloud-netflix.version>
<spring-cloud-openfeign.version>2.1.1.RELEASE</spring-cloud-openfeign.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
<version>${alibaba.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
<version>${alibaba.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-openfeign-dependencies</artifactId>
<version>${spring-cloud-openfeign.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
2.6.1.2 编写Controller
@RestController
public class AccountController {
final static Map<Integer, User> userMap = new HashMap() {{
put(1, new User(1, "张三"));
put(2, new User(2, "李四"));
put(3, new User(3, "王五"));
}
};
@Autowired
private BalanceService balanceService;
@RequestMapping("/acc/user")
public User getUser(@RequestParam Integer id) {
if(id != null && userMap.containsKey(id)) {
User user = userMap.get(id);
//service中去调用 另外的服务接口 获取余额
Balance balance = balanceService.getBalance(id);
user.setBalance(balance);
return user;
}
return new User(0, "");
}
}
2.6.1.3 编写service
编写BalanceService接口及实现类
接口:
@FeignClient(name = "payment-service", fallback = BalanceServiceImpl.class)
public interface BalanceService {
@RequestMapping(value = "/pay/balance", method = RequestMethod.GET)
Balance getBalance(@RequestParam("id") Integer id);
}
FeignClient:
name:调用服务的名称
fallback:指定一个实现Feign接口的实现类
RequestMapping****:
value:被调用服务的接口地址
实现类:
@Component
public class BalanceServiceImpl implements BalanceService {
@Override
public Balance getBalance(Integer id) {
return new Balance(0, 0, 0, "降级");
}
}
2.6.1.4 启动account-service服务
浏览器访问:http://localhost:8081/acc/user?id=1
此时在浏览器中可以看到余额信息,余额信息是从payment-service中获取
[图片上传失败...(image-2c96a9-1593312061466)]
查看payment-service服务的输出日志:
[图片上传失败...(image-c0b142-1593312061466)]
2.6.1.5 灾难测试
关闭一个payment-service服务,然后访问:http://localhost:8081/acc/user?id=1
发现服务仍然可用
[图片上传失败...(image-6553e-1593312061466)]
将两个payment-service都关闭,然后访问:http://localhost:8081/acc/user?id=1
发现报错
[图片上传失败...(image-ebc304-1593312061466)]
解决:
在account-service中添加依赖:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
<version>${alibaba.version}</version>
</dependency>
在bootstrap.yml中添加
feign:
sentinel:
enabled: true
打开sentinel对Feign的支持
此时重启account-service服务
payment-service两个服务仍然处于关闭状态
访问:http://localhost:8081/acc/user?id=1
此时发现没有报错,而是返回了默认的余额信息
[图片上传失败...(image-dd63af-1593312061466)]
2.7 SpringBoot整合SpringCloudGetWay(非必要)
Nacos仅仅提供了注册中心实现了单服务的多实例化,如果想要实现负载均衡,那么还要依赖GetWay的路由和分流
2.7.1 首先分不同端口启动两个微服务实例,见2.5.4.5
2.7.2 路由
2.7.2.1 创建新的Spring boot项目
2.7.2.2 添加依赖
注意: Spring boot 版本 和 Spring cloud gatewaty的版本必须保持一致
<properties>
<java.version>1.8</java.version>
<spring-cloud.version>Greenwich.SR1</spring-cloud.version>
<alibaba.version>0.9.0.RELEASE</alibaba.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
<version>2.1.3.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<version>1.4.0.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis-reactive</artifactId>
</dependency>
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
<version>${alibaba.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
<version>${alibaba.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
2.7.2.3 修改bootstrap.yml文件
spring:
application:
name: gateway
cloud:
nacos:
config:
server-addr: 127.0.0.1:8848
discovery:
server-addr: 127.0.0.1:8848
gateway:
discovery:
locator:
enabled: true
routes:
- id: payment-router
uri: lb://payment-service
predicates:
- Path=/pay/**
id: payment-router (值随意,方便记忆并且在所有路由定义中唯一即可)
uri: lb://payment-service
lb://为固定写法,表示开启负载均衡;payment-service即服务在Nacos中注册的名字
predicates:- Path=/pay/** (使用"Path Route Predicate Factory",规则为/pay开头的任意URI)
2.7.2.4 修改application.yml文件
端口号改为8084
spring:
profiles:
active: dev
server:
port: 8084
sleep: 0
2.7.2.5 启动类添加****@EnableDiscoveryClient****注解
@SpringBootApplication
@EnableDiscoveryClient
public class GetawayApplication {
public static void main(String[] args) {
SpringApplication.run(GetawayApplication.class, args);
}
}
2.7.2.6 启动应用测试
启动刚创建的Spring boot项目
浏览器访问http://localhost:8084/pay/balance?id=2
[图片上传失败...(image-19c353-1593312061464)]
请求成功,可以看出请求被分发到了 payment-service1和payment-service2中
同时查看两个payment服务的输出日志,并发起6次请求
可以看出多次请求被平均分配到两个实例上
[图片上传失败...(image-7a42ab-1593312061464)]
2.7.2.7 Path Route Predicate Factory
除了Path Route Predicate Factory,Gateway还支持多种设置方式:
|
类型
|
示例:
|
|
After
|
After=2017-01-20T17:42:47.789-07:00[America/Denver]
|
|
Before
|
Before=2017-01-20T17:42:47.789-07:00[America/Denver]
|
|
Between
|
2017-01-20T17:42:47.789-07:00[America/Denver], 2017-01-21T17:42:47.789-07:00[America/Denver]
|
|
Cookie
|
Cookie=chocolate, ch.p
|
|
Header
|
Header=X-Request-Id, \d+
|
|
Host
|
Host=**.somehost.org
|
|
Method
|
Method=GET
|
|
Path
|
Path=/foo/{segment}
|
|
Query
|
Query=baz
|
|
RemoteAddr
|
RemoteAddr=192.168.1.1/24
|
2.7.3 GetWay结合Nacos实现动态路由
2.7.3.1 创建监听类
Spring Cloud Gateway本身还不支持直接从Nacos动态加载路由配置表,需要自己编写监听器监听配置变化并刷新路由表。
NacosDynamicRouteService.java
|
package com.example.service;
import com.alibaba.fastjson.JSONObject;
import com.alibaba.nacos.api.NacosFactory;
import com.alibaba.nacos.api.config.ConfigService;
import com.alibaba.nacos.api.config.listener.Listener;
import com.alibaba.nacos.api.exception.NacosException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.gateway.event.RefreshRoutesEvent;
import org.springframework.cloud.gateway.route.RouteDefinition;
import org.springframework.cloud.gateway.route.RouteDefinitionWriter;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.ApplicationEventPublisherAware;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Mono;
import javax.annotation.PostConstruct;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Executor;
@Component
public class NacosDynamicRouteService implements ApplicationEventPublisherAware {
private String dataId = "gateway-router";
private String group = "DEFAULT_GROUP";
@Value("${spring.cloud.nacos.config.server-addr}")
private String serverAddr;
@Autowired
private RouteDefinitionWriter routeDefinitionWriter;
private ApplicationEventPublisher applicationEventPublisher;
private static final List<String> ROUTE_LIST = new ArrayList<>();
@PostConstruct
public void dynamicRouteByNacosListener() {
try {
ConfigService configService = NacosFactory.createConfigService(serverAddr);
configService.getConfig(dataId, group, 5000);
configService.addListener(dataId, group, new Listener() {
@Override
public void receiveConfigInfo(String configInfo) {
clearRoute();
try {
List<RouteDefinition> gatewayRouteDefinitions = JSONObject.
parseArray(configInfo, RouteDefinition.class);
for (RouteDefinition routeDefinition : gatewayRouteDefinitions) {
addRoute(routeDefinition);
}
publish();
} catch (Exception e) {
e.printStackTrace();
}
}
@Override
public Executor getExecutor() {
return null;
}
});
} catch (NacosException e) {
e.printStackTrace();
}
}
private void clearRoute() {
for(String id : ROUTE_LIST) {
this.routeDefinitionWriter.delete(Mono.just(id)).subscribe();
}
ROUTE_LIST.clear();
}
private void addRoute(RouteDefinition definition) {
try {
routeDefinitionWriter.save(Mono.just(definition)).subscribe();
ROUTE_LIST.add(definition.getId());
} catch (Exception e) {
e.printStackTrace();
}
}
private void publish() {
this.applicationEventPublisher.publishEvent(new RefreshRoutesEvent(this.routeDefinitionWriter));
}
@Override
public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) {
this.applicationEventPublisher = applicationEventPublisher;
}
}
|
2.7.3.2 创建nacos配置
代码中监听的配置ID为gateway-router,按此ID在Nacos中创建配置
[图片上传失败...(image-6baa7b-1593312061456)]
2.7.3.3 注释bootstrap.yml中的路由配置
从bootstrap.yml中删除路由配置,即删除以下内容
routes:
- id: payment-router
uri: lb://payment-service
predicates:
- Path=/pay/**
2.7.3.4 添加动态路由设置
动态路由功能修改完成,启动gateway测试,目前路由表中仅匹配了/acc/**的,分别测试一下/acc和/pay
[图片上传失败...(image-d7be3b-1593312061464)]
[图片上传失败...(image-36f15f-1593312061464)]
/acc/user请求成功转发到account-service,而/pay/balance没有找到匹配的路由信息,与期望行为一致。
下面来动态增加/pay的路由,修改Nacos中的gateway-router配置如下:
[{
"id": "account-router",
"order": 0,
"predicates": [{
"args": {
"pattern": "/acc/**"
},
"name": "Path"
}],
"uri": "[lb://account-service](lb://account-service)"
},{
"id": "payment-router",
"order": 2,
"predicates": [{
"args": {
"pattern": "/pay/**"
},
"name": "Path"
}],
"uri": "[lb://payment-service](lb://payment-service)"
}]
不重启gateway再次测试/pay/balance请求 ,已经可以达到访问预期效果。
[图片上传失败...(image-78f185-1593312061463)]
2.8 在ecology整合二次开发微服务
2.8.1 说明
原则上所有的ecology和微服务的交互都是通过RestApi
2.8.2 微服务配置平台部署
2.8.2.1 部署微服务配置平台
将“微服务配置平台后端.zip”解压覆盖到ecology目录(如果已经部署过”wn_E9Mybatis.jar”,请使用“微服务配置平台(不含Mybatis.jar)”版本)。
将\WEB-INF\config\mapper\weavernorth\MicoService\MicoService.xml注册到WN_MyBatisConfig.xml中
[图片上传失败...(image-996017-1593312061463)]
重启服务器。
访问ecology->后端应用中心->微服务配置平台->微服务网关注册平台,如果出现以下页面代表部署成功
[图片上传失败...(image-727e16-1593312061463)]
2.8.2.2 使用微服务配置平台
2.8.2.2.1 注册网关
在“微服务网关注册平台”中点击编辑,设置ServiceID、URL、是否启用,URL填写SpringCloudGetWay网关地址或springboot单体应用地址都可以,ServiceID保证唯一。
[图片上传失败...(image-4bde0d-1593312061463)]
2.8.2.2.2 在Action中代码示例
|
import com.alibaba.fastjson.JSONObject;
import com.weavernorth.Developer.MicoServiceUtil;
import weaver.soa.workflow.request.RequestInfo;
import weaver.workflow.action.BaseAction;
public class SalaryPaymentAction extends BaseAction {
@Override
public String execute(RequestInfo requestInfo) {
//获取微服务网关地址
String strMicoServiceURI= MicoServiceUtil.getMicoServiceURI("Salary");
//获取Requestid
String strRequestid=requestInfo.getRequestid();
//调用微服务
JSONObject returnjson=MicoServiceUtil.httpGet(strMicoServiceURI+"/api/weavernorth/hrm/computeSalary?strRequestid="+strRequestid);
if(returnjson.get("flag").equals("success")){
return SUCCESS;
}else{
requestInfo.getRequestManager().setMessage(returnjson.get("message").toString());
return "0";
}
}
}
|
2.8.2.2.3 在JavaScript中代码示例
<script type="text/javascript" src="/weavernorth/developer/MicoServiceUtil.js"></script>
<script>
//微服务地址
var MicoServiceURI="";
$(function(){
MicoServiceURI=getMicoServiceURI("Salary");
})
$.ajax({
type:"get",
url:MicoServiceURI+"/api/weavernorth/hrm/getSalaryReissue,
contentType:"application/json",
dataType:"json",
success:function(jsonarray){
console.log(jsonarray)
}
})
<script>