页面静态化

不做页面静态化,在高并发的情况下,导致页面的响应变慢,因为每次请求的流程都是一样的,所以可以做页面静态化来解决;
页面静态化,就是让第一次请求响应的html缓存,下次请求就不走后台请求流程了

页面静态化使用场景

1.高并发
2.页面数据不经常变化的

页面静态化方案

springboot在静态方法前properties填入数据 springboot 页面静态化_java

1.模板存储

在后台管理页面的时候上传模板到fastdfs,同时保持到数据库。

2.什么时候出发页面静态化

1.手动点击生成触发
2.页面内容改变自动触发

代码实现

项目结构

springboot在静态方法前properties填入数据 springboot 页面静态化_java_02

1.导包

hrm-page-static-server-2060

<!--注册中心客户端-->
       <dependency>
           <groupId>org.springframework.cloud</groupId>
           <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
       </dependency>
       <!--springboot的web支持包-->
       <dependency>
           <groupId>org.springframework.boot</groupId>
           <artifactId>spring-boot-starter-web</artifactId>
       </dependency>

       <dependency>
           <groupId>mysql</groupId>
           <artifactId>mysql-connector-java</artifactId>
       </dependency>
       <dependency>
           <groupId>com.alibaba</groupId>
           <artifactId>druid</artifactId>
       </dependency>

       <dependency>
           <groupId>cn.itsource</groupId>
           <artifactId>hrm-basic-utils</artifactId>
       </dependency>

       <!--集成swagger-->
       <dependency>
           <groupId>io.springfox</groupId>
           <artifactId>springfox-swagger2</artifactId>
       </dependency>
       <!-- https://mvnrepository.com/artifact/io.springfox/springfox-swagger-ui -->
       <dependency>
           <groupId>io.springfox</groupId>
           <artifactId>springfox-swagger-ui</artifactId>
       </dependency>

       <!--配置中心-->
       <dependency>
           <groupId>org.springframework.cloud</groupId>
           <artifactId>spring-cloud-starter-config</artifactId>
       </dependency>

       <dependency>
           <groupId>cn.itsource</groupId>
           <artifactId>hrm-page-static-common</artifactId>
           <version>1.0-SNAPSHOT</version>
       </dependency>

hrm-page-static-common

<!--mybatis-plus-->
           <dependency>
               <groupId>com.baomidou</groupId>
               <artifactId>mybatis-plus-boot-starter</artifactId>
           </dependency>

           <dependency>
               <groupId>cn.itsource</groupId>
               <artifactId>hrm-basic-common</artifactId>
           </dependency>
2.注册到注册中心及基本代码生成
3.页面静态化的CRUD
4.什么时候触发页面静态化

当需要做页面静态化的服务,发生增删改时,页面需要重新生成;
1.准备页面需要的数据,存到redis
2.准备redis的key和页面静态化表的name
3.通过feign调用页面静态化服务,触发页面静态化
4.当前服务要调用页面静态化服务,所以需要写页面静态化服务的feign,调用其中的页面静态化方法pageStatic

//注入feign的接口
    @Autowired
    private IRedisFeignClient redisFeignClient;

    @Autowired
    private IPageStaticFeignClient pageStaticFeignClient;

    //课程页面静态化
    public void PageStaticCourse() {
        //准备页面数据,存到redis
        List<CourseType> treeData = this.selectTreeType();

        //准备页面静态化name
        String pageName = "home";

        //封装数据,调用方封装自己的数据
        Map<String,Object> model = new HashMap<>();
        model.put("courseTypes", treeData);

        // 存到redis
        AjaxResult ajaxResult = redisFeignClient.set(RedisConstant.COURSE_PAGE_STATIC, JSON.toJSONString(model));
        if (!ajaxResult.isSuccess()) {
            throw new RuntimeException("页面静态化数据异常"+ajaxResult.getMessage());
        }
        //redisFeignClient.set(RedisConstant.COURSE_PAGE_STATIC, JSON.toJSONString(treeData));

        //通过feign调用页面静态化服务,来触发页面静态化
        AjaxResult ajax = pageStaticFeignClient.pageStatic(pageName, RedisConstant.COURSE_PAGE_STATIC);
        if (!ajax.isSuccess()) {
            throw new RuntimeException("页面静态化异常"+ajax.getMessage());
        }
    }
5.页面静态化
  • 页面静态化feign接口IPageStaticFeignClient
@FeignClient(value = "page-server",fallbackFactory = PageStaticFallbackFactory.class)
public interface IPageStaticFeignClient {

    //页面静态化
    @RequestMapping(value = "/pager/pageStatic", method = RequestMethod.POST)
    AjaxResult pageStatic(@RequestParam("pageName") String pageName,@RequestParam("redisKey")String redisKey);
}
  • 回调方法
    自定义类实现FallbackFactory
@Component
public class PageStaticFallbackFactory implements FallbackFactory<IPageStaticFeignClient> {
    @Override
    public IPageStaticFeignClient create(final Throwable throwable) {

       return new IPageStaticFeignClient() {

           @Override
           public AjaxResult pageStatic(String pageName, String redisKey) {
               throwable.printStackTrace();
               return AjaxResult.me().setSuccess(false).setMessage("页面静态化异常....");
           }
       };
    }
}
  • 页面静态化的controller
@RestController
@RequestMapping("/pager")
public class PagerController {
    @Autowired
    public IPagerService pagerService;

    /**
     * 保存和修改公用的
     *
     * @param * pager 传递的实体
     * @return Ajaxresult转换结果
     */
    //页面静态化
    @RequestMapping(value = "/pageStatic", method = RequestMethod.POST)
    public AjaxResult pageStatic(@RequestParam("pageName") String pageName,@RequestParam("redisKey")String redisKey) {
        try {
            pagerService.pageStatic(pageName,redisKey);
            return AjaxResult.me();
        } catch (Exception e) {
            e.printStackTrace();
            return AjaxResult.me().setMessage("保存对象失败!" + e.getMessage());
        }
    }
 }
  • 集成fastdfs
    1.添加需要页面静态化的数据时,把模板上传到fastdfs
<!--集成fastdfs-->
       <dependency>
           <groupId>org.csource</groupId>
           <artifactId>fastdfs-client-java</artifactId>
           <version>1.29-SNAPSHOT</version>
       </dependency>

2.fastdfs的feign接口

@FeignClient(value = "fastdfs-server",fallbackFactory = FastDFSFallbackFactory.class)
public interface IFastDFSFeignClient {
	//下载模板
    @PostMapping("/fastdfs/download")
    byte[] download(@RequestParam("path") String path);
	
    //上传html到fastdfs
    @PostMapping("/fastdfs/uploadHtml")
    AjaxResult uploadHtml(@RequestBody byte[] htmlFile, @RequestParam("exName")String exName);
}

3.fastdfs的回调方法

@Component
public class FastDFSFallbackFactory implements FallbackFactory<IFastDFSFeignClient> {
    @Override
    public IFastDFSFeignClient create(final Throwable throwable) {

       return new IFastDFSFeignClient() {

           @Override
           public byte[] download(String path) {
               throwable.printStackTrace();
               return null;
           }

           @Override
           public AjaxResult uploadHtml(byte[] htmlFile, String exName) {
               return null;
           }
       };
    }
}

4.fastdfs的controller

@RestController
@RequestMapping("/fastdfs")
public class FastdfsController {
    //下载模板
    @PostMapping("/download")
    public byte[] download(@RequestParam("path") String path) {
        try {
            // /group1/M00/00/02/dhma1l5Yt8uAStQTAACBNnahKck980.zip
            path = path.substring(1);
            int index = path.indexOf("/");
            String groupName = path.substring(0, index);
            String fileName = path.substring(index + 1);
            System.out.println(groupName);
            System.out.println(fileName);
            byte[] download = FastDfsApiOpr.download(groupName, fileName);
            return download;

        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }
    //上传html到fastdfs
    @PostMapping("/uploadHtml")
    public AjaxResult uploadHtml(@RequestBody byte[] htmlFile,@RequestParam("exName")String exName) {
        try {
            String upload = FastDfsApiOpr.upload(htmlFile, exName);
            //上传成功返回图片路径给前台,前台在发送请求再将图片保存到数据库
            return AjaxResult.me().setResultObj(upload);
        } catch (Exception e) {
            e.printStackTrace();
            return AjaxResult.me().setSuccess(false).setMessage("文件上传失败");
        }
    }
}
  • 集成RabbitMQ
    需要集成RabbitMQ向nginx发送消息
<!--集成rabbitmq-->
       <dependency>
           <groupId>org.springframework.boot</groupId>
           <artifactId>spring-boot-starter-amqp</artifactId>
       </dependency>

谁集成MQ,配置文件也要配置

spring:
  application:
    name: page-server  #服务名称
  rabbitmq:
    host: 127.0.0.1
    port: 5672
    username: guest
    password: guest
    virtualHost: /
  • 页面静态化实现类
    1.课程服务调用页面静态化方法,将需要页面静态化的数据传递到页面静态化的方法中,
    2.通过redis的key,取出上传到fastdfs的资源路径,进行下载
    3.保存到临时目录中,进行解压
    4.解压之后,合并模板
    5.然后将html模板上传到fastdfs,nginx通过路径,下载模板
@Service
public class PagerServiceImpl extends ServiceImpl<PagerMapper, Pager> implements IPagerService {

    @Autowired
    private IFastDFSFeignClient fastDFSFeignClient;

    @Autowired
    private IRedisFeignClient redisFeignClient;

    @Autowired
    private RabbitTemplate rabbitTemplate;

    @Autowired
    private ISiteService siteService;

    //获取临时目录
    public String getTempDir() {
        //C:\Users\yddn\AppData\Local\Temp\
        String tempDir = System.getProperty("java.io.tmpdir");
        return tempDir;
    }


    @Override
    public void pageStatic(String pageName, String redisKey) throws Exception {
        //判断
        if (!StringUtils.hasLength(pageName) || !StringUtils.hasLength(redisKey)) {
            throw new RuntimeException("页面静态化异常");
        }
        //准备模板 根据pageName查询pager对象
        Pager pager = baseMapper.findByName(pageName);

        //根据查询出来的pager对象获取路径,去fastdfs下载模板
        String templateUrl = pager.getTemplateUrl();

        //根据路径下载模板
        byte[] templateZip = fastDFSFeignClient.download(templateUrl);
        if (templateZip.length == 0 || templateZip == null) {
            throw new RuntimeException("下载失败");
        }

        //把下载的模板写到磁盘 获取临时目录  C:\Users\yddn\AppData\Local\Temp\
        //拼接路径及zip文件名
        String tempZipDir = getTempDir()+pageName+".zip";

        //创建文件
        File zipFile = MyFileUtil.createFile(tempZipDir);

        //将文件下载到磁盘
        FileCopyUtils.copy(templateZip, zipFile);

        //把下载的zip模板进行解压
        String unZipDir = getTempDir() + "pagestatic/";
        //tempZipDir 文件的地址 unZip解压到什么地方
        ZipUtils.unZip(tempZipDir, unZipDir);

        //redis通过rediskey获取数据
        AjaxResult ajaxResult = redisFeignClient.get(redisKey);
        if (!ajaxResult.isSuccess() || ajaxResult.getResultObj() == null) {
            throw new RuntimeException("redis数据异常");
        }

        //模板需要两个数据 staticRoot 和courseType
        //获取redis中的数据 courseType是字符串,前台需要list,所以直接子课程服务存对应类型的数据到redis
        String courseType = ajaxResult.getResultObj().toString();
        //staticRoot 就是zip压缩文件解压后的路径  unZipDir

        //合并模板  把字符串转成map
        Map<String, Object> model = JSON.parseObject(courseType,Map.class );
        //unZip 解压路径作为前台staticRoot的值
        model.put("staticRoot", unZipDir);

        //将解压后的home.vm模板保存到  C:\Users\yddn\AppData\Local\Temp\pagestatic
        String vmTemplate = unZipDir + pageName + ".vm";

        //将解压后的home.html模板保存
        String htmlTemplate = unZipDir + pageName + ".html";

        //合并模板
        VelocityUtils.staticByTemplate(model,vmTemplate , htmlTemplate);

        //将html上传到fastdfs
        byte[] htmlBytes = FileCopyUtils.copyToByteArray(new File(htmlTemplate));
        AjaxResult result = fastDFSFeignClient.uploadHtml(htmlBytes, "html");
        if (!result.isSuccess() || result.getResultObj() == null) {
            throw new RuntimeException("html上传失败");
        }

        //返回的html地址
        String htmlResult = result.getResultObj().toString();

        //nigix的物理路径
        String physicalPath = pager.getPhysicalPath()+"/"+pageName+".html";

        //消息内容
        Map<String, String> map = new HashMap<>();
        map.put("htmlResult", htmlResult);
        map.put("physicalPath", physicalPath);

        //获取routingkey 当前pager的站点的sn
        String routingkey = siteService.selectById(pager.getSiteId()).getSn();
        //集成MQ,发送消息
        rabbitTemplate.convertAndSend(MQConstant.EXCHANGE_NAME_PAGE_DRICT, routingkey, JSON.toJSONString(map));

    }
}
6.nginx接收消息

在页面静态化中,通过MQ发送消息到nginx,nginx通过获取路径,下载模板;

1.新建模板

springboot在静态方法前properties填入数据 springboot 页面静态化_spring_03


2.需要注册到注册中心以及开启feign 3.依赖fastdfs的feign以及集成MQ

配置文件

spring:
  application:
    name: proxy-server  #服务名称
  rabbitmq:
    host: 127.0.0.1
    port: 5672
    username: guest
    password: guest
    virtualHost: /
    listener:
      simple:
        acknowledge-mode: manual #手动签收
#配置站点 通过routingkey绑定
site:
  routingkey: hrmCourseSite

4.声明交换机及队列

@Configuration
public class RabbitMQConfig {

    @Value("${site.routingkey}")//routingkey从配置文件获取
    private String routingkey;

    //定义交换机
    @Bean(MQConstant.EXCHANGE_NAME_PAGE_DRICT)
    public Exchange exchange() {
        //durable(true)持久化,消息队列重启后交换机仍然存在
        return ExchangeBuilder.topicExchange(MQConstant.EXCHANGE_NAME_PAGE_DRICT).durable(true).build();
    }
    /*******************************************************************/
    //声明队列
    @Bean(MQConstant.QUEUE_NAME_PAGE_DRICT)
    public Queue queue() {
        Queue queue = new Queue(MQConstant.QUEUE_NAME_PAGE_DRICT,true);
        return queue;
    }

    //绑定队列到交换机
    @Bean
    public Binding BINDING_QUEUE_INFORM_EMAIL() {
        return BindingBuilder.bind(queue()).to(exchange()).with(routingkey).noargs();
    }
}

5.nginx接收消息

@Component
public class PageStaticHandle {

    @Autowired
    private IFastDFSFeignClient fastDFSFeignClient;
    //监听队列  queues可以监听多个队列
    @RabbitListener(queues = {MQConstant.QUEUE_NAME_PAGE_DRICT})
    public void pageStatic(String msg, Message message, Channel channel) throws IOException {

        //接收数据 将传递过来的字符串转成map
        Map<String, String> map = JSON.parseObject(msg,Map.class );

        //获取html路径
        String htmlResult = map.get("htmlResult");
        //获取nigix路径
        String physicalPath = map.get("physicalPath");

        //从fastdfs下载
        byte[] result = fastDFSFeignClient.download(htmlResult);
        if (result != null) {
            //创建nginx的物理路径
            File file = MyFileUtil.createFile(physicalPath);
            //将html模板拷贝到本地nginx中
            FileCopyUtils.copy(result, file);
            //手动签收  true 可能有多个消费者,等所以消费者所以消费完之后,再签收
            channel.basicAck(message.getMessageProperties().getDeliveryTag(), true);
        }
    }
}
7.需要用到的工具类

1.MyFileUtil 创建文件的工具类

public class MyFileUtil {
    //创建文件
    public static File createFile(String path){
        try {
            File file = new File(path);
            if(!file.exists()){
                File parentFile = file.getParentFile();
                if(!parentFile.exists()){
                    //创建多级父目录
                    parentFile.mkdirs();
                }
                //创建文件
                file.createNewFile();
            }
            return file;
        } catch (IOException e) {
            e.printStackTrace();
        }
        return null;
    }
}

2.合并模板的工具类VelocityUtils
导包

<!--velocity jar包-->
        <dependency>
            <groupId>org.apache.velocity</groupId>
            <artifactId>velocity</artifactId>
            <version>1.7</version>
        </dependency>
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();
		}
	}
}

3.解压的工具类ZipUtils

导包

<!--解压 jar包-->
        <dependency>
            <groupId>ant</groupId>
            <artifactId>ant</artifactId>
            <version>1.7.0</version>
        </dependency>
public class ZipUtils {
    
    /**
     * 使用GBK编码可以避免压缩中文文件名乱码
     */
    private static final String CHINESE_CHARSET = "GBK";
    
    /**
     * 文件读取缓冲区大小
     */
    private static final int CACHE_SIZE = 1024;
    
    /**
     * <p>
     * 压缩文件
     * </p>
     * 
     * @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();
    }
    
    /**
     * <p>
     * 递归压缩文件
     * </p>
     * 
     * @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();
            }
        }
    }
    
    /**
     * <p>
     * 解压压缩包
     * </p>
     * 
     * @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();
    }
  
}