文章详情的两种实现方案

方案一:动态渲染

方案二:静态模版展示

Freemarket概述

FreeMarker 是一款 模板引擎: 即一种基于模板和要改变的数据, 并用来生成输出文本(HTML网页,电子邮件,配置文件,源代码等)的通用工具。 它不是面向最终用户的,而是一个Java类库,是一款程序员可以嵌入他们所开发产品的组件。

技术选型对比

技术 说明
Jsp Jsp 为 Servlet 专用,不能单独进行使用
Velocity Velocity从2010年更新完 2.0 版本后,7年没有更新。Spring Boot 官方在 1.4 版本后对此也不在支持
thmeleaf 新技术,功能较为强大,但是执行的效率比较低
freemarker 性能好,强大的模板语言、轻量

指令语法

基础指令

注释,即<#-- -->,介于其之间的内容会被freemarker忽略

<#--我是一个freemarker注释-->

插值(Interpolation):即${..}部分,freemarker会用真实的值代替${..}

Hello ${name} 

FTL指令:和HTML标记类似,名字前加#予以区分,Freemarker会解析标签中的表达式或逻辑。

<# >FTL指令</#>

文本,仅文本信息,这些不是freemarker的注释、插值、FTL指令的内容会被freemarker忽略解析,直接输出内容。

<#--freemarker中的普通文本-->
我是一个普通的文本

List指令

<#list></#list>

<#list stus as stu>
<tr>
<td>${stu_index+1}</td>
<td>${stu.name}</td>
<td>${stu.age}</td>
<td>${stu.money}</td>
</tr>
</#list>

${k_index}:得到循环的下标,使用方法是在stu后边加”_index”,它的值是从0开始

Map指令

获取map中的值

map['keyname'].property
map.keyname.property

<#list userMap?keys as key>
key:${key}--value:${userMap["${key}"]}
</#list>
if指令
<#if expression>
<#else>
</#if>

需求:在list集合中判断学生为小红的数据字体显示为红色。

<#if stu.name='小红'>
<tr style="color: red">
<td>${stu_index}</td>
<td>${stu.name}</td>
<td>${stu.age}</td>
<td>${stu.money}</td>
</tr>
<#else >
<tr>
<td>${stu_index}</td>
<td>${stu.name}</td>
<td>${stu.age}</td>
<td>${stu.money}</td>
</tr>
</#if>

在freemarker中,判断是否相等,=与==是一样的

运算符

算术运算符

FreeMarker表达式中完全支持算术运算,FreeMarker支持的算术运算符包括:

① 加法: +

② 减法: -

③ 乘法: *****

④ 除法: /

求模 (求余): %

比较运算符

比较运算符 说明
=或者== 判断两个值是否相等
!= 判断两个值是否不等
>或者gt 判断左边值是否大于右边值
>=或者gte 判断左边值是否大于等于右边值
<或者lt 判断左边值是否小于右边值
<=或者lte 判断左边值是否小于等于右边值
  • =和!=可以用于字符串、数值和日期来比较是否相等

  • =和!=两边必须是相同类型的值,否则会产生错误

  • 字符串 “x” 、”x “ 、”X”比较是不等的.因为FreeMarker是精确比较

  • gt代替>, FreeMarker会把>解释成FTL标签的结束字符,可使用括号避免这种情况,如:<#if (x>y)>

逻辑运算符

  1. 逻辑与:**&&**

  2. 逻辑或:**||**

  3. 逻辑非:**!**

空值处理

1、判断某变量是否存在使用 “??”

用法为:variable??,如果该变量存在,返回true,否则返回false

<#if stus??>
<#list stus as stu>
......
</#list>
</#if>

2、缺失变量默认值使用 “!”

  • 使用!要以指定一个默认值,当变量为空时显示默认值

    例:${name!''}表示如果name为空显示空字符串。

  • 如果是嵌套对象则建议使用’()’括起来

    例:${(stu.name)!''}表示,如果stu或name为空默认显示空字符串。

内建函数

内建函数语法格式: 变量+?+函数名称

1、集合的大小

${集合名?size}

2、日期格式化

显示年月日: ${today?date}
显示时分秒:${today?time}
显示日期+时间:${today?datetime}
自定义格式化:${today?string("yyyy年MM月")}

3、内建函数c

model.addAttribute("point", 102920122);

point是数字型,使用${point}会显示这个数字的值,每三位使用逗号分隔。

如果不想显示为每三位分隔的数字,可以使用c函数将数字型转成字符串输出

${point?c}

4、将json字符串转成对象

<#assign text="{'bank':'工商银行','account':'10101920201920212'}" />
<#assign data=text?eval />
开户行:${data.bank} 账号:${data.account}

输出静态化文件

使用freemarker原生Api将页面生成html文件

application.yaml
spring:
suffix: .ftl
template-loader-path: classpath:/templates

测试

public class FreemarkerTest {

@Autowired
private Configuration configuration;

public void test() throws IOException {
Template template = configuration.getTemplate("test.ftl");

// arg1: 模型数据,arg2: 输出流
template.process(getData(), new FileWriter("/temp/test.html"));
}

private Map<String, Object> getData() {
Map<String, Object> model = new HashMap<>();
map.put("test", "test");
map.put("date", new Date());
return map;
}
}

MinIO简介

对象存储的方式对比

存储方式 优点 缺点
服务器磁盘 开发便捷,成本低 扩展困难
分布式文件系统 容易实现扩容 复杂度高
第三方存储 开发简单,功能强大,免维护 收费

分布式文件系统

存储方式 优点 缺点
FastDFS 1. 主备服务,高可用 2. 支持主从文件,支持自定义扩展名 3. 支持动态扩容 1. 没有完备官方文档,近几年没有更新 2. 环境搭建较为麻烦
MinIO 1. 性能高,准硬件条件下它能达到55GB/s的读、35GB/s的写速率 2. 部署自带管理界面 3. MinIO.Inc运营的开源项目,社区活跃度高 4. 提供了所有主流开发语言的SDK 1. 不支持动态增加节点

MinIO基于Apache License v2.0开源协议的对象存储服务,可以做为云存储的解决方案用来保存海量的图片,视频,文档。

uGolang语言实现,配置简单,单行命令可以运行起来。

uMinIO兼容亚马逊S3云存储服务接口,适合于存储大容量非结构化的数据,一个对象文件可以是任意大小,从几kb到最大5T不等。

u官网文档:http://docs.minio.org.cn/docs/

食用教程

①:拉取镜像

docker pull minio/minio

②:创建容器

docker run -p 9000:9000 -p 9001:9001 \
--name minio \
-d --restart=always \
-e "MINIO_ACCESS_KEY=minio" \
-e "MINIO_SECRET_KEY=minio123" \
-v /home/data:/data \
-v /home/config:/root/.minio \
minio/minio server /data \
--console-address ":9001"

③:访问minio系统

http://ip:9001

快速入门

目标:把list.html文件上传到minio中,并且可以在浏览器中访问

public static void main(String[] args) {
FileInputStream fileInputStream = null;
try {
fileInputStream = new FileInputStream("D:\\list.html");;
//1.创建minio链接客户端
MinioClient minioClient = MinioClient.builder()
.credentials("minio", "minio123")
.endpoint("http://192.168.200.130:9000")
.build();
//2.上传
PutObjectArgs putObjectArgs = PutObjectArgs.builder()
.object("list.html")//文件名
.contentType("text/html")//文件类型
.bucket("leadnews")//桶名词 与minio创建的名词一致
.stream(fileInputStream, fileInputStream.available(), -1) //文件流
.build();
minioClient.putObject(putObjectArgs);
System.out.println("http://192.168.200.130:9000/leadnews/list.html");
} catch (Exception ex) {
ex.printStackTrace();
}
}

封装MinIO为starter

创建子模块file-starter

引入依赖

pom.xml
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-autoconfigure</artifactId>
</dependency>
<dependency>
<groupId>io.minio</groupId>
<artifactId>minio</artifactId>
<version>7.1.0</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
</dependencies>

创建包com.swx.file

创建包config,并在包下创建两个配置文件

MinIOConfig

config.MinIOConfig
@Configuration
@EnableConfigurationProperties({MinIOConfigProperties.class})
//当引入FileStorageService接口时
@ConditionalOnClass(FileStorageService.class)
public class MinIOConfig {

@Autowired
private MinIOConfigProperties minIOConfigProperties;

@Bean
public MinioClient buildMinioClient() {
return MinioClient
.builder()
.credentials(minIOConfigProperties.getAccessKey(), minIOConfigProperties.getSecretKey())
.endpoint(minIOConfigProperties.getEndpoint())
.build();
}
}

ConfigurationProperties

ConfigurationProperties
@Data
@ConfigurationProperties(prefix = "minio") // 文件上传 配置前缀file.oss
public class MinIOConfigProperties implements Serializable {

private String accessKey;
private String secretKey;
private String bucket;
private String endpoint;
private String readPath;
}

创建包serviceservice.impl,创建接口类FileStorageService

FileStorageService
/**
* @author itheima
*/
public interface FileStorageService {


/**
* 上传图片文件
* @param prefix 文件前缀
* @param filename 文件名
* @param inputStream 文件流
* @return 文件全路径
*/
public String uploadImgFile(String prefix, String filename,InputStream inputStream);

/**
* 上传html文件
* @param prefix 文件前缀
* @param filename 文件名
* @param inputStream 文件流
* @return 文件全路径
*/
public String uploadHtmlFile(String prefix, String filename,InputStream inputStream);

/**
* 删除文件
* @param pathUrl 文件全路径
*/
public void delete(String pathUrl);

/**
* 下载文件
* @param pathUrl 文件全路径
* @return
*
*/
public byte[] downLoadFile(String pathUrl);

}

实现接口

@Slf4j
@EnableConfigurationProperties(MinIOConfigProperties.class)
@Import(MinIOConfig.class)
public class MinIOFileStorageService implements FileStorageService {

private final MinioClient minioClient;
private final MinIOConfigProperties minIOConfigProperties;

private final static String separator = "/";

public MinIOFileStorageService(MinioClient minioClient, MinIOConfigProperties minIOConfigProperties) {
this.minioClient = minioClient;
this.minIOConfigProperties = minIOConfigProperties;
}

/**
* @param dirPath
* @param filename yyyy/mm/dd/file.jpg
* @return
*/
public String builderFilePath(String dirPath,String filename) {
StringBuilder stringBuilder = new StringBuilder(50);
if(!StringUtils.isEmpty(dirPath)){
stringBuilder.append(dirPath).append(separator);
}
SimpleDateFormat sdf = new SimpleDateFormat("yyyy/MM/dd");
String todayStr = sdf.format(new Date());
stringBuilder.append(todayStr).append(separator);
stringBuilder.append(filename);
return stringBuilder.toString();
}

/**
* 上传图片文件
* @param prefix 文件前缀
* @param filename 文件名
* @param inputStream 文件流
* @return 文件全路径
*/
@Override
public String uploadImgFile(String prefix, String filename,InputStream inputStream) {
String filePath = builderFilePath(prefix, filename);
try {
PutObjectArgs putObjectArgs = PutObjectArgs.builder()
.object(filePath)
.contentType("image/jpg")
.bucket(minIOConfigProperties.getBucket()).stream(inputStream,inputStream.available(),-1)
.build();
minioClient.putObject(putObjectArgs);
StringBuilder urlPath = new StringBuilder(minIOConfigProperties.getReadPath());
urlPath.append(separator+minIOConfigProperties.getBucket());
urlPath.append(separator);
urlPath.append(filePath);
return urlPath.toString();
}catch (Exception ex){
log.error("minio put file error.",ex);
throw new RuntimeException("上传文件失败");
}
}

/**
* 上传html文件
* @param prefix 文件前缀
* @param filename 文件名
* @param inputStream 文件流
* @return 文件全路径
*/
@Override
public String uploadHtmlFile(String prefix, String filename,InputStream inputStream) {
String filePath = builderFilePath(prefix, filename);
try {
PutObjectArgs putObjectArgs = PutObjectArgs.builder()
.object(filePath)
.contentType("text/html")
.bucket(minIOConfigProperties.getBucket()).stream(inputStream,inputStream.available(),-1)
.build();
minioClient.putObject(putObjectArgs);
StringBuilder urlPath = new StringBuilder(minIOConfigProperties.getReadPath());
urlPath.append(separator+minIOConfigProperties.getBucket());
urlPath.append(separator);
urlPath.append(filePath);
return urlPath.toString();
}catch (Exception ex){
log.error("minio put file error.",ex);
ex.printStackTrace();
throw new RuntimeException("上传文件失败");
}
}

/**
* 删除文件
* @param pathUrl 文件全路径
*/
@Override
public void delete(String pathUrl) {
String key = pathUrl.replace(minIOConfigProperties.getEndpoint()+"/","");
int index = key.indexOf(separator);
String bucket = key.substring(0,index);
String filePath = key.substring(index+1);
// 删除Objects
RemoveObjectArgs removeObjectArgs = RemoveObjectArgs.builder().bucket(bucket).object(filePath).build();
try {
minioClient.removeObject(removeObjectArgs);
} catch (Exception e) {
log.error("minio remove file error. pathUrl:{}",pathUrl);
e.printStackTrace();
}
}


/**
* 下载文件
* @param pathUrl 文件全路径
* @return 文件流
*
*/
@Override
public byte[] downLoadFile(String pathUrl) {
String key = pathUrl.replace(minIOConfigProperties.getEndpoint()+"/","");
int index = key.indexOf(separator);
String bucket = key.substring(0,index);
String filePath = key.substring(index+1);
InputStream inputStream = null;
try {
inputStream = minioClient.getObject(GetObjectArgs.builder().bucket(minIOConfigProperties.getBucket()).object(filePath).build());
} catch (Exception e) {
log.error("minio down file error. pathUrl:{}",pathUrl);
e.printStackTrace();
}

ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
byte[] buff = new byte[100];
int rc = 0;
while (true) {
try {
if (!((rc = inputStream.read(buff, 0, 100)) > 0)) break;
} catch (IOException e) {
e.printStackTrace();
}
byteArrayOutputStream.write(buff, 0, rc);
}
return byteArrayOutputStream.toByteArray();
}
}

自动装配

创建/resources/META-INF/spring.factories,写入如下内容:

org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.swx.file.service.impl.MinIOFileStorageService

使用教程

public class MinIOTest {

@Autowired
private FileStorageService fileStorageService;

public void test() thorws FileNotFoundException {
FileInputStream fileInputStream = new FileInputStream("/temp/test.html");
String url = fileStorageService.uploadHtmlFile("", "test.html");
System.out.println(url);
}
}

集成Freemarket和MinIO

1.在article微服务中添加MinIO和freemarker的支持

pom.xml
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-freemarker</artifactId>
</dependency>
<dependency>
<groupId>com.swx</groupId>
<artifactId>leadnews-file-starter</artifactId>
</dependency>
</dependencies>

记得在父工程的pom文件中添加版本依赖

将如下配置加入到leadnews-article的Nacos配置中,发布:

minio:
accessKey: minio
secretKey: minio123
bucket: leadnews
endpoint: http://ip:9000
readPath: http://ip:9000

2.下载模板文件(article.ftl)拷贝到article微服务下

下载地址:https://wwab.lanzoue.com/ic4iE14ki4qj

3.下载模版文件,解压后将plugins目录手动上传到MinIO中

下载地址:https://wwab.lanzoue.com/ic4iE14ki4qj

在MinIO中创建Buckets名字为leadnews,并将Access Ploicy设置为Public

leadnews中上传plugins目录

生成静态模版

创建测试类,后续添加时会在项目中创建静态模版,这里先使用测试类生成

ArticleFreemarkerTest
@SpringBootTest(classes = ArticleApplication.class)
@RunWith(SpringRunner.class)
public class ArticleFreemarkerTest {

@Autowired
private ApArticleContentMapper apArticleContentMapper;

@Autowired
Configuration configuration;

@Autowired
private FileStorageService fileStorageService;

@Autowired
private ApArticleMapper apArticleMapper;

@Test
public void createStaticUrlTest() throws Exception {
// 1. 获取文章内容
ApArticleContent apArticleContent = apArticleContentMapper.selectOne(
Wrappers.<ApArticleContent>lambdaQuery().eq(ApArticleContent::getArticleId, 1383827787629252610L));

// 文章内容通过freemarker生成html文件
Template template = configuration.getTemplate("article.ftl");
StringWriter out = new StringWriter();
// 数据模型
HashMap<String, Object> params = new HashMap<>();
params.put("content", JSONArray.parseArray(apArticleContent.getContent()));
// 合成
template.process(params, out);

// 把html文件上传到minio中
InputStream is = new ByteArrayInputStream(out.toString().getBytes());
String url = fileStorageService.uploadHtmlFile("", apArticleContent.getArticleId() + ".html", is);

// 修改ap_article表,保存static_url字段
ApArticle article = new ApArticle();
article.setId(apArticleContent.getArticleId());
article.setStaticUrl(url);
apArticleMapper.updateById(article);
}
}