微服务-页面静态化方案2020-03-02

1.概述

页面静态化:在前端门户网站或者其他的一些页面,比如首页,是需要频繁的访问,这样会对服务器造成很大的压力,使用页面静态化,对页面进行一种类似“缓存”的效果,在页面发生改变的时候(添加、修改、删除数据)的时候,对页面进行静态化,输出到静态服务器ngnix,以后访问该页面,直接访问静态化后的页面,减轻服务器的压力!

页面静态化所适用的场景:

①:页面并发量很高

②:数据不易轻易改变

页面静态化是利用模板加数据生成文件,在微服务中,每个服务都是一台电脑,可以分布在不同的地方,那么数据如何传递,模板文件从哪里获得?

页面静态化应该作为一个独立的微服务,需要做页面静态化的服务直接通过feign来调用即可!

2.方案

2020年2月26日-页面静态化-以前画的.png

1:需要一个后台管理中心,管理需要作页面静态化的页面

1583122890173.png

需要保存的数据,页面名字,最终输出到站点的路径、以及模板文件上传到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();
            }
        }
    }
}

你可能感兴趣的:(微服务-页面静态化方案2020-03-02)