OfferCampus前期构建简单介绍: 搭建完整的模板Spring Cloud项目
基于SpringCloud搭建模板项目 ----- financeCampus实例
Cfeng在构建OfferCampus项目时,最开始选用Dubbo搭建该微服务项目,最终落入单体陷阱: 微服务之间的依赖过多,形成了一个分崩离析的单体项目,效果甚至不如单体项目; 不符合微服务项目的设计理念,因此选用Spring Cloud完整的生态进行微服务项目的构建
这里只是会简单的提及各个组件的功能和相关的使用,搭建一个微服务项目的模板供以后的服务搭建使用【 当然每个微服务都会采用Middleware进行性能提升】
本篇文章只是简单介绍几个cfeng认为重要的点,大部分简略带过 – 该项目Cfeng也没有完全深入做,只是以此来分析创建一个微服务项目所需要做的工作,像核心的业务逻辑则是每个项目具体分析
该项目如果有机会会深入,但是目前来看技术含量不大,但是代码的逻辑还是庞大,因此这里只是简单引入一下项目,了解微服务项目架构的基本的技术
而offerCampus项目将会深入,同时将会增加一些个性化功能方便Cfeng自身的使用
这里的项目拆分方法采用的垂直拆分,没有按照业务拆分, 这里拆分为common、service-base、service-core; core —》 base —》 common
但是像sms和oss等还是还是按照的业务方式,整个脚手架项目的微服务移动就只有一个核心的服务和对象存储、sms服务, 而Cfeng经手的OfferCapmus项目是按照业务划分多个微服务
这里以Cloud搭建一个通用的后端项目,当然具体的业务逻辑需要具体分析,但是架构的代码其实是类似的
选择Alibaba的Sentinel做服务降级和监控、nacos做注册和配置中心,使用redis、rabbitMQ作为分布式缓存和消息件,使用mysql、mongoDB数据库,使用minIO做服务文件对象存储、同时使用短信服务 【部署运维采用docker】
SpringBoot的版本选择:SpringBoot 2.4 + 与之下的版本变化比较大【比如security配置完全变化,但是可以理解】
目前,单机项目Cfeng 采用的是2.7.2的SpringBoot版本【本地仓库Cfeng有N个版本】
而微服务Cfeng预备采用2.6.3版本,对应的SpringCloud会采用2021.0.1,对应的Alibaba的版本也就是2021.0.1.x
而2021.0.1.0版本的Alibaba对应的组件: Sentinel — 1.8.3, Nacos— 1.4.2, rocketMQ — 4.9.2, Seata ---- 1.4.2
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-alibaba-dependenciesartifactId>
<version>2021.0.1.0version>
<type>pomtype>
<scope>importscope>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-dependenciesartifactId>
<version>2021.0.0version>
<type>pomtype>
<scope>importscope>
dependency>
金融借贷中介平台构建 — 服务人群分为: 投资者、第三方托管平台、借款人(托管的资金池 ---- 银行就是一个资金池)
资金有风险,如果资金池长期入量大,出量少,资金池有成本(付利息给投资者),形成庞氏骗局,中介可能会倒闭; 资金出量主要是找项目和风控,(eg:基金、股票,可能一下子就赔了),可能导致资金不能流回; 提款出水量变大 — 一个黑天鹅事件挤兑,可能导致投资者疯狂回收,导致平台垮掉; 还有中介平台把资金池钱取走、跑路…
所以资金第三方存管需要认清资质,第三方托管平台一般都是银行(风控体系完善、国家背书,不跑路)
financeCpmpus主要就是服务投资者和借款人的一个金融中介平台,涉及资金的流动,所以项目要求高安全性
项目技术选型: 主要就是上面那套模板,SpringBoot,数据库ORM采用mybatis-plus(JPA也好用,但是可控性没有plus好,实际项目中使用少,但是Cfeng个人项目经常使用)
该项目主要是3个微服务
微服务一般情况下还是建立一个父级项目进行统一的管理,这里就直接新建一个maven项目,删除src和其他无关的文件夹(just for 整洁)
然后就是统一配置管理项目的公共依赖
<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.0modelVersion>
<groupId>indv.cfenggroupId>
<artifactId>fianceCampusartifactId>
<version>1.0.0version>
<properties>
<spring-boot.version>2.6.3spring-boot.version>
<spring-cloud.version>2021.0.0spring-cloud.version>
<spring-cloud-alibaba.version>2021.0.1.0spring-cloud-alibaba.version>
<java.version>17java.version>
<project.build.sourceEncoding>UTF-8project.build.sourceEncoding>
<maven.compiler.source>${java.version}maven.compiler.source>
<maven.conpiler.target>${java.version}maven.conpiler.target>
<mybatis-plus.version>3.4.1mybatis-plus.version>
<velocity.version>2.3velocity.version>
<swagger.version>2.9.2swagger.version>
<swagger-bootstrap-ui.version>1.9.2swagger-bootstrap-ui.version>
<commons-lang3.version>3.9commons-lang3.version>
<commons-fileupload.version>1.3.1commons-fileupload.version>
<commons-io.version>2.6commons-io.version>
<alibaba.easyexcel.version>3.0.5alibaba.easyexcel.version>
<apache.xmlbeans.version>3.1.0apache.xmlbeans.version>
<fastjson.version>1.2.28fastjson.version>
<gson.version>2.8.2gson.version>
<json.version>20170516json.version>
<aliyun-java-sdk-core.version>4.3.3aliyun-java-sdk-core.version>
<aliyun-sdk-oss.version>3.10.2aliyun-sdk-oss.version>
<minIO.version>1.0.0minIO.version>
<jodatime.version>2.10.1jodatime.version>
<jwt.version>0.7.0jwt.version>
<httpclient.version>4.5.1httpclient.version>
<freemaker.version>2.3.28freemaker.version>
<guava.version>31.1-jreguava.version>
properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-dependenciesartifactId>
<version>${spring-boot.version}version>
<type>pomtype>
<scope>importscope>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-dependenciesartifactId>
<version>${spring-cloud.version}version>
<type>pomtype>
<scope>importscope>
dependency>
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-alibaba-dependenciesartifactId>
<version>${spring-cloud-alibaba.version}version>
<type>pomtype>
<scope>importscope>
dependency>
<dependency>
<groupId>com.baomidougroupId>
<artifactId>mybatis-plus-boot-starterartifactId>
<version>${mybatis-plus.version}version>
dependency>
<dependency>
<groupId>com.baomidougroupId>
<artifactId>mybatis-plus-generatorartifactId>
<version>${mybatis-plus.version}version>
dependency>
<dependency>
<groupId>org.apache.velocitygroupId>
<artifactId>velocity-engine-coreartifactId>
<version>${velocity.version}version>
dependency>
<dependency>
<groupId>io.springfoxgroupId>
<artifactId>springfox-swagger2artifactId>
<version>${swagger.version}version>
dependency>
<dependency>
<groupId>io.springfoxgroupId>
<artifactId>springfox-swagger-uiartifactId>
<version>${swagger.version}version>
dependency>
<dependency>
<groupId>com.github.xiaoymingroupId>
<artifactId>swagger-bootstrap-uiartifactId>
<version>${swagger-bootstrap-ui.version}version>
dependency>
<dependency>
<groupId>org.apache.commonsgroupId>
<artifactId>commons-lang3artifactId>
<version>${commons-lang3.version}version>
dependency>
<dependency>
<groupId>commons-fileuploadgroupId>
<artifactId>commons-fileuploadartifactId>
<version>${commons-fileupload.version}version>
dependency>
<dependency>
<groupId>commons-iogroupId>
<artifactId>commons-ioartifactId>
<version>${commons-io.version}version>
dependency>
<dependency>
<groupId>com.alibabagroupId>
<artifactId>easyexcelartifactId>
<version>${alibaba.easyexcel.version}version>
dependency>
<dependency>
<groupId>org.apache.xmlbeansgroupId>
<artifactId>xmlbeansartifactId>
<version>${apache.xmlbeans.version}version>
dependency>
<dependency>
<groupId>com.alibabagroupId>
<artifactId>fastjsonartifactId>
<version>${fastjson.version}version>
dependency>
<dependency>
<groupId>org.jsongroupId>
<artifactId>jsonartifactId>
<version>${json.version}version>
dependency>
<dependency>
<groupId>com.google.code.gsongroupId>
<artifactId>gsonartifactId>
<version>${gson.version}version>
dependency>
<dependency>
<groupId>com.aliyungroupId>
<artifactId>aliyun-java-sdk-coreartifactId>
<version>${aliyun-java-sdk-core.version}version>
dependency>
<dependency>
<groupId>com.aliyun.ossgroupId>
<artifactId>aliyun-sdk-ossartifactId>
<version>${aliyun-sdk-oss.version}version>
dependency>
<dependency>
<groupId>com.indCfenggroupId>
<artifactId>minio-spring-boot-starterartifactId>
<version>${minIO.version}version>
dependency>
<dependency>
<groupId>joda-timegroupId>
<artifactId>joda-timeartifactId>
<version>${jodatime.version}version>
dependency>
<dependency>
<groupId>io.jsonwebtokengroupId>
<artifactId>jjwtartifactId>
<version>${jwt.version}version>
dependency>
<dependency>
<groupId>org.apache.httpcomponentsgroupId>
<artifactId>httpclientartifactId>
<version>${httpclient.version}version>
dependency>
<dependency>
<groupId>org.freemarkergroupId>
<artifactId>freemarkerartifactId>
<version>${freemaker.version}version>
dependency>
<dependency>
<groupId>com.google.guavagroupId>
<artifactId>guavaartifactId>
<version>${guava.version}version>
dependency>
dependencies>
dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-testartifactId>
<scope>testscope>
dependency>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
dependency>
dependencies>
project>
这里只说明一点,那就是DenpendencyManagement中的依赖主要是供版本号管理,子项目按需导入时,不需要再写版本号,和denpendencies不同,其中的依赖不会自动加入到子项目中
父项目的主要作用就是组织依赖version管理,dependencies中的依赖会自动加入每一个子项目,而management中的只是为声明version, 子项目导入依赖都不需要写version
注意微服务项目要尽可能减少耦合,所以这里的finance中不是为了像dubbo一样接口工程存放实体类和… , 微服务项目中确实有需要共享的类型 ------ 项目通用version【为了防止各自定义不同,比如Resp】
但是一般的实体类和其余的java类都不要耦合,避免出现一致和不一致的问题
定义统一响应类型Resp,统一的异常处理【定义在common模块中的注解要想被扫描,必须使用@ComponentScan进行指定位置】 异常分为到达处理器前的异常,和之后的异常
该模块是垂直拆分的,下属模块为公共模块,又支撑service-core模块, 该模块定义swagger的配置
core模块为项目的核心业务逻辑,和service-base一起组成最核心的业务微服务,调用其他的微服务
java领域中解析、生成Excel的一个开源的框架,相比其余的产品,主要的特点为使用简单、节省内存,采用按行解析模式,将一行的结果解析通过观察者模式通知处理AnalysisEventListener
<dependency>
<groupId>com.alibabagroupId>
<artifactId>easyexcelartifactId>
<version>2.1.7version>
dependency>
<dependency>
<groupId>org.apache.xmlbeansgroupId>
<artifactId>xmlbeansartifactId>
<version>3.1.0version>
dependency>
创建对应的写入excel表的实体类,直接@Data, 在需要作为字段名的属性使用@ExcelProperty
指定即可,比如@ExcelProperty(“姓名”)
而写入数据进入excel很简单,直接先新建.xlsx文件,之后使用EasyExcel.write方法指定fileName和该excel对应的实体类类型
EasyExcel.write(fileName, ExcelStudentDTO.class).excelType(ExcelTypeEnum.XLS).sheet("模板").doWrite(data());
EasyExcel.write(fileName, ExcelStudentDTO.class).sheet("模板").doWrite(data());
}
//辅助方法
private List<ExcelStudentDTO> data(){
List<ExcelStudentDTO> list = new ArrayList<>();
//算上标题,做多可写65536行
//超出:java.lang.IllegalArgumentException: Invalid row number (65536) outside allowable range (0..65535)
for (int i = 0; i < 65535; i++) {
ExcelStudentDTO data = new ExcelStudentDTO();
data.setName("Helen" + i);
data.setBirthday(new Date());
data.setSalary(123456.1234);
list.add(data);
}
return list;
}
xls版本的Excel一次性最多写0…65535行, xlsx版本最多1048575
读取数据直接创建监听器,继承AnalysisEventListener,监听的类型为对用的实体类类型,invoke中就会监听到数据,doAfterAllAnalysed就是所有的数据完成后执行
public class ExcelDictDtoListener extends AnalysisEventListener<ExcelDictDto> {
/**
* 每间隔5条存储数据库,可以加大为3000条,之后清理list,进行内存回收
*/
private static final int BATCH_COUNT = 5;
List<ExcelDictDto> list = new ArrayList<>();
private DictMapper dictMapper;
//构造器注入dictMapper
public ExcelDictDtoListener(DictMapper dictMapper) {
this.dictMapper = dictMapper;
}
/**
* 按行读取记录
*/
@Override
public void invoke(ExcelDictDto excelDictDto, AnalysisContext analysisContext) {
log.info("解析到一条记录:{}",excelDictDto);
list.add(excelDictDto);
//每次解析一条数据之后,就检查是否达到边界Batch,如果超过就要清理内存
if(list.size() >= BATCH_COUNT) {
//存数据
this.saveData();
//清理空间
list.clear();
}
}
/**
* 所有数据读取完成
*/
@Override
public void doAfterAllAnalysed(AnalysisContext analysisContext) {
//需要持久化到数据库,确保内存中的数据都是持久化成功
this.saveData();
log.info("所有数据解析完成");
}
/**
* 持久化
*/
private void saveData() {
log.info("本次持久化{}条数据",list.size());
dictMapper.insertBatch(list);
log.info("批量持久化成功");
}
}
之后调用EasyExcel.read方法进行读取
EasyExcel.read(fileName, ExcelStudentDTO.class, new ExcelStudentDTOListener()).sheet().doRead();
数据字典负责管理系统常用的分类数据或者固定的数据【省市区三级级联查询、民族、学历】, 主要是为了管理相关的通用的数据
CREATE TABLE `dict` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'id',
`parent_id` bigint(20) NOT NULL DEFAULT 0 COMMENT '上级id',
`name` varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL DEFAULT '' COMMENT '名称',
`value` int(11) NULL DEFAULT NULL COMMENT '值',
`dict_code` varchar(20) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '编码',
`create_time` timestamp(0) NOT NULL DEFAULT CURRENT_TIMESTAMP(0) COMMENT '创建时间',
`update_time` timestamp(0) NOT NULL DEFAULT CURRENT_TIMESTAMP(0) ON UPDATE CURRENT_TIMESTAMP(0) COMMENT '更新时间',
`is_deleted` tinyint(1) NOT NULL DEFAULT 0 COMMENT '删除标记(0:不可用 1:可用)',
PRIMARY KEY (`id`) USING BTREE,
UNIQUE INDEX `uk_parent_id_value`(`parent_id`, `value`) USING BTREE,
INDEX `idx_parent_id`(`parent_id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 82008 CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '数据字典' ROW_FORMAT = DYNAMIC;
通过id和parent_id构建上下级关系, name就是名称【填写用户信息,select选择民族,汉族就是名称】,value【比如用1代表汉族】,dict_code就是编码,全局唯一,比如学历数据education、收入来源数据returnSource, 通过数据字典配合EasyExcel就可以实现数据的导入和显示
但是在微服务项目中,要想正确完成令牌验证,只能使用Session共享 ----- 搭建redis集群作为session集群
但是这里的缺点就是因为进行统一的认证,所以认证服务器的压力较大
JWT令牌 — JSON WEB Token 自包含令牌, 使用在webapi、web服务器无状态分布式身份验证
JWT最重要的功能就是token防伪, 一个JWT由Headr、payload、签名哈希 组成,之后base64编码得到jwt【通过.分隔为3个字串】
JET头时是描述元数据的JSON对象,比如alg 代表签名算法,默认HS256,typ表示令牌类型,比如JWT
payload就是内容部分,也是json对象,包含传递的对象,指定七个默认字段: sub主题、iss签发者、aud接收方、iat签发时间、exp过期时间、nbf什么时间之前jwt不可用、jti唯一的身份表示,一次性token,避免重放
还可以自定义数据,比如name、password…
JWT默认不加密,任何人可以解读,所以不要构建私密信息的字段,防止信息泄露
签名HASH就是利用alg生成hash进行签名,数据不会被篡改,相当于类似之前的
JWT - test使用
<dependency>
<groupId>io.jsonwebtokengroupId>
<artifactId>jjwtartifactId>
<version>0.7.0version>
dependency>
使用JWT很简单,通过JWTs的builder创建一个JWT的token,set指定header和相关的参数
public class JwtTests {
//设置token的过期时间
private static long tokenExpiration = 24 * 60 * 60 * 1000;
//密钥
private static String tokenSignKey = "wZ0jH0";
@Test
public void testCreateJwtToken() {
String token = Jwts.builder()
.setHeaderParam("typ","JWT") //令牌类型
.setHeaderParam("alg", "HS256") //签名算法
.setSubject("sys-user") //令牌主题
.setIssuer("cfeng") //签发者
.setAudience("cfeng") //接收者
.setIssuedAt(new Date())
.setExpiration(new Date(System.currentTimeMillis() + tokenExpiration)) //过期时间
.setNotBefore(new Date(System.currentTimeMillis() + 20 * 1000)) //20s后才可用
.setId(UUID.randomUUID().toString()) //令牌的唯一标识
//自定义负载
.claim("nickname", "java") //昵称
.claim("avatar", "1.jpg") //头像
.signWith(SignatureAlgorithm.HS256,tokenSignKey) //使用密钥按照HS256加密
.compact(); //转为字符串,生成了token
System.out.println(token);
}
/**
* 解析token
*/
@Test
public void testGetTokenInfo() {
String token = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJzeXMtdXNlciIsImlzcyI6ImNmZW5nIiwiYXVkIjoiY2ZlbmciLCJpYXQiOjE2NjcyNjkxMzYsImV4cCI6MTY2NzM1NTUzNiwibmJmIjoxNjY3MjY5MTU2LCJqdGkiOiJkZGNlZGFkZS02MWE2LTQ2ZTEtYjBiMC0yNTM0OTZkNGI5MWIiLCJuaWNrbmFtZSI6ImphdmEiLCJhdmF0YXIiOiIxLmpwZyJ9.ycXa-6z4YsW3Jwy45cj6EfyQmyfo7fX6_8y-WM1LVgQ";
//解析JWT
Jws<Claims> claimsJws = Jwts.parser().setSigningKey(tokenSignKey).parseClaimsJws(token);
//获取负载payload
Claims claims = claimsJws.getBody();
//该claims就是一个集合,从中获取即可
String subject = claims.getSubject();
String issuer = claims.getIssuer();
String audience = claims.getAudience();
Date issueAt = claims.getIssuedAt();
Date expiration = claims.getExpiration();
Date notBefore = claims.getNotBefore();
String id = claims.getId();
String nickname = (String) claims.get("nickname");
System.out.println(subject + "," + issuer + "," + audience + "," + issueAt + "," + expiration + "," + notBefore + "," + id + "," + nickname);
}
}
生成Token的时候就是利用Jwts.builder之后set注入相关的header、payload,同时指定加密的alg和signKey
而解析jwt令牌则是使用Jwts.parses指定signKey之后转化为jwtClaims即可,该claims的body就是payload,解析出相关的参数即可
使用JWT格式的token可以让token更加规范化,使用简单的时间戳可能存在安全问题,JWT加密之后可以防止篡改
这里以QQ邮箱为例,要开通SMTP服务,生成一个对应的权限码
引入mail的依赖
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-mailartifactId>
dependency>
配置mail的yml配置项
spring:
mail:
host: smtp.qq.com #平台地址,这里使用的qq邮箱
username: 158xxxxxxxx9@qq.com #发送邮件的邮箱
password: dldgyXXXXXXch #发送的校验码,邮箱密码
default-encoding: UTF-8
properties:
mail:
smtp:
ssl:
enabled: true
之后就可以利用自动配置的mailSender对象send邮件,普通邮件SimpleMessage,带附件邮件MimeMessage, 普通邮件直接new之后set注入,而附件邮件则是通过sender构建,并且使用MimeHelper进行set注入
@SpringBootTest
public class MailTest {
//邮件服务发送
@Resource
private JavaMailSender mailSender;
@Test
public void sendMail() {
try {
SimpleMailMessage mailMessage = new SimpleMailMessage();
mailMessage.setSubject("验证码邮件"); //主题
String code = "545345";
mailMessage.setText("你的验证码是: " + code);
mailMessage.setFrom("1XXXX8XXX79@qq.com"); //发送方
mailMessage.setTo("2XXXXX89@qq.com"); //发给谁
System.out.println("邮件发送成功");
mailSender.send(mailMessage);
} catch (Exception e) {
e.printStackTrace();
}
}
@Test
public void sendAttachmentMail() {
File file = new File("D:\\TransferStation\\zjn.pdf");
//附件文件对象FileSystemResource读取成文件资源
FileSystemResource resource = new FileSystemResource(file);
//通过sender自动对象创建MimeMessage,和SimpMessage不同
MimeMessage mimeMessage = mailSender.createMimeMessage();
//使用mimeMessageHelper封装附件形式的邮件
try {
//multipart多文件
MimeMessageHelper helper = new MimeMessageHelper(mimeMessage,true);
helper.setSubject("请查收zjn的简历");
helper.setText("你的验证码是: 8987863" + " 查看附件的简历");
helper.setFrom("15XXXX2749@qq.com");
helper.setTo("2326XXX289@qq.com");
helper.addAttachment("zjn简历",resource);
} catch (MessagingException e) {
e.printStackTrace();
}
mailSender.send(mimeMessage);
}
}