页面静态化
不做页面静态化,在高并发的情况下,导致页面的响应变慢,因为每次请求的流程都是一样的,所以可以做页面静态化来解决;
页面静态化,就是让第一次请求响应的html缓存,下次请求就不走后台请求流程了
页面静态化使用场景
1.高并发
2.页面数据不经常变化的
页面静态化方案
1.模板存储
在后台管理页面的时候上传模板到fastdfs,同时保持到数据库。
2.什么时候出发页面静态化
1.手动点击生成触发
2.页面内容改变自动触发
代码实现
项目结构
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.新建模板
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();
}
}