一. 分布式文件存储-FastDFS
1.FastDFS简介
1.1FastDFS体系结构
FastDFS是一个开源的轻量级分布式文件系统,他对文件进行管理,功能包括:文件存储、文件同步、文件访问,解决来大容量存储和负载均衡的问题,特别适合以文件为载体的在线服务
FastDFS具备冗余备份、负载均衡、线性扩容等机制,并注重高可用,高性能等标注
FastDFS架构包括Tracker server 和 Storage server 客户端请求Tracker server进行文件上传、下载,通过Tracker server调度最终由Storage server 完成文件上传和下载
Tracker server 作用是负载均衡和调度,通过 Tracker server 在文件上传时可以根据一些策略找到Storage server 提供文件上传服务。可以将 tracker 称为追踪服务器或调度服务器。Storage server 作用是文件存储,客户端上传的文件最终存储在 Storage 服务器上,Storageserver 没有实现自己的文件系统而是利用操作系统的文件系统来管理文件。可以将storage称为存储服务器。
1.2上传流程
客户端上传文件后存储服务器将文件 ID 返回给客户端,此文件 ID 用于以后访问该文件的索引信息。文件索引信息包括:组名,虚拟磁盘路径,数据两级目录,文件名。
1.3代码实现
(1)修改pom.xml,引入依赖
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>net.oschina.zcx7878</groupId>
<artifactId>fastdfs-client-java</artifactId>
<version>1.27.0.0</version>
</dependency>
<dependency>
<groupId>com.changgou</groupId>
<artifactId>changgou_common</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
</dependencies>
(2)在resources文件夹下创建fasfDFS的配置文件fdfs_client.conf
connect_timeout = 60
network_timeout = 60
charset = UTF‐8
http.tracker_http_port = 8080
tracker_server = 192.168.200.128:22122
connect_timeout:连接超时时间,单位为秒。
network_timeout:通信超时时间,单位为秒。发送或接收数据时。假设在超时时间后
还不能发送或接收数据,则本次网络通信失败
charset: 字符集
http.tracker_http_port :.tracker的http端口
tracker_server: tracker服务器IP和端口设置
(3)在resources文件夹下创建application.yml
spring:
servlet:
multipart:
max‐file‐size: 10MB
max‐request‐size: 10MB
server:
port: 9008
eureka:
client:
service‐url:
defaultZone: http://127.0.0.1:6868/eureka
instance:
prefer‐ip‐address: true
feign:
hystrix:
enabled: true
max-file-size是单个文件大小,max-request-size是设置总上传的数据大小
(4)创建com.changgou.file包,创建启动类FileApplication
@SpringBootApplication
@EnableEurekaClient
public class FileApplication {
public static void main(String[] args) {
SpringApplication.run(FileApplication.class,args);
}
}
(5) 创建com.changgou.file.pojo.FastDFSFile 工具类
public class FastDFSFile {
//文件名字
private String name;
//文件内容
private byte[] content;
//文件扩展名
private String ext;
//文件MD5摘要值
private String md5;
//文件创建作者
private String author;
public FastDFSFile(String name, byte[] content, String ext, String height,
String width, String author) {
super();
this.name = name;
this.content = content;
this.ext = ext;
this.author = author;
}
public FastDFSFile(String name, byte[] content, String ext) {
super();
this.name = name;
this.content = content;
this.ext = ext;
}
getter和setter.......
}
(6) 创建FastDFSClient工具类
public class FastDFSClient {
private static org.slf4j.Logger logger = LoggerFactory.getLogger(FastDFSClient.class);
/***
* 初始化加载FastDFS的TrackerServer配置
*/
static {
try {
String filePath = new ClassPathResource("fdfs_client.conf").getFile().getAbsolutePath();
ClientGlobal.init(filePath);
} catch (Exception e) {
logger.error("FastDFS Client Init Fail!",e);
}
}
/***
* 文件上传
* @param file
* @return 1.文件的组名 2.文件的路径信息
*/
public static String[] upload(FastDFSFile file) {
//获取文件的作者
NameValuePair[] meta_list = new NameValuePair[1];
meta_list[0] = new NameValuePair("author", file.getAuthor());
//接收返回数据
String[] uploadResults = null;
StorageClient storageClient=null;
try {
//创建StorageClient客户端对象
storageClient = getTrackerClient();
/***
* 文件上传
* 1)文件字节数组
* 2)文件扩展名
* 3)文件作者
*/
uploadResults = storageClient.upload_file(file.getContent(), file.getExt(), meta_list);
} catch (Exception e) {
logger.error("Exception when uploadind the file:" + file.getName(), e);
}
if (uploadResults == null && storageClient!=null) {
logger.error("upload file fail, error code:" + storageClient.getErrorCode());
}
//获取组名
String groupName = uploadResults[0];
//获取文件存储路径
String remoteFileName = uploadResults[1];
return uploadResults;
}
/***
* 获取文件信息
* @param groupName:组名
* @param remoteFileName:文件存储完整名
* @return
*/
public static FileInfo getFile(String groupName, String remoteFileName) {
try {
StorageClient storageClient = getTrackerClient();
return storageClient.get_file_info(groupName, remoteFileName);
} catch (Exception e) {
logger.error("Exception: Get File from Fast DFS failed", e);
}
return null;
}
/***
* 文件下载
* @param groupName
* @param remoteFileName
* @return
*/
public static InputStream downFile(String groupName, String remoteFileName) {
try {
//创建StorageClient
StorageClient storageClient = getTrackerClient();
//下载文件
byte[] fileByte = storageClient.download_file(groupName, remoteFileName);
InputStream ins = new ByteArrayInputStream(fileByte);
return ins;
} catch (Exception e) {
logger.error("Exception: Get File from Fast DFS failed", e);
}
return null;
}
/***
* 文件删除
* @param groupName
* @param remoteFileName
* @throws Exception
*/
public static void deleteFile(String groupName, String remoteFileName)
throws Exception {
//创建StorageClient
StorageClient storageClient = getTrackerClient();
//删除文件
int i = storageClient.delete_file(groupName, remoteFileName);
}
/***
* 获取Storage组
* @param groupName
* @return
* @throws IOException
*/
public static StorageServer[] getStoreStorages(String groupName)
throws IOException {
//创建TrackerClient
TrackerClient trackerClient = new TrackerClient();
//获取TrackerServer
TrackerServer trackerServer = trackerClient.getConnection();
//获取Storage组
return trackerClient.getStoreStorages(trackerServer, groupName);
}
/***
* 获取Storage信息,IP和端口
* @param groupName
* @param remoteFileName
* @return
* @throws IOException
*/
public static ServerInfo[] getFetchStorages(String groupName,
String remoteFileName) throws IOException {
TrackerClient trackerClient = new TrackerClient();
TrackerServer trackerServer = trackerClient.getConnection();
return trackerClient.getFetchStorages(trackerServer, groupName, remoteFileName);
}
/***
* 获取Tracker服务地址
* @return
* @throws IOException
*/
public static String getTrackerUrl() throws IOException {
return "http://"+getTrackerServer().getInetSocketAddress().getHostString()+":"+ClientGlobal.getG_tracker_http_port()+"/";
}
/***
* 获取Storage客户端
* @return
* @throws IOException
*/
private static StorageClient getTrackerClient() throws IOException {
TrackerServer trackerServer = getTrackerServer();
StorageClient storageClient = new StorageClient(trackerServer, null);
return storageClient;
}
/***
* 获取Tracker
* @return
* @throws IOException
*/
private static TrackerServer getTrackerServer() throws IOException {
TrackerClient trackerClient = new TrackerClient();
TrackerServer trackerServer = trackerClient.getConnection();
return trackerServer;
}
}
(7) 在controller下创建FileController
上传流程
- Storage server 定时获取Storage 的信息,然后将Storage 的状态信息发送给Tracker server (那台机子负载轻啊,容量高啊)
- 客户端发送一个上传请求给Tracker
- Tracker接受请求,并查询可用的Storage
- 讲Storage的信息返回给客户端,包括Storage的端口以及 ip
- 客户端上传文件(file content 和 metadata),发送给Storage
- Storage将会生成文件ID 并将文件写到磁盘,然后将file_id(路径信息和文件名)返回给客户端
- 客户端存储信息
@RestController
@RequestMapping("/file")
public class FileController {
@PostMapping("/upload")
public Result uploadFile(MultipartFile file){
try{
//判断文件是否存在
if (file == null){
throw new RuntimeException("文件不存在");
}
//获取文件的完整名称
String originalFilename = file.getOriginalFilename();
if (StringUtils.isEmpty(originalFilename)){
throw new RuntimeException("文件不存在");
}
//获取文件的扩展名称 abc.jpg jpg
String extName = originalFilename.substring(originalFilename.lastIndexOf(".") + 1);
//获取文件内容
byte[] content = file.getBytes();
//创建文件上传的封装实体类
FastDFSFile fastDFSFile = new FastDFSFile(originalFilename,content,extName);
//基于工具类进行文件上传,并接受返回参数 String[]
String[] u
ploadResult = FastDFSClient.upload(fastDFSFile);
//封装返回结果
String url = FastDFSClient.getTrackerUrl()+uploadResult[0]+"/"+uploadResult[1];
return new Result(true,StatusCode.OK,"文件上传成功",url);
}catch (Exception e){
e.printStackTrace();
}
return new Result(false, StatusCode.ERROR,"文件上传失败");
}
}
1.4Postman测试文件上传
步骤:
1、选择post请求方式,输入请求地址 http://localhost:9008/file/upload
2、填写body 选择form-data 然后选择文件file 点击添加文件,最后发送即可。
二.Power Designer使用
1.1创建物理数据模型
操作步骤:
(1)创建数据模型PDM
(2)选择数据库类型
(3)创建表和字段
指定表名
创建字段
1.2从PDM导出SQL脚本
可以通过PowerDesigner设计的PDM模型导出为SQL脚本,如下:
1.3逆向工程
上面我们是首先创建PDM模型,然后通过PowerDesigner提供的功能导出SQL脚本。实际上这个过程也
可以反过来,也就是我们可以通过SQL脚本逆向生成PDM模型,这称为逆向工程,操作如下:
1.4生成数据库报表文件
通过PowerDesigner提供的功能,可以将PDM模型生成报表文件,具体操作如下:
(1)打开报表向导窗口
(2)指定报表名称和语言
(3)选择报表格式和样式
(4)选择对象类型
(5)执行生成操作
三.定时任务组件Quartz
1.Quartz介绍
Quartz是Job scheduling(作业调度)领域的一个开源项目,Quartz既可以单独使用也可以跟spring框
架整合使用,在实际开发中一般会使用后者。使用Quartz可以开发一个或者多个定时任务,每个定时任
务可以单独指定执行的时间,例如每隔1小时执行一次、每个月第一天上午10点执行一次、每个月最后
一天下午5点执行一次等。
官网:http://www.quartz-scheduler.org/
2.Quartz入门案例
本案例基于Quartz和spring整合的方式使用。具体步骤:
(1)创建maven工程quartzdemo,导入Quartz和spring相关坐标,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>
<groupId>com.itheima</groupId>
<artifactId>quartdemo</artifactId>
<version>1.0-SNAPSHOT</version>
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context-support</artifactId>
<version>5.0.2.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-tx</artifactId>
<version>5.0.2.RELEASE</version>
</dependency>
<dependency>
<groupId>org.quartz-scheduler</groupId>
<artifactId>quartz</artifactId>
<version>2.2.1</version>
</dependency>
<dependency>
<groupId>org.quartz-scheduler</groupId>
<artifactId>quartz-jobs</artifactId>
<version>2.2.1</version>
</dependency>
</dependencies>
</project>
(2)自定义一个Job
package com.itheima.jobs;
/**
* 自定义Job
*/
public class JobDemo {
public void run(){
System.out.println("job execute...");
}
}
(3)提供Spring配置文件spring-jobs.xml,配置自定义Job、任务描述、触发器、调度工厂等
<?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:context="http://www.springframework.org/schema/context"
xmlns:dubbo="http://code.alibabatech.com/schema/dubbo"
xmlns:mvc="http://www.springframework.org/schema/mvc"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/mvc
http://www.springframework.org/schema/mvc/spring-mvc.xsd
http://code.alibabatech.com/schema/dubbo
http://code.alibabatech.com/schema/dubbo/dubbo.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd">
<!-- 注册自定义Job -->
<bean id="jobDemo" class="com.itheima.jobs.JobDemo"></bean>
<!-- 注册JobDetail,作用是负责通过反射调用指定的Job -->
<bean id="jobDetail"
class="org.springframework.scheduling.quartz.MethodInvokingJobDetailFactoryBean">
<!-- 注入目标对象 -->
<property name="targetObject" ref="jobDemo"/>
<!-- 注入目标方法 -->
<property name="targetMethod" value="run"/>
</bean>
<!-- 注册一个触发器,指定任务触发的时间 -->
<bean id="myTrigger" class="org.springframework.scheduling.quartz.CronTriggerFactoryBean">
<!-- 注入JobDetail -->
<property name="jobDetail" ref="jobDetail"/>
<!-- 指定触发的时间,基于Cron表达式 -->
<property name="cronExpression">
<value>0/10 * * * * ?</value>
</property>
</bean>
<!-- 注册一个统一的调度工厂,通过这个调度工厂调度任务 -->
<bean id="scheduler" class="org.springframework.scheduling.quartz.SchedulerFactoryBean">
<!-- 注入多个触发器 -->
<property name="triggers">
<list>
<ref bean="myTrigger"/>
</list>
</property>
</bean>
</beans>
(4)编写main方法进行测试
package com.itheima.jobs.com.itheima.app;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class App {
public static void main(String[] args) {
new ClassPathXmlApplicationContext("spring-jobs.xml");
}
}
执行上面main方法观察控制台,可以发现每隔10秒会输出一次,说明每隔10秒自定义Job被调用一次。
(5) cron表达式在线生成器
前面介绍了cron表达式,但是自己编写表达式还是有一些困难的,我们可以借助一些cron表达式在线生成器来根据我们的需求生成表达式即可。
http://cron.qqe2.com/
四.阿里云短信发送
1.注册阿里云账号
自己自行注册,阿里云官网:https://www.aliyun.com/
2.发送短信
2.1导入maven坐标
<dependency>
<groupId>com.aliyun</groupId>
<artifactId>aliyun-java-sdk-core</artifactId>
<version>3.3.1</version>
</dependency>
<dependency>
<groupId>com.aliyun</groupId>
<artifactId>aliyun-java-sdk-dysmsapi</artifactId>
<version>1.0.0</version>
</dependency>
2.2封装工具类
package com.itheima.utils;
import com.aliyuncs.DefaultAcsClient;
import com.aliyuncs.IAcsClient;
import com.aliyuncs.dysmsapi.model.v20170525.SendSmsRequest;
import com.aliyuncs.dysmsapi.model.v20170525.SendSmsResponse;
import com.aliyuncs.exceptions.ClientException;
import com.aliyuncs.http.MethodType;
import com.aliyuncs.profile.DefaultProfile;
import com.aliyuncs.profile.IClientProfile;
/**
* 短信发送工具类
*/
public class SMSUtils {
public static final String VALIDATE_CODE = "SMS_159620392";//发送短信验证码
public static final String ORDER_NOTICE = "SMS_159771588";//体检预约成功通知
/**
* 发送短信
* @param phoneNumbers
* @param param
* @throws ClientException
*/
public static void sendShortMessage(String templateCode,String phoneNumbers,String param) throws ClientException{
// 设置超时时间-可自行调整
System.setProperty("sun.net.client.defaultConnectTimeout", "10000");
System.setProperty("sun.net.client.defaultReadTimeout", "10000");
// 初始化ascClient需要的几个参数
final String product = "Dysmsapi";// 短信API产品名称(短信产品名固定,无需修改)
final String domain = "dysmsapi.aliyuncs.com";// 短信API产品域名(接口地址固定,无需修改)
// 替换成你的AK
final String accessKeyId = "accessKeyId";// 你的accessKeyId,参考本文档步骤2
final String accessKeySecret = "accessKeySecret";// 你的accessKeySecret,参考本文档步骤2
// 初始化ascClient,暂时不支持多region(请勿修改)
IClientProfile profile = DefaultProfile.getProfile("cn-hangzhou", accessKeyId, accessKeySecret);
DefaultProfile.addEndpoint("cn-hangzhou", "cn-hangzhou", product, domain);
IAcsClient acsClient = new DefaultAcsClient(profile);
// 组装请求对象
SendSmsRequest request = new SendSmsRequest();
// 使用post提交
request.setMethod(MethodType.POST);
// 必填:待发送手机号。支持以逗号分隔的形式进行批量调用,批量上限为1000个手机号码,批量调用相对于单条调用及时性稍有延迟,验证码类型的短信推荐使用单条调用的方式
request.setPhoneNumbers(phoneNumbers);
// 必填:短信签名-可在短信控制台中找到
request.setSignName("传智健康");
// 必填:短信模板-可在短信控制台中找到
request.setTemplateCode(templateCode);
// 可选:模板中的变量替换JSON串,如模板内容为"亲爱的${name},您的验证码为${code}"时,此处的值为
// 友情提示:如果JSON中需要带换行符,请参照标准的JSON协议对换行符的要求,比如短信内容中包含\r\n的情况在JSON中需要表示成\\r\\n,否则会导致JSON在服务端解析失败
request.setTemplateParam("{\"code\":\""+param+"\"}");
// 可选-上行短信扩展码(扩展码字段控制在7位或以下,无特殊需求用户请忽略此字段)
// request.setSmsUpExtendCode("90997");
// 可选:outId为提供给业务方扩展字段,最终在短信回执消息中将此值带回给调用者
// request.setOutId("yourOutId");
// 请求失败这里会抛ClientException异常
SendSmsResponse sendSmsResponse = acsClient.getAcsResponse(request);
if (sendSmsResponse.getCode() != null && sendSmsResponse.getCode().equals("OK")) {
// 请求成功
System.out.println("请求成功");
}
}
}
2.3测试短信发送
public static void main(String[] args)throws Exception {
SMSUtils.sendShortMessage("SMS_159620392","13812345678","1234");
}
五.静态化技术Freemarker
1.环境搭建
创建maven工程并导入Freemarker的maven坐标
<dependency>
<groupId>org.freemarker</groupId>
<artifactId>freemarker</artifactId>
<version>2.3.23</version>
</dependency>
2.创建模板文件
模板文件中有四种元素:
1、文本,直接输出的部分 2、注释,即<#–…-->格式不会输出 3、插值(Interpolation):即${…}部分,将使用数据模型中的部分替代输出 4、FTL指令:FreeMarker指令,和HTML标记类似,名字前加#予以区分,不会输出
Freemarker的模板文件后缀可以任意,一般建议为ftl。
在D盘创建ftl目录,在ftl目录中创建名称为test.ftl的模板文件,内容如下:
<html>
<head>
<meta charset="utf-8">
<title>Freemarker入门</title>
</head>
<body>
<#--我只是一个注释,我不会有任何输出 -->
${name}你好,${message}
</body>
</html>
3.配置文件
在静态页面模板模块中添加配置freemarker.properties
out_put_path=指定将静态HTML页面生成的目录位置
在Spring配置文件中配置
<bean id="freemarkerConfig"
class="org.springframework.web.servlet.view.freemarker.FreeMarkerConfigurer">
<!--指定模板文件所在目录-->
<property name="templateLoaderPath" value="/WEB-INF/ftl/" />
<!--指定字符集-->
<property name="defaultEncoding" value="UTF-8" />
</bean>
<context:property-placeholder location="classpath:freemarker.properties"/>
4.生成文件
使用步骤:
第一步:创建一个 Configuration 对象,直接 new 一个对象。构造方法的参数就是 freemarker的版本号。
第二步:设置模板文件所在的路径。
第三步:设置模板文件使用的字符集。一般就是 utf-8。
第四步:加载一个模板,创建一个模板对象。
第五步:创建一个模板使用的数据集,可以是 pojo 也可以是 map。一般是 Map。
第六步:创建一个 Writer 对象,一般创建 FileWriter 对象,指定生成的文件名。
第七步:调用模板对象的 process 方法输出文件。
第八步:关闭流。
public static void main(String[] args) throws Exception{
//1.创建配置类
Configuration configuration=new Configuration(Configuration.getVersion());
//2.设置模板所在的目录
configuration.setDirectoryForTemplateLoading(new File("D:\\ftl"));
//3.设置字符集
configuration.setDefaultEncoding("utf-8");
//4.加载模板
Template template = configuration.getTemplate("test.ftl");
//5.创建数据模型
Map map=new HashMap();
map.put("name", "张三");
map.put("message", "欢迎来到传智播客!");
//6.创建Writer对象
Writer out =new FileWriter(new File("d:\\test.html"));
//7.输出
template.process(map, out);
//8.关闭Writer对象
out.close();
}
上面的入门案例中Configuration配置对象是自己创建的,字符集和模板文件所在目录也是在Java代码中指定的。在项目中应用时可以将Configuration对象的创建交由Spring框架来完成,并通过依赖注入方式将字符集和模板所在目录注入进去。
5. Freemarker指令
5.1 assign指令
assign指令用于在页面上定义一个变量
(1)定义简单类型
<#assign linkman="周先生">
联系人:${linkman}
(2)定义对象类型
<#assign info={"mobile":"13812345678",'address':'北京市昌平区'} >
电话:${info.mobile} 地址:${info.address}
5.2 include指令
include指令用于模板文件的嵌套
(1)创建模板文件head.ftl
<h1>黑马程序员</h1>
(2)修改入门案例中的test.ftl,在test.ftl模板文件中使用include指令引入上面的模板文件
<#include "head.ftl"/>
5.3 if指令
if指令用于判断
(1)在模板文件中使用if指令进行判断
<#if success=true>
你已通过实名认证
<#else>
你未通过实名认证
</#if>
(2)在java代码中为success变量赋值
map.put("success", true);
在freemarker的判断中,可以使用= 也可以使用==
5.4 list指令
list指令用于遍历
(1)在模板文件中使用list指令进行遍历
<#list goodsList as goods>
商品名称: ${goods.name} 价格:${goods.price}<br>
</#list>
(2)在java代码中为goodsList赋值
List goodsList=new ArrayList();
Map goods1=new HashMap();
goods1.put("name", "苹果");
goods1.put("price", 5.8);
Map goods2=new HashMap();
goods2.put("name", "香蕉");
goods2.put("price", 2.5);
Map goods3=new HashMap();
goods3.put("name", "橘子");
goods3.put("price", 3.2);
goodsList.add(goods1);
goodsList.add(goods2);
goodsList.add(goods3);
map.put("goodsList", goodsList);
六.微服务网关
1.微服务网关概述
微服务网关就是一个系统,通过暴露该微服务网关系统,方便我们进行相关的鉴
权,安全控制,日志统一处理,易于监控的相关功能。
2.微服务网关微服务搭建
1.添加依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring‐cloud‐starter‐gateway</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring‐cloud‐starter‐netflix‐hystrix</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring‐cloud‐starter‐netflix‐eureka‐client</artifactId>
</dependency>
2.创建引导类
@SpringBootApplication
@EnableEurekaClient
public class GatewayApplication {
public static void main(String[] args) {
SpringApplication.run(GatewayApplication.class, args);}
}
3.在resources下创建application.yml
spring:
application:
name: sysgateway
cloud:
gateway:
routes:
- id: goods
uri: lb://goods
predicates:
- Path=/goods/**
filters:
- StripPrefix= 1
- id: system
uri: lb://system
predicates:
- Path=/system/**
filters:
- StripPrefix= 1
server:
port: 9101
eureka:
client:
service-url:
defaultZone: http://127.0.0.1:6868/eureka
instance:
prefer-ip-address: true
3.微服务网关跨域
globalcors:
cors-configurations:
'[/**]': # 匹配所有请求
allowedOrigins: "*" #跨域处理 允许所有的域
allowedMethods: # 支持的方法
- GET
- POST
- PUT
- DELETE
4.微服务网关过滤器
我们可以通过网关过滤器,实现一些逻辑的处理,比如ip黑白名单拦截、特定地址的拦截
等。下面的代码中做了两个过滤器,并且设定的先后顺序,只演示过滤器与运行效果。
示例:
@Component
public class IpFilter implements GlobalFilter, Ordered {
@Override
public Mono<Void> filter(ServerWebExchange exchange,GatewayFilterChain chain) {
System.out.println("经过第1个过滤器IpFilter");
ServerHttpRequest request = exchange.getRequest();
InetSocketAddress remoteAddress = request.getRemoteAddress();
System.out.println("ip:"+remoteAddress.getHostName());
return chain.filter(exchange);
}
@Override
public int getOrder() {
return 1;
}
}
七.网关限流
为了防止系统被频繁的请求压垮,我们需要在微服务网关中做限流操作
我们常用的方法有令牌桶算法
1.令牌桶算法
1.1令牌桶算法的大概意思是,所有的请求在处理前必须要有一个令牌才能被处理
1.2根据限流的设定,令牌会以一定的速率添加到令牌桶中
1.3桶设置最大值,满的时候就添加不进去
1.4所有请求首先要获取令牌,拿着令牌进行业务处理,结束了就销毁令牌
1.5令牌桶有最低限额,当桶中的令牌达到最低限额的时候,请求处理完之后将不会删除令牌,以此保证足够的限流
2.代码实现
2.1导入依赖
<!‐‐redis‐‐>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring‐boot‐starter‐data‐redis‐reactive</artifactId>
<version>2.1.3.RELEASE</version>
</dependency>
(2)定义KeyResolver
//定义一个KeyResolver
@Bean
public KeyResolver ipKeyResolver() {
return new KeyResolver() {
@Override
public Mono<String> resolve(ServerWebExchange exchange) {
return Mono.just(exchange.getRequest().getRemoteAddress().getHostName());
}
};
}
(3)修改application.yml中配置项,指定限制流量的配置以及REDIS的配置,修改后最
终配置如下:
spring:
application:
name: sysgateway
cloud:
gateway:
globalcors:
cors-configurations:
'[/**]': # 匹配所有请求
allowedOrigins: "*" #跨域处理 允许所有的域
allowedMethods: # 支持的方法
- GET
- POST
- PUT
- DELETE
routes:
- id: goods
uri: lb://goods
predicates:
- Path=/goods/**
filters:
- StripPrefix= 1
- name: RequestRateLimiter #请求数限流 名字不能随便写
args:
key-resolver: "#{@ipKeyResolver}"
redis-rate-limiter.replenishRate: 1 #令牌桶每秒填充平均速率
redis-rate-limiter.burstCapacity: 1 #令牌桶总容量
- id: system
uri: lb://system
predicates:
- Path=/system/**
filters:
- StripPrefix= 1
redis:
host: 192.168.200.128
server:
port: 9101
eureka:
client:
service-url:
defaultZone: http://127.0.0.1:6868/eureka
instance:
prefer-ip-address: true
burstCapacity:令牌桶总容量。
replenishRate:令牌桶每秒填充平均速率。
key-resolver:用于限流的键的解析器的 Bean 对象的名字。它使用 SpEL 表达式根
据#{@beanName}从 Spring 容器中获取 Bean 对象。
八.BCrypt密码加密
1.管理员登录密码验证
1.1需求分析
系统管理用户需要管理后台,需要先输入用户名和密码进行登录,才能进入管理后台。
思路:
用户发送请求,输入用户名和密码
后台管理微服务controller接收参数,验证用户名和密码是否正确,如果正确则返回
用户登录成功结果
1.2代码实现
(1)AdminService新增方法定义
/**
* 登录验证密码
* @param admin
* @return
*/
boolean login(Admin admin);
(2)AdminServiceImpl实现此方法
@Override
public boolean login(Admin admin) {
//根据登录名查询管理员
Admin admin1 = new Admin();
admin1.setLoginName(admin.getLoginName());
admin1.setStatus("1");
Admin admin2 = adminMapper.selectOne(admin1);//数据库查询出的对象
if (admin2 == null) {
return false;
} else {
//验证密码, Bcrypt为spring的包, 第一个参数为明文密码, 第二个参数为密文密码
return BCrypt.checkpw(admin.getPassword(), admin2.getPassword());
}
}
(3)AdminController新增方法
/**
* 登录
*
* @param admin
* @return
*/
@PostMapping("/login")
public Result login(@RequestBody Admin admin) {
boolean login = adminService.login(admin);
if (login) {
return new Result();
} else {
return new Result(false, StatusCode.LOGINERROR, "用户名或密码错误");
}
}
九.JWT 实现微服务鉴权
1. 什么是微服务鉴权
鉴权就是实现对用户是否有权限的校验,如果有就放行,如果就直接返回用户,那么我们可以采用JWT的方式来实现鉴权校验 .
2.JWT
JSON Web Token(JWT)是一个非常轻巧的规范 ,一个JWT实际上就是一个字符串,它由三部分组成,头部、载荷与签名。
3.畅购微服务鉴权代码实现
3.1思路分析
1. 用户进入网关开始登陆,网关过滤器进行判断,如果是登录,则路由到后台管理微服务进
行登录
2. 用户登录成功,后台管理微服务签发JWT TOKEN信息返回给用户
3. 用户再次进入网关开始访问,网关过滤器接收用户携带的TOKEN
4. 网关过滤器解析TOKEN ,判断是否有权限,如果有,则放行,如果没有则返回未认证错误
3.2 系统微服务签发token
3.2.1添加依赖
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.0</version>
</dependency>
3.2.2在changgou_service_system中创建类: JwtUtil
package com.changgou.system.util;
import io.jsonwebtoken.JwtBuilder;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import java.util.Base64;
import java.util.Date;
/**
* JWT工具类
*/
public class JwtUtil {
//有效期为
public static final Long JWT_TTL = 3600000L;// 60 * 60 *1000 一个小时
//设置秘钥明文
public static final String JWT_KEY = "itcast";
/**
* 创建token
*
* @param id
* @param subject
* @param ttlMillis
* @return
*/
public static String createJWT(String id, String subject, Long ttlMillis) {
SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
long nowMillis = System.currentTimeMillis();
Date now = new Date(nowMillis);
if (ttlMillis == null) {
ttlMillis = JwtUtil.JWT_TTL;
}
long expMillis = nowMillis + ttlMillis;
Date expDate = new Date(expMillis);
SecretKey secretKey = generalKey();
JwtBuilder builder = Jwts.builder()
.setId(id) //唯一的ID
.setSubject(subject) // 主题 可以是JSON数据
.setIssuer("admin") // 签发者
.setIssuedAt(now) // 签发时间
.signWith(signatureAlgorithm, secretKey) //使用HS256对称加密算法签名, 第二个参数为秘钥
.setExpiration(expDate);// 设置过期时间
return builder.compact();
}
/**
* 生成加密后的秘钥 secretKey
*
* @return
*/
public static SecretKey generalKey() {
byte[] encodedKey = Base64.getDecoder().decode(JwtUtil.JWT_KEY);
SecretKey key = new SecretKeySpec(encodedKey, 0,
encodedKey.length, "AES");
return key;
}
}
3.2.3修改AdminController的login方法, 用户登录成功 则 签发TOKEN
/**
* 登录
*
* @param admin
* @return
*/
@PostMapping("/login")
public Result login(@RequestBody Admin admin) {
boolean login = adminService.login(admin);
if (login) { //如果验证成功
Map<String, String> info = new HashMap<>();
info.put("username", admin.getLoginName());
String token = JwtUtil.createJWT(UUID.randomUUID().toString(), admin.getLoginName(), null);
info.put("token", token);
return new Result(true, StatusCode.OK, "登录成功", info);
} else {
return new Result(false, StatusCode.LOGINERROR, "用户名或密码错误");
}
}
3.3网关过滤器验证token
(1)在changgou_gateway_system网关系统添加依赖
<!‐‐鉴权‐‐>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.0</version>
</dependency>
(2)创建JWTUtil类
package com.changgou.gateway.util;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import java.util.Base64;
/**
* jwt校验工具类
*/
public class JwtUtil {
//有效期为
public static final Long JWT_TTL = 3600000L;// 60 * 60 *1000 一个小时
//设置秘钥明文
public static final String JWT_KEY = "itcast";
/**
* 生成加密后的秘钥 secretKey
* *
*
* @return
*/
public static SecretKey generalKey() {
byte[] encodedKey = Base64.getDecoder().decode(JwtUtil.JWT_KEY);
SecretKey key = new SecretKeySpec(encodedKey, 0,
encodedKey.length, "AES");
return key;
}
/**
* 解析
* *
*
* @param jwt
* @return
* @throws Exception
*/
public static Claims parseJWT(String jwt) throws Exception {
SecretKey secretKey = generalKey();
return Jwts.parser()
.setSigningKey(secretKey)
.parseClaimsJws(jwt)
.getBody();
}
}
(3)创建过滤器,用于token验证
import org.springframework.core.Ordered;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
/**
* 鉴权过滤器 验证token
*/
@Component
public class AuthorizeFilter implements GlobalFilter, Ordered {
private static final String AUTHORIZE_TOKEN = "token";
@Override
public Mono<Void> filter(ServerWebExchange exchange,
GatewayFilterChain chain) {
//1. 获取请求
ServerHttpRequest request = exchange.getRequest();
//2. 则获取响应
ServerHttpResponse response = exchange.getResponse();
//3. 如果是登录请求则放行
if (request.getURI().getPath().contains("/admin/login")) {
return chain.filter(exchange);
}
//4. 获取请求头
HttpHeaders headers = request.getHeaders();
//5. 请求头中获取令牌
String token = headers.getFirst(AUTHORIZE_TOKEN);
//6. 判断请求头中是否有令牌
if (StringUtils.isEmpty(token)) {
//7. 响应中放入返回的状态吗, 没有权限访问
response.setStatusCode(HttpStatus.UNAUTHORIZED);
//8. 返回
return response.setComplete();
}
//9. 如果请求头中有令牌则解析令牌
try {
JwtUtil.parseJWT(token);
} catch (Exception e) {
e.printStackTrace();
//10. 解析jwt令牌出错, 说明令牌过期或者伪造等不合法情况出现
response.setStatusCode(HttpStatus.UNAUTHORIZED);
//11. 返回
return response.setComplete();
}
//12. 放行
return chain.filter(exchange);
}
@Override
public int getOrder() {
return 0;
}
}
十.分布式文件存储系统 FastDFS
1、CORS解决跨域问题
什么是跨域
同源策略是一种约定,他是浏览器最核心也是最基本的安全功能,如果缺少 了同源策略,则浏览器的正常功能可能都会受到影响。同源策略会阻止一个域的javascript脚本和另外一个域的内容进行交互。
同源就是两个页面具有相同的协议,主机和端口号
只有发生ajax请求,才可能出现跨域情况
CORS是W3C标准,全称 跨域资源共享。CORS需要浏览器和服务器同时支持,目前所有浏览器都支持该功能 服务器中springMVC在4.2或以上版本。可以使用注解实现跨域,Controller类上添加注解@CrossOrigin
2、分布式文件存储系统 FastDFS
FastDFS是一个开源的轻量级分布式文件系统,他对文件进行管理,功能包括:文件存储、文件同步、文件访问,解决来大容量存储和负载均衡的问题,特别适合以文件为载体的在线服务
FastDFS具备冗余备份、负载均衡、线性扩容等机制,并注重高可用,高性能等标注
FastDFS架构包括Tracker server 和 Storage server 客户端请求Tracker server进行文件上传、下载,通过Tracker server调度最终由Storage server 完成文件上传和下载
Tracker server 作用是负载均衡和调度,通过 Tracker server 在文件上传时可以根据一些策略找到Storage server 提供文件上传服务。可以将 tracker 称为追踪服务器或调度服务器。Storage server 作用是文件存储,客户端上传的文件最终存储在 Storage 服务器上,Storageserver 没有实现自己的文件系统而是利用操作系统的文件系统来管理文件。可以将storage称为存储服务器。
上传流程
- Storage server 定时获取Storage 的信息,然后将Storage 的状态信息发送给Tracker server (那台机子负载轻啊,容量高啊)
- 客户端发送一个上传请求给Tracker
- Tracker接受请求,并查询可用的Storage
- 讲Storage的信息返回给客户端,包括Storage的端口以及 ip
- 客户端上传文件(file content 和 metadata),发送给Storage
- Storage将会生成文件ID 并将文件写到磁盘,然后将file_id(路径信息和文件名)返回给客户端
- 客户端存储信息
文件服务器的搭建
1.导入依赖
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>net.oschina.zcx7878</groupId>
<artifactId>fastdfs-client-java</artifactId>
<version>1.27.0.0</version>
</dependency>
<dependency>
<groupId>com.changgou</groupId>
<artifactId>changgou_common</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
</dependencies>
2.配置文件的配置
fdfs_client.conf
spring:
servlet:
multipart:
max-file-size: 10MB # 单个文件大小
max-request-size: 10MB # 设置总上传的数据大小
server:
port: 9008
eureka:
client:
service-url:
defaultZone: http://127.0.0.1:6868/eureka
instance:
prefer-ip-address: true
feign:
hystrix:
enabled: true
3.文件信息封装
public class FastDFSFile {
//文件名字
private String name;
//文件内容
private byte[] content;
//文件扩展名
private String ext;
//文件MD5摘要值
private String md5;
//文件创建作者
private String author;
public FastDFSFile(String name, byte[] content, String ext, String height,
String width, String author) {
super();
this.name = name;
this.content = content;
this.ext = ext;
this.author = author;
}
public FastDFSFile(String name, byte[] content, String ext) {
super();
this.name = name;
this.content = content;
this.ext = ext;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public byte[] getContent() {
return content;
}
public void setContent(byte[] content) {
this.content = content;
}
public String getExt() {
return ext;
}
public void setExt(String ext) {
this.ext = ext;
}
public String getMd5() {
return md5;
}
public void setMd5(String md5) {
this.md5 = md5;
}
public String getAuthor() {
return author;
}
public void setAuthor(String author) {
this.author = author;
}
}
4.创建FastDFSClient类,实现FastDFS信息获取以及文件的相关操作
public class FastDFSClient {
private static org.slf4j.Logger logger = LoggerFactory.getLogger(FastDFSClient.class);
/***
* 初始化加载FastDFS的TrackerServer配置
*/
static {
try {
String filePath = new ClassPathResource("fdfs_client.conf").getFile().getAbsolutePath();
ClientGlobal.init(filePath);
} catch (Exception e) {
logger.error("FastDFS Client Init Fail!",e);
}
}
/***
* 文件上传
* @param file
* @return 1.文件的组名 2.文件的路径信息
*/
public static String[] upload(FastDFSFile file) {
//获取文件的作者
NameValuePair[] meta_list = new NameValuePair[1];
meta_list[0] = new NameValuePair("author", file.getAuthor());
//接收返回数据
String[] uploadResults = null;
StorageClient storageClient=null;
try {
//创建StorageClient客户端对象
storageClient = getTrackerClient();
/***
* 文件上传
* 1)文件字节数组
* 2)文件扩展名
* 3)文件作者
*/
uploadResults = storageClient.upload_file(file.getContent(), file.getExt(), meta_list);
} catch (Exception e) {
logger.error("Exception when uploadind the file:" + file.getName(), e);
}
if (uploadResults == null && storageClient!=null) {
logger.error("upload file fail, error code:" + storageClient.getErrorCode());
}
//获取组名
String groupName = uploadResults[0];
//获取文件存储路径
String remoteFileName = uploadResults[1];
return uploadResults;
}
/***
* 获取文件信息
* @param groupName:组名
* @param remoteFileName:文件存储完整名
* @return
*/
public static FileInfo getFile(String groupName, String remoteFileName) {
try {
StorageClient storageClient = getTrackerClient();
return storageClient.get_file_info(groupName, remoteFileName);
} catch (Exception e) {
logger.error("Exception: Get File from Fast DFS failed", e);
}
return null;
}
/***
* 文件下载
* @param groupName
* @param remoteFileName
* @return
*/
public static InputStream downFile(String groupName, String remoteFileName) {
try {
//创建StorageClient
StorageClient storageClient = getTrackerClient();
//下载文件
byte[] fileByte = storageClient.download_file(groupName, remoteFileName);
InputStream ins = new ByteArrayInputStream(fileByte);
return ins;
} catch (Exception e) {
logger.error("Exception: Get File from Fast DFS failed", e);
}
return null;
}
/***
* 文件删除
* @param groupName
* @param remoteFileName
* @throws Exception
*/
public static void deleteFile(String groupName, String remoteFileName)
throws Exception {
//创建StorageClient
StorageClient storageClient = getTrackerClient();
//删除文件
int i = storageClient.delete_file(groupName, remoteFileName);
}
/***
* 获取Storage组
* @param groupName
* @return
* @throws IOException
*/
public static StorageServer[] getStoreStorages(String groupName)
throws IOException {
//创建TrackerClient
TrackerClient trackerClient = new TrackerClient();
//获取TrackerServer
TrackerServer trackerServer = trackerClient.getConnection();
//获取Storage组
return trackerClient.getStoreStorages(trackerServer, groupName);
}
/***
* 获取Storage信息,IP和端口
* @param groupName
* @param remoteFileName
* @return
* @throws IOException
*/
public static ServerInfo[] getFetchStorages(String groupName,
String remoteFileName) throws IOException {
TrackerClient trackerClient = new TrackerClient();
TrackerServer trackerServer = trackerClient.getConnection();
return trackerClient.getFetchStorages(trackerServer, groupName, remoteFileName);
}
/***
* 获取Tracker服务地址
* @return
* @throws IOException
*/
public static String getTrackerUrl() throws IOException {
return "http://"+getTrackerServer().getInetSocketAddress().getHostString()+":"+ClientGlobal.getG_tracker_http_port()+"/";
}
/***
* 获取Storage客户端
* @return
* @throws IOException
*/
private static StorageClient getTrackerClient() throws IOException {
TrackerServer trackerServer = getTrackerServer();
StorageClient storageClient = new StorageClient(trackerServer, null);
return storageClient;
}
/***
* 获取Tracker
* @return
* @throws IOException
*/
private static TrackerServer getTrackerServer() throws IOException {
TrackerClient trackerClient = new TrackerClient();
TrackerServer trackerServer = trackerClient.getConnection();
return trackerServer;
}
}
5.创建一个FileController,在该控制器中实现文件上传操作
@RestController
@RequestMapping("/file")
public class FileController {
@PostMapping("/upload")
public Result uploadFile(MultipartFile file){
try{
//判断文件是否存在
if (file == null){
throw new RuntimeException("文件不存在");
}
//获取文件的完整名称
String originalFilename = file.getOriginalFilename();
if (StringUtils.isEmpty(originalFilename)){
throw new RuntimeException("文件不存在");
}
//获取文件的扩展名称 abc.jpg jpg
String extName = originalFilename.substring(originalFilename.lastIndexOf(".") + 1);
//获取文件内容
byte[] content = file.getBytes();
//创建文件上传的封装实体类
FastDFSFile fastDFSFile = new FastDFSFile(originalFilename,content,extName);
//基于工具类进行文件上传,并接受返回参数 String[]
String[] uploadResult = FastDFSClient.upload(fastDFSFile);
//封装返回结果
String url = FastDFSClient.getTrackerUrl()+uploadResult[0]+"/"+uploadResult[1];
return new Result(true,StatusCode.OK,"文件上传成功",url);
}catch (Exception e){
e.printStackTrace();
}
return new Result(false, StatusCode.ERROR,"文件上传失败");
}
}