1.概述
页面静态化:在前端门户网站或者其他的一些页面,比如首页,是需要频繁的访问,这样会对服务器造成很大的压力,使用页面静态化,对页面进行一种类似“缓存”的效果,在页面发生改变的时候(添加、修改、删除数据)的时候,对页面进行静态化,输出到静态服务器ngnix,以后访问该页面,直接访问静态化后的页面,减轻服务器的压力!
页面静态化所适用的场景:
①:页面并发量很高
②:数据不易轻易改变
页面静态化是利用模板加数据生成文件,在微服务中,每个服务都是一台电脑,可以分布在不同的地方,那么数据如何传递,模板文件从哪里获得?
页面静态化应该作为一个独立的微服务,需要做页面静态化的服务直接通过feign来调用即可!
2.方案
1:需要一个后台管理中心,管理需要作页面静态化的页面
需要保存的数据,页面名字,最终输出到站点的路径、以及模板文件上传到fastdfs的路径!
2:后台管理页面模板文件上传,页面上传,不需要通过feign调用!
①:fastdfs提供上传方法
@PostMapping("/upload")
public AjaxResult upload(MultipartFile file){
String fileExtensionName = FilenameUtils.getExtension(file.getOriginalFilename());
try {
String filePath = FastDfsApiOpr.upload(file.getBytes(), fileExtensionName);
return AjaxResult.me().setSuccess(true).setMessage("上传成功!").setResultObj(filePath);
} catch (IOException e) {
e.printStackTrace();
return AjaxResult.me().setSuccess(false).setMessage("上传失败!");
}
}
②:所用到的工具类
package com.hanfengyi.fastdfs.utils;
import org.csource.fastdfs.*;
public class FastDfsApiOpr {
public static String CONF_FILENAME = FastDfsApiOpr.class.getClassLoader()
.getResource("fdfs_client.conf").getFile();
/**
* 上传文件
* @param file
* @param extName
* @return
*/
public static String upload(byte[] file,String extName) {
try {
ClientGlobal.init(CONF_FILENAME);
TrackerClient tracker = new TrackerClient();
TrackerServer trackerServer = tracker.getTrackerServer();
StorageServer storageServer = null;
StorageClient storageClient = new StorageClient(trackerServer, storageServer);
String fileIds[] = storageClient.upload_file(file,extName,null);
System.out.println(fileIds.length);
System.out.println("组名:" + fileIds[0]);
System.out.println("路径: " + fileIds[1]);
return "/"+fileIds[0]+"/"+fileIds[1];
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
/**
* 上传文件
* @param extName
* @return
*/
public static String upload(String path,String extName) {
try {
ClientGlobal.init(CONF_FILENAME);
TrackerClient tracker = new TrackerClient();
TrackerServer trackerServer = tracker.getTrackerServer();
StorageServer storageServer = null;
StorageClient storageClient = new StorageClient(trackerServer, storageServer);
String fileIds[] = storageClient.upload_file(path, extName,null);
System.out.println(fileIds.length);
System.out.println("组名:" + fileIds[0]);
System.out.println("路径: " + fileIds[1]);
return "/"+fileIds[0]+"/"+fileIds[1];
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
/**
* 下载文件
* @param groupName
* @param fileName
* @return
*/
public static byte[] download(String groupName,String fileName) {
try {
ClientGlobal.init(CONF_FILENAME);
TrackerClient tracker = new TrackerClient();
TrackerServer trackerServer = tracker.getTrackerServer();
StorageServer storageServer = null;
StorageClient storageClient = new StorageClient(trackerServer, storageServer);
byte[] b = storageClient.download_file(groupName, fileName);
return b;
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
/**
* 删除文件
* @param groupName
* @param fileName
*/
public static void delete(String groupName,String fileName){
try {
ClientGlobal.init(CONF_FILENAME);
TrackerClient tracker = new TrackerClient();
TrackerServer trackerServer = tracker.getTrackerServer();
StorageServer storageServer = null;
StorageClient storageClient = new StorageClient(trackerServer,
storageServer);
int i = storageClient.delete_file(groupName,fileName);
System.out.println( i==0 ? "删除成功" : "删除失败:"+i);
} catch (Exception e) {
e.printStackTrace();
throw new RuntimeException("删除异常,"+e.getMessage());
}
}
}
③:所需要的配置文件fdfs_client.conf 指向tracker-server服务端口
tracker_server=47.103.93.59:22122
④:点击提交按钮,将数据保存在数据库中
3. 创建页面静态化服务作为独立的微服务
①:集成eureka注册中心、config配置中心、集成feign
②:创建控制器,页面静态化的服务方法
@RestController
@RequestMapping("/static")
public class pageStaticAction {
@Autowired
private PagerServiceImpl pagerService;
@PostMapping("/page")
public AjaxResult pageStaticAction(@RequestParam("key")String key,@RequestParam("templateName")String templateName){
pagerService.pageStaticAction(key,templateName);
return AjaxResult.me();
}
}
该方法接收两个参数:key表示页面静态化所需要的数据存储在redis中的key,templateName模板文件名字,对哪一个页面做页面静态化
③:集成feign,创建feign的接口以及托底类
@FeignClient(value = "static-page-server",fallbackFactory = StaticPageFignFallBack.class)
public interface StaticPageFignClient{
@PostMapping("/static/page")
AjaxResult pageStaticAction(@RequestParam("key")String key, @RequestParam("templateName")String templateName);
}
@Component
public class StaticPageFignFallBack implements FallbackFactory {
@Override
public StaticPageFignClient create(Throwable throwable) {
return new StaticPageFignClient() {
@Override
public AjaxResult pageStaticAction(String key, String name) {
throwable.printStackTrace();
return AjaxResult.me().setSuccess(false).setMessage("发生了一点小问题:["+throwable.getMessage()+"]");
}
};
}
}
4. 以课程为例,在页面发生改变的时候通过feign调用页面静态化微服务
java/**
* 触发静态页面生成
*/
public void pageStaticAction(){
//1. 查询数据放到redis中
List courseTypes = treeData();
Map map = new HashMap<>();
//模板文件中需要的属性名字
map.put("courseTypes", courseTypes);
AjaxResult result = redisFeignClient.set(RedisKeyConstants.COURSE_TYPE_PAGE_STATIC, JSON.toJSONString(map));
if(!result.isSuccess()){
throw new RuntimeException("数据存储失败!");
}
String templateName = "home";
//2. 通过feign调用页面静态化微服务
AjaxResult ajaxResult = staticPageFignClient.pageStaticAction(RedisKeyConstants.COURSE_TYPE_PAGE_STATIC,templateName);
if(!ajaxResult.isSuccess()){
throw new RuntimeException("页面静态化失败!");
}
}
5. 页面静态化中具体执行的逻辑
/**
* 静态化页面生成
* @param key redis存储的数据的key
* @param templateName 模板文件名字
*/
@Override
public void pageStaticAction(String key, String templateName) {
//2. 拿到模板
Pager pager = baseMapper.getTemplateByName(templateName);
if(pager==null && pager.getTemplateUrl().isEmpty()){
throw new RuntimeException("所需模板获取失败!");
}
//3. 从fastdfs上下载模板
byte[] templateFile = fastDFSFignClient.getTemplateFile(pager.getTemplateUrl());
if(templateFile==null || templateFile.length==0){
throw new RuntimeException("文件下载失败!");
}
//4. 获得windows文件临时存储路径 C:\Users\Han\AppData\Local\Temp\
String systemTempPath =System.getProperty("java.io.tmpdir");
//拼接zip文件存储路径:C:\Users\Han\AppData\Local\Temp\home.zip
String zipPath = systemTempPath+templateName+".zip";
try {
FileCopyUtils.copy(templateFile, CreateFileUtils.createFile(zipPath));
} catch (IOException e) {
e.printStackTrace();
}
//5. 将下载的zip文件解压到当前目录 解压路径:C:\Users\Han\AppData\Local\Temp\templateFile\
String unzipPath = systemTempPath+"templateFile/";
try {
ZipUtils.unZip(zipPath, unzipPath);
} catch (Exception e) {
e.printStackTrace();
}
//6. 将下载解压后的模板与数据合并生成html文件
//6.1 拿到数据
AjaxResult ajaxResult = redisFeignClient.get(key);
if(ajaxResult==null || !ajaxResult.isSuccess()){
throw new RuntimeException("所需数据获取失败!");
}
String treeData = ajaxResult.getResultObj().toString();
Map model = JSONObject.parseObject(treeData, Map.class);
model.put("staticRoot", unzipPath);
// model:数据对象 templateFilePathAndName 模板文件的物理路径 targetFilePathAndName 目标输出文件的物理路径
String templateFilePathAndName = unzipPath+templateName+".vm"; //C:\Users\Han\AppData\Local\Temp\template\templateFile\home.vm
String targetFilePathAndName = unzipPath+templateName+".html"; //C:\Users\Han\AppData\Local\Temp\template\templateFile\home.html
VelocityUtils.staticByTemplate(model,templateFilePathAndName, targetFilePathAndName);
//7. 将合并后的文件上传到fastdfs
String htmlPathInFastdfs = null;
try {
byte[] bytes = FileCopyUtils.copyToByteArray(CreateFileUtils.createFile(targetFilePathAndName));
if(bytes!=null && bytes.length>0){
AjaxResult uploadResult = fastDFSFignClient.uploadByBytes(bytes,"html");
if(!ajaxResult.isSuccess() || ajaxResult.getResultObj()==null){
throw new RuntimeException("html文件上传失败");
}
htmlPathInFastdfs = uploadResult.getResultObj().toString();
}
} catch (IOException e) {
e.printStackTrace();
}
fastdfs中上传及下载方法
@RestController
@RequestMapping("/fastdfs")
public class FastDFSController {
/**
* 文件上传
* @param file
* @return
*/
@PostMapping("/upload")
public AjaxResult upload(MultipartFile file){
String fileExtensionName = FilenameUtils.getExtension(file.getOriginalFilename());
try {
String filePath = FastDfsApiOpr.upload(file.getBytes(), fileExtensionName);
return AjaxResult.me().setSuccess(true).setMessage("上传成功!").setResultObj(filePath);
} catch (IOException e) {
e.printStackTrace();
return AjaxResult.me().setSuccess(false).setMessage("上传失败!");
}
}
@PostMapping("/uploadByBytes")
public AjaxResult uploadByBytes(@RequestBody byte[] fileBytes,@RequestParam("extName")String extName){
try {
String filePath = FastDfsApiOpr.upload(fileBytes, extName);
return AjaxResult.me().setSuccess(true).setMessage("上传成功!").setResultObj(filePath);
} catch (Exception e) {
e.printStackTrace();
return AjaxResult.me().setSuccess(false).setMessage("上传失败!");
}
}
/**
* 文件下载
* @param fileUrl 文件路径
* @return
*/
@PostMapping("/download")
public byte[] getTemplateFile(@RequestParam("fileUrl") String fileUrl){
int index = fileUrl.indexOf("/");
fileUrl = StringUtils.startsWith(fileUrl, "/") ?fileUrl.substring(index+1):fileUrl;
String fileName = fileUrl.substring(fileUrl.indexOf("/")+1);
String groupName = fileUrl.substring(0,fileUrl.indexOf("/"));
return FastDfsApiOpr.download(groupName, fileName);
}
}
压缩文件解压工具类
org.apache.ant
ant
1.7.1
package com.hanfengyi.hrm;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Enumeration;
import org.apache.tools.zip.ZipEntry;
import org.apache.tools.zip.ZipFile;
import org.apache.tools.zip.ZipOutputStream;
/**
*
* ZIP工具包
*
*
* 依赖:ant-1.7.1.jar
*
*
* @author IceWee
* @date 2012-5-26
* @version 1.0
*/
public class ZipUtils {
/**
* 使用GBK编码可以避免压缩中文文件名乱码
*/
private static final String CHINESE_CHARSET = "GBK";
/**
* 文件读取缓冲区大小
*/
private static final int CACHE_SIZE = 1024;
/**
*
* 压缩文件
*
*
* @param sourceFolder 压缩文件夹
* @param zipFilePath 压缩文件输出路径
* @throws Exception
*/
public static void zip(String sourceFolder, String zipFilePath) throws Exception {
OutputStream out = new FileOutputStream(zipFilePath);
BufferedOutputStream bos = new BufferedOutputStream(out);
ZipOutputStream zos = new ZipOutputStream(bos);
// 解决中文文件名乱码
zos.setEncoding(CHINESE_CHARSET);
File file = new File(sourceFolder);
String basePath = null;
if (file.isDirectory()) {
basePath = file.getPath();
} else {
basePath = file.getParent();
}
zipFile(file, basePath, zos);
zos.closeEntry();
zos.close();
bos.close();
out.close();
}
/**
*
* 递归压缩文件
*
*
* @param parentFile
* @param basePath
* @param zos
* @throws Exception
*/
private static void zipFile(File parentFile, String basePath, ZipOutputStream zos) throws Exception {
File[] files = new File[0];
if (parentFile.isDirectory()) {
files = parentFile.listFiles();
} else {
files = new File[1];
files[0] = parentFile;
}
String pathName;
InputStream is;
BufferedInputStream bis;
byte[] cache = new byte[CACHE_SIZE];
for (File file : files) {
if (file.isDirectory()) {
zipFile(file, basePath, zos);
} else {
pathName = file.getPath().substring(basePath.length() + 1);
is = new FileInputStream(file);
bis = new BufferedInputStream(is);
zos.putNextEntry(new ZipEntry(pathName));
int nRead = 0;
while ((nRead = bis.read(cache, 0, CACHE_SIZE)) != -1) {
zos.write(cache, 0, nRead);
}
bis.close();
is.close();
}
}
}
/**
*
* 解压压缩包
*
*
* @param zipFilePath 压缩文件路径
* @param destDir 压缩包释放目录
* @throws Exception
*/
public static void unZip(String zipFilePath, String destDir) throws Exception {
ZipFile zipFile = new ZipFile(zipFilePath, CHINESE_CHARSET);
Enumeration> emu = zipFile.getEntries();
BufferedInputStream bis;
FileOutputStream fos;
BufferedOutputStream bos;
File file, parentFile;
ZipEntry entry;
byte[] cache = new byte[CACHE_SIZE];
while (emu.hasMoreElements()) {
entry = (ZipEntry) emu.nextElement();
if (entry.isDirectory()) {
new File(destDir + entry.getName()).mkdirs();
continue;
}
bis = new BufferedInputStream(zipFile.getInputStream(entry));
file = new File(destDir + entry.getName());
parentFile = file.getParentFile();
if (parentFile != null && (!parentFile.exists())) {
parentFile.mkdirs();
}
fos = new FileOutputStream(file);
bos = new BufferedOutputStream(fos, CACHE_SIZE);
int nRead = 0;
while ((nRead = bis.read(cache, 0, CACHE_SIZE)) != -1) {
fos.write(cache, 0, nRead);
}
bos.flush();
bos.close();
fos.close();
bis.close();
}
zipFile.close();
}
}
创建文件工具类
public class CreateFileUtils {
/**
* 创建文件
* @param filePath
* @return
*/
public static File createFile(String filePath){
File file = new File(filePath);
if(!file.exists()){
//创建父路径
if(!file.getParentFile().exists()){
file.getParentFile().mkdirs();
}
try {
file.createNewFile();
} catch (IOException e) {
e.printStackTrace();
}
}
return file;
}
}
需要用到的模板引擎Velocity
org.apache.velocity
velocity-engine-core
2.0
需要用到的模板引擎Velocity工具类
package com.hanfengyi.hrm;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileOutputStream;
import java.io.FileWriter;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.StringWriter;
import java.util.Properties;
import org.apache.velocity.Template;
import org.apache.velocity.VelocityContext;
import org.apache.velocity.app.Velocity;
import org.apache.velocity.app.VelocityEngine;
public class VelocityUtils {
private static Properties p = new Properties();
static {
p.setProperty(Velocity.FILE_RESOURCE_LOADER_PATH, "");
p.setProperty(Velocity.ENCODING_DEFAULT, "UTF-8");
p.setProperty(Velocity.INPUT_ENCODING, "UTF-8");
p.setProperty(Velocity.OUTPUT_ENCODING, "UTF-8");
}
/**
* 返回通过模板,将model中的数据替换后的内容
* @param model
* @param templateFilePathAndName
* @return
*/
public static String getContentByTemplate(Object model, String templateFilePathAndName){
try {
Velocity.init(p);
Template template = Velocity.getTemplate(templateFilePathAndName);
VelocityContext context = new VelocityContext();
context.put("model", model);
StringWriter writer = new StringWriter();
template.merge(context, writer);
String retContent = writer.toString();
writer.close();
return retContent;
} catch (Exception e) {
e.printStackTrace();
}
return "";
}
/**
* 根据模板,静态化model到指定的文件 模板文件中通过访问model来访问设置的内容
*
* @param model
* 数据对象
* @param templateFilePathAndName
* 模板文件的物理路径
* @param targetFilePathAndName
* 目标输出文件的物理路径
*/
public static void staticByTemplate(Object model, String templateFilePathAndName, String targetFilePathAndName) {
try {
Velocity.init(p);
Template template = Velocity.getTemplate(templateFilePathAndName);
VelocityContext context = new VelocityContext();
context.put("model", model);
FileOutputStream fos = new FileOutputStream(targetFilePathAndName);
BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(fos, "UTF-8"));// 设置写入的文件编码,解决中文问题
template.merge(context, writer);
writer.close();
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 静态化内容content到指定的文件
*
* @param content
* @param targetFilePathAndName
*/
public static void staticBySimple(Object content, String targetFilePathAndName) {
VelocityEngine ve = new VelocityEngine();
ve.init(p);
String template = "${content}";
VelocityContext context = new VelocityContext();
context.put("content", content);
StringWriter writer = new StringWriter();
ve.evaluate(context, writer, "", template);
try {
FileWriter fileWriter = new FileWriter(new File(targetFilePathAndName));
fileWriter.write(writer.toString());
fileWriter.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
6. 集成MQ,使用定向的方式发布消息,不声明交换机和队列,在ngnix集成的微服务中声明
//8. 集成RabbitMQ,发布消息:文件在fastdfs上的路径,以及站点路径
Site site = siteMapper.selectById(pager.getSiteId());
Map map = new HashMap<>();
map.put("htmlPath", htmlPathInFastdfs);
map.put("sitePath", pager.getPhysicalPath());
rabbitTemplate.convertAndSend(RabbitMQConstants.EXCHANGE_NAME_DIRECT, site.getSn(), JSON.toJSONString(map));
}
7.ngnix站点(消费者)集成MQ,声明交换机和队列,侦听队列消息,拿到fastdfs上的路径下载输出到站点路径
①:导入MQ的包
org.springframework.boot
spring-boot-starter-amqp
②:创建配置类
@Configuration
public class RabbitMQConfig {
@Value("${pageStatic.routingKey}")
String routingKey;
//交换机
@Bean
public Exchange directExchange(){
return ExchangeBuilder.directExchange(RabbitMQConstants.EXCHANGE_NAME_DIRECT).build();
}
//队列
@Bean
public Queue pageStaticQueue(){
return new Queue(RabbitMQConstants.QUEUE_NAME_PAGE_STATIC);
}
//绑定队列到交换机
@Bean
public Binding smsBinding(){
return BindingBuilder.bind(pageStaticQueue()).to(directExchange()).with(routingKey).noargs();
}
}
③:创建配置文件
eureka:
client:
serviceUrl:
defaultZone: http://localhost:1010/eureka/ #注册中心服务端的注册地址
instance:
prefer-ip-address: true #使用ip进行注册
instance-id: proxy-server:2070 #服务注册到注册中心的id
server:
port: 2070
spring:
application:
name: proxy-server
rabbitmq:
host: 127.0.0.1
port: 5672
username: guest
password: guest
virtualHost: /
listener:
simple:
acknowledge-mode: manual #手动签收
④:消费方法
@Component
public class Consumer {
@Autowired
private FastDFSFignClient fastDFSFignClient;
@RabbitListener(queues = {RabbitMQConstants.QUEUE_NAME_PAGE_STATIC})
public void MQListener(String msg, Message message, Channel channel){
//1. 从map中取出文件在fastdfs上的路径,以及站点路径
System.out.println(msg);
Map map = JSONObject.parseObject(msg, Map.class);
String htmlPath = map.get("htmlPath");
String sitePath = map.get("sitePath");
//2. 通过feign调用fastdfs,把html文件下载下来下载到对应站点路径
byte[] templateFile = fastDFSFignClient.getTemplateFile(htmlPath);
if(templateFile!=null || templateFile.length>0){
try {
FileCopyUtils.copy(templateFile, CreateFileUtils.createFile(sitePath));
//3. 触发手动签收
channel.basicAck(message.getMessageProperties().getDeliveryTag(), true);
} catch (IOException e) {
e.printStackTrace();
}
}
}
}