文章详情的两种实现方案
方案一:动态渲染
方案二:静态模版展示
Freemarket概述
FreeMarker 是一款 模板引擎: 即一种基于模板和要改变的数据, 并用来生成输出文本(HTML网页,电子邮件,配置文件,源代码等)的通用工具。 它不是面向最终用户的,而是一个Java类库,是一款程序员可以嵌入他们所开发产品的组件。
技术选型对比
技术 |
说明 |
Jsp |
Jsp 为 Servlet 专用,不能单独进行使用 |
Velocity |
Velocity从2010年更新完 2.0 版本后,7年没有更新。Spring Boot 官方在 1.4 版本后对此也不在支持 |
thmeleaf |
新技术,功能较为强大,但是执行的效率比较低 |
freemarker |
性能好,强大的模板语言、轻量 |
指令语法
基础指令
注释,即<#-- -->
,介于其之间的内容会被freemarker忽略
插值(Interpolation):即${..}
部分,freemarker会用真实的值代替${..}
FTL指令:和HTML标记类似,名字前加#予以区分,Freemarker会解析标签中的表达式或逻辑。
文本,仅文本信息,这些不是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、判断某变量是否存在使用 “??”
用法为:variable??
,如果该变量存在,返回true,否则返回false
<#if stus??> <#list stus as stu> ...... </#list> </#if>
|
2、缺失变量默认值使用 “!”
内建函数
内建函数语法格式: 变量+?+函数名称
1、集合的大小
2、日期格式化
显示年月日: ${today?date} 显示时分秒:${today?time} 显示日期+时间:${today?datetime} 自定义格式化:${today?string("yyyy年MM月")}
|
3、内建函数c
model.addAttribute("point", 102920122);
|
point是数字型,使用${point}会显示这个数字的值,每三位使用逗号分隔。
如果不想显示为每三位分隔的数字,可以使用c函数将数字型转成字符串输出
4、将json字符串转成对象
<#assign text="{'bank':'工商银行','account':'10101920201920212'}" /> <#assign data=text?eval /> 开户行:${data.bank} 账号:${data.account}
|
输出静态化文件
使用freemarker原生Api将页面生成html文件
application.yamlspring: 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"); 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 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");; MinioClient minioClient = MinioClient.builder() .credentials("minio", "minio123") .endpoint("http://192.168.200.130:9000") .build(); PutObjectArgs putObjectArgs = PutObjectArgs.builder() .object("list.html") .contentType("text/html") .bucket("leadnews") .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})
@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") public class MinIOConfigProperties implements Serializable {
private String accessKey; private String secretKey; private String bucket; private String endpoint; private String readPath; }
|
创建包service
和service.impl
,创建接口类FileStorageService
FileStorageService
public interface FileStorageService {
public String uploadImgFile(String prefix, String filename,InputStream inputStream);
public String uploadHtmlFile(String prefix, String filename,InputStream inputStream);
public void delete(String pathUrl);
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; }
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(); }
@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("上传文件失败"); } }
@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("上传文件失败"); } }
@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); 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(); } }
@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 { ApArticleContent apArticleContent = apArticleContentMapper.selectOne( Wrappers.<ApArticleContent>lambdaQuery().eq(ApArticleContent::getArticleId, 1383827787629252610L));
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);
InputStream is = new ByteArrayInputStream(out.toString().getBytes()); String url = fileStorageService.uploadHtmlFile("", apArticleContent.getArticleId() + ".html", is);
ApArticle article = new ApArticle(); article.setId(apArticleContent.getArticleId()); article.setStaticUrl(url); apArticleMapper.updateById(article); } }
|