学习内容:
一、app端文章列表
1. 需求分析
文章的布局展示
2. 导入文章数据库
可以使用IDEA的数据库连接工具执行SQL脚本
3. 表结构分析
ap_article文章基本信息表
ap_article_config文章配置表
ap_article_content文章内容表
把课前资料里提供的实体类复制到model模块下的如下位置
4. 表的拆分-垂直分表
垂直分表:将一个表的字段分散到多个表中,每个表存储其中一部分字段。
优势:
- 减少IO争抢,减少锁表的几率,查看文章概述与文章详情互不影响
- 充分发挥高频数据的操作效率,对文章概述数据操作的高效率不会被操作文章详情数据的低效率所拖累。
拆分规则:
- 把不常用的字段单独放在一张表
- 把text、blob等大字段拆分出来单独放在一张表
- 经常组合查询的字段单独放在一张表中
5. 实现思路
- 1. 在默认频道展示10条文章信息
- 2. 可以切换频道查看不同种类文章
- 3. 当用户下拉可以加载最新的文章(分页)本页文章列表中发布时间为最大的的时间为依据
- 4. 当用户上拉可以加载更多的文章信息(按照发布时间)本页文章列表中发布时间最小的时间为依据
- 5. 如果是当前频道的首页,前端传递默认参数:maxBehotTime:0(毫秒)、minBehotTime:20000000000000(毫秒)2063年
6. 接口定义
7. 实现步骤
步骤①:把课前资料里提供的文章微服务复制到service模块下,如图
然后,在pom.xml(heima-leadnews-service)中添加模块声明,刷新maven
<modules>
<module>heima-leadnews-user</module>
<module>heima-leadnews-article</module>
</modules>
②在nacos中新建配置leadnews-article
spring:
datasource:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://localhost:3306/leadnews_article?useUnicode=true&characterEncoding=UTF-8&serverTimezone=UTC
username: root
password: 123456
# 设置Mapper接口所对应的XML文件位置,如果你在Mapper接口中有自定义方法,需要进行该配置
mybatis-plus:
mapper-locations: classpath*:mapper/*.xml
# 设置别名包扫描路径,通过该属性可以给包中的类注册别名
type-aliases-package: com.heima.model.article.pojos
③在model模块下的article新建ArticleHomeDto
package com.heima.model.article.dtos;
import lombok.Data;
import java.util.Date;
@Data
public class ArticleHomeDto {
// 最大时间
Date maxBehotTime;
// 最小时间
Date minBehotTime;
// 分页size
Integer size;
// 频道ID
String tag;
}
④在article模块中定义接口
package com.heima.article.controller.v1;
import com.heima.model.article.dtos.ArticleHomeDto;
import com.heima.model.common.dtos.ResponseResult;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/api/v1/article")
@Api("app端文章列表")
public class ArticleHomeController {
/**
* 加载首页
* @param dto
* @return
*/
@ApiOperation("加载首页")
@PostMapping("/load")
public ResponseResult load(@RequestBody ArticleHomeDto dto) {
return null;
}
/**
* 加载更多
* @param dto
* @return
*/
@ApiOperation("加载更多")
@PostMapping("/loadmore")
public ResponseResult loadmore(@RequestBody ArticleHomeDto dto) {
return null;
}
/**
* 加载最新
* @param dto
* @return
*/
@ApiOperation("加载最新")
@PostMapping("/loadnew")
public ResponseResult loadnew(@RequestBody ArticleHomeDto dto) {
return null;
}
}
⑤编写mapper
package com.heima.article.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.heima.model.article.dtos.ArticleHomeDto;
import com.heima.model.article.pojos.ApArticle;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.List;
@Mapper
public interface ApArticleMapper extends BaseMapper<ApArticle> {
/**
* 加载文章列表
* @param dto
* @param type 1:加载更多,2:加载最新
* @return
*/
public List<ApArticle> loadArticleList(@Param("dto") ArticleHomeDto dto, @Param("type") Short type);
}
⑥在resources中新建mapper/ApArticleMapper.xml,配置如下
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.heima.article.mapper.ApArticleMapper">
<resultMap id="resultMap" type="com.heima.model.article.pojos.ApArticle">
<id column="id" property="id"/>
<result column="title" property="title"/>
<result column="author_id" property="authorId"/>
<result column="author_name" property="authorName"/>
<result column="channel_id" property="channelId"/>
<result column="channel_name" property="channelName"/>
<result column="layout" property="layout"/>
<result column="flag" property="flag"/>
<result column="images" property="images"/>
<result column="labels" property="labels"/>
<result column="likes" property="likes"/>
<result column="collection" property="collection"/>
<result column="comment" property="comment"/>
<result column="views" property="views"/>
<result column="province_id" property="provinceId"/>
<result column="city_id" property="cityId"/>
<result column="county_id" property="countyId"/>
<result column="created_time" property="createdTime"/>
<result column="publish_time" property="publishTime"/>
<result column="sync_status" property="syncStatus"/>
<result column="static_url" property="staticUrl"/>
</resultMap>
<select id="loadArticleList" resultMap="resultMap">
SELECT
aa.*
FROM
`ap_article` aa
LEFT JOIN ap_article_config aac ON aa.id = aac.article_id
<where>
and aac.is_delete != 1
and aac.is_down != 1
<!-- loadmore -->
<if test="type != null and type == 1">
and aa.publish_time <![CDATA[<]]> #{dto.minBehotTime}
</if>
<if test="type != null and type == 2">
and aa.publish_time <![CDATA[>]]> #{dto.maxBehotTime}
</if>
<if test="dto.tag != '__all__'">
and aa.channel_id = #{dto.tag}
</if>
</where>
order by aa.publish_time desc
limit #{dto.size}
</select>
</mapper>
<![CDATA[ Your Text Here ]]>,用于定义不需要进行解析的文本数据块。在XML中,当文本内容包含特殊字符,如<,>,&等时,可以使用CDATA部分来告诉解析器不要解析其中的内容。这样做可以避免特殊字符与XML标签混淆,确保文本内容正确显示。
⑦编写业务层代码
在common模块添加常量类ArticleConstants
package com.heima.common.constants;
public class ArticleConstants {
public static final Short LOADTYPE_LOAD_MORE = 1;
public static final Short LOADTYPE_LOAD_NEW = 2;
public static final String DEFAULT_TAG = "__all__";
}
ApArticleService
package com.heima.article.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.heima.model.article.dtos.ArticleHomeDto;
import com.heima.model.article.pojos.ApArticle;
import com.heima.model.common.dtos.ResponseResult;
public interface ApArticleService extends IService<ApArticle> {
/**
* 加载文章列表
* @param dto
* @param type 1:加载更多,2:加载最新
* @return
*/
public ResponseResult load(ArticleHomeDto dto, Short type);
}
ApArticleServiceImpl
package com.heima.article.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.heima.article.mapper.ApArticleMapper;
import com.heima.article.service.ApArticleService;
import com.heima.common.constants.ArticleConstants;
import com.heima.model.article.dtos.ArticleHomeDto;
import com.heima.model.article.pojos.ApArticle;
import com.heima.model.common.dtos.ResponseResult;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.Date;
import java.util.List;
@Service
@Transactional
@Slf4j
public class ApArticleServiceImpl extends ServiceImpl<ApArticleMapper, ApArticle> implements ApArticleService {
@Autowired
private ApArticleMapper apArticleMapper;
// 单页最大加载的数字
private final static short MAX_PAGE_SIZE = 50;
/**
* 加载文章列表
*
* @param dto
* @param type 1:加载更多,2:加载最新
* @return
*/
@Override
public ResponseResult load(ArticleHomeDto dto, Short type) {
// 1. 校验参数
// 1.1 分页条数的校验
Integer size = dto.getSize();
if (size == null || size == 0) {
size = 10;
}
// 分页的值不能超过50
size = Math.min(size, MAX_PAGE_SIZE);
dto.setSize(size);
// 1.2 类型参数type的校验
if (!type.equals(ArticleConstants.LOADTYPE_LOAD_MORE) && !type.equals(ArticleConstants.LOADTYPE_LOAD_NEW)) {
type = ArticleConstants.LOADTYPE_LOAD_MORE;
}
// 1.3 文章频道校验
if (StringUtils.isBlank(dto.getTag())) {
dto.setTag(ArticleConstants.DEFAULT_TAG);
}
// 1.4 时间校验
if (dto.getMaxBehotTime() == null) dto.setMaxBehotTime(new Date());
if (dto.getMinBehotTime() == null) dto.setMinBehotTime(new Date());
// 2. 查询数据
List<ApArticle> articleList = apArticleMapper.loadArticleList(dto, type);
// 3. 结果返回
return ResponseResult.okResult(articleList);
}
}
⑧编写控制器代码
package com.heima.article.controller.v1;
import com.heima.article.service.ApArticleService;
import com.heima.common.constants.ArticleConstants;
import com.heima.model.article.dtos.ArticleHomeDto;
import com.heima.model.common.dtos.ResponseResult;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/api/v1/article")
@Api("app端文章列表")
public class ArticleHomeController {
@Autowired
private ApArticleService apArticleService;
/**
* 加载首页
* @param dto
* @return
*/
@ApiOperation("加载首页")
@PostMapping("/load")
public ResponseResult load(@RequestBody ArticleHomeDto dto) {
return apArticleService.load(dto, ArticleConstants.LOADTYPE_LOAD_MORE);
}
/**
* 加载更多
* @param dto
* @return
*/
@ApiOperation("加载更多")
@PostMapping("/loadmore")
public ResponseResult loadmore(@RequestBody ArticleHomeDto dto) {
return apArticleService.load(dto, ArticleConstants.LOADTYPE_LOAD_MORE);
}
/**
* 加载最新
* @param dto
* @return
*/
@ApiOperation("加载最新")
@PostMapping("/loadnew")
public ResponseResult loadnew(@RequestBody ArticleHomeDto dto) {
return apArticleService.load(dto, ArticleConstants.LOADTYPE_LOAD_NEW);
}
}
⑨在nacos的leadnews-app-gateway添加文字微服务的路径
spring:
cloud:
gateway:
globalcors:
add-to-simple-url-handler-mapping: true
corsConfigurations:
'[/**]':
allowedHeaders: "*"
allowedOrigins: "*"
allowedMethods:
- GET
- POST
- DELETE
- PUT
- OPTION
routes:
# 用户微服务
- id: user
uri: lb://leadnews-user
predicates:
- Path=/user/**
filters:
- StripPrefix= 1
# 文章微服务
- id: article
uri: lb://leadnews-article
predicates:
- Path=/article/**
filters:
- StripPrefix= 1
⑩swagger测试或前后端联调测试
启动ArticleApplication、AppGatewayApplication和UserApplication
在启动ArticleApplication之前可以先clean一下项目,防止各种报错
图片加载不出来是正常的!!!
二、文章详情
1. 需求分析
2. 实现思路
实现方案一
用户某一条文章,根据文章的id去查询文章内容表,返回渲染页面
实现方案2-静态模板展示
3. FreeMarker
(1)简介
FreeMarker是一款模板引擎:即一种基于模板和要改变的数据,并用来生成输出文本(HTML网页、电子邮件、配置文件、源代码等)的通用工具。它不是面向最终用户的,而是一个Java类库,是一款程序员可以嵌入他们所开发产品的组件。
模板编写为FreeMarker Template Language(FTL)。它是简单的,专用的语言,不是像PHP那样成熟的编程语言。那就意味着要准备数据在真实编程语言中显示,比如数据库查询和业务运算,之后模板显示已经准备好的数据,在模板中,你可以专注于如何展现数据,而在模板之外可以专注于要展示什么数据。
(2)技术选型对比
技术 | 说明 |
Jsp | Jsp为Servlet专用,不能单独进行使用 |
Velocity | Velocity从2010年更新完2.0版本后,7年没有更新。SpringBoot官方在1.4版本后对此也不再支持 |
thmeleaf | 新技术,功能较为强大,但是执行的效率较低 |
freemarker | 性能好,强大的模板语言、轻量 |
(3)环境搭建和快速入门
①在test模块下创建一个freemarker-demo的测试工程专门用于freemarker的功能测试与模板的测试。
②pom文件如下
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>heima-leadnews-test</artifactId>
<groupId>com.heima</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>freemarker-demo</artifactId>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-freemarker</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
</dependency>
<!-- lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<!-- apache 对 java io 的封装工具库 -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-io</artifactId>
<version>1.3.2</version>
</dependency>
</dependencies>
</project>
③准备配置文件application.yml
server:
port: 8881 #服务端口
spring:
application:
name: freemarker-demo #指定服务名
freemarker:
cache: false #关闭模板缓存,方便测试
settings:
template_update_delay: 0 #检查模板更新延迟时间,设置为0表示立即检查,如果时间大于0会有缓存不方便进行模板测试
suffix: .ftl #指定Freemarker模板文件的后缀名
注意事项:freemarker模板文件通常都是以ftl作为扩展名,也可以为tml、xml、jsp等。
④在resources下创建templates,此目录为freemarker的默认模板存放目录。
在templates下创建模板文件01-basic.ftl,模板中的插值表达式最终会被freemarker替换成具体的数据。
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Hello World!</title>
</head>
<body>
<b>普通文本 String 展示:</b><br><br>
Hello ${name} <br>
<hr>
<b>对象Student中的数据展示:</b><br/>
姓名:${stu.name}<br/>
年龄:${stu.age}
<hr>
</body>
</html>
⑤freemarker作为SpringMVC一种视图格式,默认情况下SpringMVC支持freemarker视图格式
⑥创建启动类FreemarkerDemoApplication
package com.heima.freemarker;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class FreemarkerDemoApplication {
public static void main(String[] args) {
SpringApplication.run(FreemarkerDemoApplication.class, args);
}
}
⑦创建模型类
package com.heima.freemarker.entity;
import lombok.Data;
import java.util.Date;
@Data
public class Student {
private String name;//姓名
private int age;//年龄
private Date birthday;//生日
private Float money;//钱包
}
⑧创建Controller类,向Map中添加name,最后返回模板文件
package com.heima.freemarker.controller;
import com.heima.freemarker.entity.Student;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
@Controller
public class HelloController {
@GetMapping("/basic")
public String test(Model model) {
// 1. 纯文本形式的参数
model.addAttribute("name", "freemarker");
// 2. 实体类相关的参数
Student student = new Student();
student.setName("小明");
student.setAge(18);
model.addAttribute("stu", student);
return "01-basic";
}
}
⑨启动FreemarkerDemoApplication
http://localhost:8881/basic
(4)Freemarker指令语法
基础语法种类
1. 注释,即<#-- -->,介于其之间的内容会被freemarker忽略
<#-- 我是一个freemarker注释 -->
2. 插值(Interpolation):即${...}部分,freemarker会用真实的值代替${...}
Hello ${name}
3. FTL指令:和HTML标记类似,名字前加#予以区分,Freemarker会解析标签中的表达式或逻辑。
<#>FTL指令</#>
4. 文本,仅文本信息,这些不是freemarker的注释、插值、FTL指令的内容会被freemarker忽略解析,直接输出内容。
<#--freemarker中的普通文本-->
我是一个普通的文本
集合指令(List和Map)
List指令格式
<#list></#list>
Map
获取map中的值
map['keyname'].property
map.keyname.property
遍历map
<#list userMap?keys as key>
key:${key}--value:${userMap["$key}"]}
</#list>
①在HelloController中新增如下方法:
package com.heima.freemarker.controller;
import com.heima.freemarker.entity.Student;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
@Controller
public class HelloController {
// ... ...
@GetMapping( "/list")
public String list(Model model) {
Student stu1 = new Student();
stu1.setName("小强");
stu1.setAge(18);
stu1.setMoney(1000.86f);
stu1.setBirthday(new Date());
Student stu2 = new Student();
stu2.setName("小红");
stu2.setAge(19);
stu2.setMoney(200.1f);
// 将两个对象模型数据存放到List集合中
List<Student> stus = new ArrayList<>();
stus.add(stu1);
stus.add(stu2);
// 向model中存放List集合数据
model.addAttribute("stus", stus);
// 创建Map数据
HashMap<String, Student> stuMap = new HashMap<>();
stuMap.put("stu1", stu1);
stuMap.put("stu2", stu2);
// 向model中存放Map数据
model.addAttribute("stuMap", stuMap);
return "02-list";
}
}
②在templates目录下新增02-list.ftl文件
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Hello World!</title>
</head>
<body>
<#-- list 数据的展示 -->
<b>展示list中的stu数据:</b>
<br>
<br>
<table>
<tr>
<td>序号</td>
<td>姓名</td>
<td>年龄</td>
<td>钱包</td>
</tr>
<#list stus as stu>
<tr>
<td>${stu_index+1}</td>
<td>${stu.name}</td>
<td>${stu.age}</td>
<td>${stu.money}</td>
</tr>
</#list>
</table>
<hr>
<#-- Map 数据的展示 -->
<b>map数据的展示:</b>
<br/><br/>
<a href="###">方式一:通过map['keyname'].property</a><br/>
输出stu1的学生信息:<br/>
姓名:${stuMap['stu1'].name}<br/>
年龄:${stuMap['stu1'].age}<br/>
<br/>
<a href="###">方式二:通过map.keyname.property</a><br/>
输出stu2的学生信息:<br/>
姓名:${stuMap.stu2.name}<br/>
年龄:${stuMap.stu2.age}<br/>
<br/>
<a href="###">遍历map中两个学生信息:</a><br/>
<table>
<tr>
<td>序号</td>
<td>姓名</td>
<td>年龄</td>
<td>钱包</td>
</tr>
<#list stuMap?keys as key>
<tr>
<#-- 得到循环的下标,使用方法是在stu后边加"_index",它的值是从0开始 -->
<td>${key_index}</td>
<td>${stuMap[key].name}</td>
<td>${stuMap[key].age}</td>
<td>${stuMap[key].money}</td>
</tr>
</#list>
</table>
<hr>
</body>
</html>
if指令
<#if expression>
<#else>
</#if>
需求:在list集合中判断学生为小红的数据字体显示为红色。
运算符
数学运算
FreeMarker表达式中完全支持算术运算,FreeMarker支持的算术运算符包括:
- 加法:+
- 减法:-
- 乘法:*
- 除法:/
- 求模(求余):%
比较运算符
逻辑运算符
- 逻辑与:&&
- 逻辑或:||
- 逻辑非:!
空值处理
1. 判断某变量是否存在使用"??"
用法为:variable??,如果该变量存在,返回true,否则返回false
例:为防止stus为空报错可以加上判断如下:
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标签,assign的作用是定义一个变量。
<#assign text="{'bank':'工商银行','account':'10101920201920212'}" />
<#assign data=text?eval />
开户行:${data.bank} 账号:${data.account}
(5)输出静态化文件
使用freemarker原生Api将页面生成html文件
package com.heima.freemarker.test;
import com.heima.freemarker.FreemarkerDemoApplication;
import com.heima.freemarker.entity.Student;
import freemarker.template.Configuration;
import freemarker.template.Template;
import freemarker.template.TemplateException;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
import java.io.FileWriter;
import java.io.IOException;
import java.util.*;
@SpringBootTest(classes = FreemarkerDemoApplication.class)
@RunWith(SpringRunner.class)
public class FreemarkerTest {
@Autowired
private Configuration configuration;
@Test
public void test() throws IOException, TemplateException {
// freemarker的模板对象,获取模板
Template template = configuration.getTemplate("02-list.ftl");
Map params = getData();
/**
* 合成方法
* 两个参数
* 第一个参数:模板数据
* 第二个参数:输出流
*/
template.process(params, new FileWriter("d:/list.html"));
}
private Map getData() {
Map<String, Object> map = new HashMap<>();
Student stu1 = new Student();
stu1.setName("小强");
stu1.setAge(18);
stu1.setMoney(1000.86f);
stu1.setBirthday(new Date());
Student stu2 = new Student();
stu2.setName("小红");
stu2.setAge(19);
stu2.setMoney(200.1f);
// 将两个对象模型数据存放到List集合中
List<Student> stus = new ArrayList<>();
stus.add(stu1);
stus.add(stu2);
// 向map中存放List集合数据
map.put("stus", stus);
// 创建Map数据
HashMap<String, Student> stuMap = new HashMap<>();
stuMap.put("stu1", stu1);
stuMap.put("stu2", stu2);
// 向map中存放Map数据
map.put("stuMap", stuMap);
map.put("today", new Date());
map.put("point", 1029230122);
// 返回Map
return map;
}
}
4. MinIO
(1)对象存储方式的对比
存储方式 | 有点 | 缺点 |
服务器磁盘 | 开发便捷,成本低 | 扩展困难 |
分布式文件系统 | 容易实现扩容 | 复杂度高 |
第三方存储 | 开发简单,功能强大,免维护 | 收费 |
存储方式 | 优点 | 缺点 |
FastDFS | 1. 主备服务,高可用 2. 支持主从文件,支持自定义扩展名 3. 支持动态扩容 | 1. 没有完备官方文档,近几年没有更新 2. 环境搭建较为麻烦 |
MinIO | 1. 性能高,准硬件条件下它能达到55GB/s的读、35GB/s的写速率 2. 部署自带管理界面 3. MinIO.Inc运营的开源项目,社区活跃度高 4. 提供了所有主流开发语言的SDK | 1. 不支持动态增加节点 |
(2)MinIO概述
MinIO基于Apache License v2.0开源协议的对象存储服务,可以作为云存储的解决方案用来保存海量的图片、视频、文档。
- Golang语言实现,配置简单,单行命令可以运行起来。
- MinIO兼容亚马逊S3云存储服务接口,适合于存储大容量非结构化的数据,一个对象文件可以是任意大小,从几kb到最大5T不等。
- 官方文档:MinIO对象存储 Kubernetes — MinIO中文文档 | MinIO Kubernetes中文文档
(3)MinIO特点
特点 | 说明 |
数据保护 | MinIO使用MinIO Erasure Code(纠删码)来防止硬件故障。即便损坏一半以上的dirver,但是仍然可以从中恢复。 |
高性能 | 作为高性能对象存储,在标准硬件条件下它能达到55GB/s的读、35GB/s的写速率 |
可扩容 | 不同MinIO集群可以组成联邦,并形成一个全局的命名空间,并跨越多个数据中心 |
SDK支持 | 基于MinIO轻量的特点,它得到类似Java、Python、Go等语言的sdk支持 |
有操作页面 | 面向用户友好的简单操作界面,非常方便的管理Bucket及里面的文件资源 |
功能简单 | 这一设计原则让MinIO不容易出错、更快启动 |
丰富的API | 支持文件资源的分享连接以及分享链接的过期策略、存储桶操作、文件列表访问及文件上传下载的基本功能 |
文件变化主动通知 | 存储桶(Bucket)如果发生改变,比如上传对象和删除对象,可以使用存储桶事件通知机制进行监控,并通过以下方式发布出去:AMQP、MQTT、Elasticsearch、Redis、NATS、MySQL、Kafka、Webhooks等 |
(4)Docker安装MinIO
①拉取镜像
docker pull minio/minio
②创建容器
docker run -p 9000:9000 --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
③访问minio系统
http://192.168.200.130:9000/
Access Key:minio、Secret Key:minio23
基本概念:
- bucket:类比于文件系统的目录
- Object:类比于文件系统的文件
- Keys:类比于文件名
(5)快速入门-上传文件进行静态访问
目标:把list.html文件上传到minio中,并且可以在浏览器中访问
①新建模块minio-demo
②pom.xml内容如下
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>heima-leadnews-test</artifactId>
<groupId>com.heima</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>minio-demo</artifactId>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>io.minio</groupId>
<artifactId>minio</artifactId>
<version>7.1.0</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
</dependency>
</dependencies>
</project>
③创建引导类MinIOApplication
package com.heima.minio;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class MinIOApplication {
public static void main(String[] args) {
SpringApplication.run(MinIOApplication.class, args);
}
}
④创建测试类,用于上传html文件
package com.heima.minio.test;
import io.minio.MinioClient;
import io.minio.PutObjectArgs;
import io.minio.errors.*;
import java.io.FileInputStream;
import java.io.IOException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
public class MinIOTest {
/**
* 把list.html文件上传到minio中,并且可以在浏览器中访问
* @param args
*/
public static void main(String[] args) {
FileInputStream fileInputStream = null;
try {
fileInputStream = new FileInputStream("D:\\list.html");
// 1. 获取minio的链接信息,创建一个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);
// 3. 访问路径
System.out.println("http://192.168.200.130:9000/leadnews/list.html");
} catch (Exception e) {
e.printStackTrace();
}
}
}
把bucket的权限修改为Read and Write
http://192.168.200.130:9000/leadnews/list.html
(6)封装MinIO为starter
步骤①:把课前资料里提供的heima-leadnews-basic微服务放到父工程下
在pom.xml(heima-leadnews)添加模块,然后maven刷新
<modules>
<!-- -->
<module>heima-leadnews-test</module>
<module>heima-leadnews-basic</module>
</modules>
②在pom.xml(minio-demo)导入heima-file-starter依赖
<dependency>
<groupId>com.heima</groupId>
<artifactId>heima-file-starter</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
③在微服务minio-demo的application.yml中添加minio所需的配置信息
minio:
accessKey: minio
secretKey: minio123
bucket: leadnews
endpoint: http://192.168.200.130:9000
readPath: http://192.168.200.130:9000
④测试
package com.heima.minio.test;
import com.heima.file.service.FileStorageService;
import com.heima.minio.MinIOApplication;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
@SpringBootTest(classes = MinIOApplication.class)
@RunWith(SpringRunner.class)
public class MinIOTest {
@Autowired
private FileStorageService fileStorageService;
// 把list.html文件上传到minio中,并且可以在浏览器中访问
@Test
public void test() throws FileNotFoundException {
FileInputStream fileInputStream = new FileInputStream("D:\\list.html");
String path = fileStorageService.uploadHtmlFile("", "list.html", fileInputStream);
System.out.println(path);
}
}
(7)文章详情实现
实现步骤:
①在article微服务中添加MinIO和freemarker的支持
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-freemarker</artifactId>
</dependency>
<dependency>
<groupId>com.heima</groupId>
<artifactId>heima-file-starter</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
</dependencies>
在nacos的leadnews-article文件中添加配置
spring:
# 。。。 。。。
minio:
accessKey: minio
secretKey: minio123
bucket: leadnews
endpoint: http://192.168.200.130:9000
readPath: http://192.168.200.130:9000
②资料中找到模板文件(article.ftl)拷贝到article微服务下
③资料中找到index.js、axios.min.js和index.css三个文件手动上传到MinIO中
package com.heima.minio.test;
import io.minio.MinioClient;
import io.minio.PutObjectArgs;
import java.io.FileInputStream;
public class MinIOTest {
/**
* 把list.html文件上传到minio中,并且可以在浏览器中访问
* @param args
*/
public static void main(String[] args) {
FileInputStream fileInputStream = null;
try {
fileInputStream = new FileInputStream("D:\\IDEA\\frontProject\\tmp\\js\\index.js");
// 1. 获取minio的链接信息,创建一个minio的客户端
MinioClient minioClient = MinioClient.builder()
.credentials("minio", "minio123")
.endpoint("http://192.168.200.130:9000")
.build();
// 2. 上传
PutObjectArgs putObjectArgs = PutObjectArgs.builder()
.object("plugins/js/index.js") // 文件名称
.contentType("text/js") // 文件类型
.bucket("leadnews") // 桶名称,与minio管理界面创建的桶一致即可
.stream(fileInputStream, fileInputStream.available(), -1)
.build();
minioClient.putObject(putObjectArgs);
// 3. 访问路径
// System.out.println("http://192.168.200.130:9000/leadnews/list.html");
} catch (Exception e) {
e.printStackTrace();
}
}
}
④在article微服务中新增测试类(后期新增文章的时候创建详情静态页,目前暂时手动生成)
新建ApArticleContentMapper
package com.heima.article.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.heima.model.article.pojos.ApArticleContent;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface ApArticleContentMapper extends BaseMapper<ApArticleContent> {
}
单元测试类ArticleFreemarkerTest
package com.heima.article.test;
import com.alibaba.fastjson.JSONArray;
import com.baomidou.mybatisplus.core.toolkit.StringUtils;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.heima.article.ArticleApplication;
import com.heima.article.mapper.ApArticleContentMapper;
import com.heima.article.service.ApArticleService;
import com.heima.file.service.FileStorageService;
import com.heima.model.article.pojos.ApArticle;
import com.heima.model.article.pojos.ApArticleContent;
import freemarker.template.Configuration;
import freemarker.template.Template;
import org.checkerframework.checker.units.qual.A;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.io.StringWriter;
import java.util.HashMap;
import java.util.Map;
@SpringBootTest(classes = ArticleApplication.class)
@RunWith(SpringRunner.class)
public class ArticleFreemarkerTest {
@Autowired
private ApArticleContentMapper apArticleContentMapper;
@Autowired
private Configuration configuration;
@Autowired
private FileStorageService fileStorageService;
@Autowired
private ApArticleService apArticleService;
@Test
public void createStaticUrlTest() throws Exception {
// 已知文章的id
// 1. 获取文章内容
ApArticleContent apArticleContent = apArticleContentMapper.selectOne(Wrappers
.<ApArticleContent>lambdaQuery()
.eq(ApArticleContent::getArticleId, "1302864436297482242L"));
if(apArticleContent != null && StringUtils.isNotBlank(apArticleContent.getContent())) {
// 2. 文章内容通过freemarker生成html文件
Template template = configuration.getTemplate("article.ftl");
// 数据模型
Map<String, Object> content = new HashMap<>();
content.put("content", JSONArray.parseArray(apArticleContent.getContent()));
// 输出流
StringWriter out = new StringWriter();
template.process(content, out);
// 3. 把html文件上传到minio中
InputStream in = new ByteArrayInputStream(out.toString().getBytes());
String path = fileStorageService.uploadHtmlFile("", apArticleContent.getArticleId() + ".html", in);
// 4. 修改ap_article表,保存static_url字段
apArticleService.update(Wrappers
.<ApArticle>lambdaUpdate()
.eq(ApArticle::getId, apArticleContent.getArticleId())
.set(ApArticle::getStaticUrl, path));
}
}
}
访问测试:http://192.168.200.130:9000/leadnews/2024/07/17/1302864436297482242.html