魔音短视频是在慕课网上项目实战中的一个项目,在这个项目中我学习到了很多,也知道了很多东西,在此记录一下用到的技术和某些功能的实现流程
几个工具类
- 自定义统一响应格式:用于结果返回,很有用
package com.wrial.utils;
/**
* @Description: 自定义响应数据结构
* 这个类是提供给门户,ios,安卓,微信商城用的
* 门户接受此类数据后需要使用本类的方法转换成对于的数据类型格式(类,或者list)
* 其他自行处理
* 200:表示成功
* 500:表示错误,错误信息在msg字段中
* 501:bean验证错误,不管多少个错误都以map形式返回
* 502:拦截器拦截到用户token出错
* 555:异常抛出信息
*/
public class MyJSONResult {
// 响应业务状态
private Integer status;
// 响应消息
private String msg;
// 响应中的数据
private Object data;
public static MyJSONResult build(Integer status, String msg, Object data) {
return new MyJSONResult(status, msg, data);
}
public static MyJSONResult ok(Object data) {
return new MyJSONResult(data);
}
public static MyJSONResult ok() {
return new MyJSONResult(null);
}
public static MyJSONResult errorMsg(String msg) {
return new MyJSONResult(500, msg, null);
}
public static MyJSONResult errorMap(Object data) {
return new MyJSONResult(501, "error", data);
}
public static MyJSONResult errorTokenMsg(String msg) {
return new MyJSONResult(502, msg, null);
}
public static MyJSONResult errorException(String msg) {
return new MyJSONResult(555, msg, null);
}
public MyJSONResult() {
}
public MyJSONResult(Integer status, String msg, Object data) {
this.status = status;
this.msg = msg;
this.data = data;
}
public MyJSONResult(Object data) {
this.status = 200;
this.msg = "OK";
this.data = data;
}
public Boolean isOK() {
return this.status == 200;
}
public Integer getStatus() {
return status;
}
public void setStatus(Integer status) {
this.status = status;
}
public String getMsg() {
return msg;
}
public void setMsg(String msg) {
this.msg = msg;
}
public Object getData() {
return data;
}
public void setData(Object data) {
this.data = data;
}
}
- JsonUtils:负责JSON转对象,List,对象转JSON等,在目前的JSON库中也有提供,可要可不要
package com.wrial.utils;
import java.util.List;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.ObjectMapper;
public class JsonUtils {
// 定义jackson对象
private static final ObjectMapper MAPPER = new ObjectMapper();
/**
* 将对象转换成json字符串。
* <p>Title: pojoToJson</p>
* <p>Description: </p>
* @param data
* @return
*/
public static String objectToJson(Object data) {
try {
String string = MAPPER.writeValueAsString(data);
return string;
} catch (JsonProcessingException e) {
e.printStackTrace();
}
return null;
}
/**
* 将json结果集转化为对象
*
* @param jsonData json数据
* @param clazz 对象中的object类型
* @return
*/
public static <T> T jsonToPojo(String jsonData, Class<T> beanType) {
try {
T t = MAPPER.readValue(jsonData, beanType);
return t;
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
/**
* 将json数据转换成pojo对象list
* <p>Title: jsonToList</p>
* <p>Description: </p>
* @param jsonData
* @param beanType
* @return
*/
public static <T>List<T> jsonToList(String jsonData, Class<T> beanType) {
JavaType javaType = MAPPER.getTypeFactory().constructParametricType(List.class, beanType);
try {
List<T> list = MAPPER.readValue(jsonData, javaType);
return list;
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
}
- 对于物理分页的封装,分页四要素,当前页,总页数,总记录树,每页显示的内容
package com.wrial.utils;
import java.util.List;
/**
* @Description: 封装分页后的数据格式,也就是分页后需要的东西
*/
public class PagedResult {
private int page; // 当前页数
private int total; // 总页数
private long records; // 总记录数
private List<?> rows; // 每行显示的内容
public int getPage() {
return page;
}
public void setPage(int page) {
this.page = page;
}
public int getTotal() {
return total;
}
public void setTotal(int total) {
this.total = total;
}
public long getRecords() {
return records;
}
public void setRecords(long records) {
this.records = records;
}
public List<?> getRows() {
return rows;
}
public void setRows(List<?> rows) {
this.rows = rows;
}
}
- Redis操作工具类,基于RedisTemplate
package com.wrial.utils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;
/**
* @Description: 使用redisTemplate的操作实现类
*/
@Component
public class RedisOperator {
// @Autowired
// private RedisTemplate<String, Object> redisTemplate;
@Autowired
private StringRedisTemplate redisTemplate;
// Key(键),简单的key-value操作
/**
* 实现命令:TTL key,以秒为单位,返回给定 key的剩余生存时间(TTL, time to live)。
*
* @param key
* @return
*/
public long ttl(String key) {
return redisTemplate.getExpire(key);
}
/**
* 实现命令:expire 设置过期时间,单位秒
*
* @param key
* @return
*/
public void expire(String key, long timeout) {
redisTemplate.expire(key, timeout, TimeUnit.SECONDS);
}
/**
* 实现命令:INCR key,增加key一次
*
* @param key
* @return
*/
public long incr(String key, long delta) {
return redisTemplate.opsForValue().increment(key, delta);
}
/**
* 实现命令:KEYS pattern,查找所有符合给定模式 pattern的 key
*/
public Set<String> keys(String pattern) {
return redisTemplate.keys(pattern);
}
/**
* 实现命令:DEL key,删除一个key
*
* @param key
*/
public void del(String key) {
redisTemplate.delete(key);
}
// String(字符串)
/**
* 实现命令:SET key value,设置一个key-value(将字符串值 value关联到 key)
*
* @param key
* @param value
*/
public void set(String key, String value) {
redisTemplate.opsForValue().set(key, value);
}
/**
* 实现命令:SET key value EX seconds,设置key-value和超时时间(秒)
*
* @param key
* @param value
* @param timeout
* (以秒为单位)
*/
public void set(String key, String value, long timeout) {
redisTemplate.opsForValue().set(key, value, timeout, TimeUnit.SECONDS);
}
/**
* 实现命令:GET key,返回 key所关联的字符串值。
*
* @param key
* @return value
*/
public String get(String key) {
return (String)redisTemplate.opsForValue().get(key);
}
// Hash(哈希表)
/**
* 实现命令:HSET key field value,将哈希表 key中的域 field的值设为 value
*
* @param key
* @param field
* @param value
*/
public void hset(String key, String field, Object value) {
redisTemplate.opsForHash().put(key, field, value);
}
/**
* 实现命令:HGET key field,返回哈希表 key中给定域 field的值
*
* @param key
* @param field
* @return
*/
public String hget(String key, String field) {
return (String) redisTemplate.opsForHash().get(key, field);
}
/**
* 实现命令:HDEL key field [field ...],删除哈希表 key 中的一个或多个指定域,不存在的域将被忽略。
*
* @param key
* @param fields
*/
public void hdel(String key, Object... fields) {
redisTemplate.opsForHash().delete(key, fields);
}
/**
* 实现命令:HGETALL key,返回哈希表 key中,所有的域和值。
*
* @param key
* @return
*/
public Map<Object, Object> hgetall(String key) {
return redisTemplate.opsForHash().entries(key);
}
// List(列表)
/**
* 实现命令:LPUSH key value,将一个值 value插入到列表 key的表头
*
* @param key
* @param value
* @return 执行 LPUSH命令后,列表的长度。
*/
public long lpush(String key, String value) {
return redisTemplate.opsForList().leftPush(key, value);
}
/**
* 实现命令:LPOP key,移除并返回列表 key的头元素。
*
* @param key
* @return 列表key的头元素。
*/
public String lpop(String key) {
return (String)redisTemplate.opsForList().leftPop(key);
}
/**
* 实现命令:RPUSH key value,将一个值 value插入到列表 key的表尾(最右边)。
*
* @param key
* @param value
* @return 执行 LPUSH命令后,列表的长度。
*/
public long rpush(String key, String value) {
return redisTemplate.opsForList().rightPush(key, value);
}
}
- 关于时长统计的工具类
package com.wrial.utils;
import java.text.SimpleDateFormat;
import java.util.Date;
public class TimeAgoUtils {
private static final long ONE_MINUTE = 60000L;
private static final long ONE_HOUR = 3600000L;
private static final long ONE_DAY = 86400000L;
private static final long ONE_WEEK = 604800000L;
private static final String ONE_SECOND_AGO = "秒前";
private static final String ONE_MINUTE_AGO = "分钟前";
private static final String ONE_HOUR_AGO = "小时前";
private static final String ONE_DAY_AGO = "天前";
private static final String ONE_MONTH_AGO = "月前";
private static final String ONE_YEAR_AGO = "年前";
public static String format(Date date) {
long delta = new Date().getTime() - date.getTime();
if (delta < 1L * ONE_MINUTE) {
long seconds = toSeconds(delta);
return (seconds <= 0 ? 1 : seconds) + ONE_SECOND_AGO;
}
if (delta < 45L * ONE_MINUTE) {
long minutes = toMinutes(delta);
return (minutes <= 0 ? 1 : minutes) + ONE_MINUTE_AGO;
}
if (delta < 24L * ONE_HOUR) {
long hours = toHours(delta);
return (hours <= 0 ? 1 : hours) + ONE_HOUR_AGO;
}
if (delta < 48L * ONE_HOUR) {
return "昨天";
}
if (delta < 30L * ONE_DAY) {
long days = toDays(delta);
return (days <= 0 ? 1 : days) + ONE_DAY_AGO;
}
if (delta < 12L * 4L * ONE_WEEK) {
long months = toMonths(delta);
return (months <= 0 ? 1 : months) + ONE_MONTH_AGO;
} else {
long years = toYears(delta);
return (years <= 0 ? 1 : years) + ONE_YEAR_AGO;
}
}
private static long toSeconds(long date) {
return date / 1000L;
}
private static long toMinutes(long date) {
return toSeconds(date) / 60L;
}
private static long toHours(long date) {
return toMinutes(date) / 60L;
}
private static long toDays(long date) {
return toHours(date) / 24L;
}
private static long toMonths(long date) {
return toDays(date) / 30L;
}
private static long toYears(long date) {
return toMonths(date) / 365L;
}
public static void main(String[] args) throws Exception {
SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:m:s");
Date date = format.parse("2020-01-01 18:35:35");
System.out.println(format(date));
}
}
- 全局ID生成器,由于类太多,在项目路径org.n3r.idworker
重点功能实现逻辑
- Swagger2实现接口文档
pom依赖
<!-- swagger2 配置 -->
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>2.4.0</version>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
<version>2.4.0</version>
</dependency>
具体配置代码如下
@Configuration
@EnableSwagger2
public class Swagger2 {
/**
* @Description:swagger2的配置文件,这里可以配置swagger2的一些基本的内容,比如扫描的包等等
*/
@Bean
public Docket createRestApi() {
// 为swagger添加header参数可供输入
ParameterBuilder userTokenHeader = new ParameterBuilder();
ParameterBuilder userIdHeader = new ParameterBuilder();
List<Parameter> pars = new ArrayList<>();
// 根据项目条件构造参数
userTokenHeader.name("headerUserToken").description("userToken")
.modelRef(new ModelRef("string")).parameterType("header")
.required(false).build();
userIdHeader.name("headerUserId").description("userId")
.modelRef(new ModelRef("string")).parameterType("header")
.required(false).build();
pars.add(userTokenHeader.build());
pars.add(userIdHeader.build());
return new Docket(DocumentationType.SWAGGER_2).apiInfo(apiInfo()).select()
.apis(RequestHandlerSelectors.basePackage("com.wrial.controller")) //是controller下的接口
.paths(PathSelectors.any()).build()
.globalOperationParameters(pars); //加入配置的参数
}
/**
* @Description: 构建 api文档的信息
*/
private ApiInfo apiInfo() {
return new ApiInfoBuilder()
// 设置页面标题
.title("短视频后端接口文档")
// 设置联系人
.contact(new Contact("Wrial", "http://xxxxxxxx", "w2806935450@163.com"))
// 描述
.description("欢迎访问短视频接口文档,这里是描述信息")
// 定义版本号
.version("1.0").build();
}
}
使用方式如下
@ApiOperation(value = "上传视频", notes = "上传视频的接口")
@ApiImplicitParams({
@ApiImplicitParam(name = "userId", value = "用户id", required = true,
dataType = "String", paramType = "form"),
@ApiImplicitParam(name = "bgmId", value = "背景音乐id", required = false,
dataType = "String", paramType = "form"),
@ApiImplicitParam(name = "videoSeconds", value = "背景音乐播放长度", required = true,
dataType = "String", paramType = "form"),
@ApiImplicitParam(name = "videoWidth", value = "视频宽度", required = true,
dataType = "String", paramType = "form"),
@ApiImplicitParam(name = "videoHeight", value = "视频高度", required = true,
dataType = "String", paramType = "form"),
@ApiImplicitParam(name = "desc", value = "视频描述", required = false,
dataType = "String", paramType = "form")
})
@PostMapping(value = "/upload", headers = "content-type=multipart/form-data")
// 具体的业务
测试地址:http://localhost:8081/swagger-ui.html
可以看到用起来很方便!
2. 用户注册功能
流程图如下
代码如下:
@ApiOperation(value = "用户注册",notes = "用户注册接口")
@PostMapping("/regist")
public MyJSONResult registry(@RequestBody Users user) throws Exception {
// 1. 判断用户名和密码必须不为空
if (StringUtils.isBlank(user.getUsername()) || StringUtils.isBlank(user.getPassword())) {
return MyJSONResult.errorMsg("用户名和密码不能为空");
}
// 2. 判断用户名是否存在
boolean usernameIsExist = userService.queryUsernameIsExist(user.getUsername());
// 3. 保存用户,注册信息
if (!usernameIsExist) {
user.setNickname(user.getUsername());
user.setPassword(MD5Utils.getMD5Str(user.getPassword()));
user.setFansCounts(0);
user.setReceiveLikeCounts(0);
user.setFollowCounts(0);
userService.saveUser(user);
} else {
return MyJSONResult.errorMsg("用户名已经存在,请换一个再试");
}
//为了安全,设置为密码为null
user.setPassword("");
UsersVO userVO = setUserRedisSessionToken(user);
return MyJSONResult.ok(userVO);
}
/*
提取出的方法
*/
public UsersVO setUserRedisSessionToken(Users user) {
String uniqueToken = UUID.randomUUID().toString();
String key = USER_REDIS_SESSION + ":" + user.getId();
redis.set(key, uniqueToken, 1000 * 60 * 30);
UsersVO userVO = new UsersVO();
BeanUtils.copyProperties(user, userVO);
userVO.setUserToken(uniqueToken);
return userVO;
}
- 用户登录功能
在用户登录这一块主要的技术点就是每次登录都随机生成一个字符串、每次登录的时候都会进行缓存的替换
代码如下:
拦截器代码
public class MyLoginInterceptor extends HandlerInterceptorAdapter {
@Autowired
private RedisOperator redisOperator;
/*
执行登录拦截 通过在前端设置的请求头中的userId 和 userToken来进行判断
1. 如果userId 和 userToken都是null 那就是未登录
2. 如果userId 不为null,userToken 为在redis中查不到(说明被覆盖了),也就是说在另外一个地方登录了
3. 如果都正常,说明登录成功
*/
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String userId = request.getHeader("headerUserId");
String userToken = request.getHeader("headerUserToken");
if (StringUtils.isNotBlank(userId) && StringUtils.isNotBlank(userToken)) {
String uniqueToken = redisOperator.get(USER_REDIS_SESSION + ":" + userId);
if (StringUtils.isEmpty(uniqueToken) && StringUtils.isBlank(uniqueToken)) {
System.out.println("请登录...");
returnErrorResponse(response, new MyJSONResult().errorTokenMsg("请登录..."));
return false;
} else {
if (!uniqueToken.equals(userToken)) {
System.out.println("账号在别处登录...");
returnErrorResponse(response, new MyJSONResult().errorTokenMsg("账号在别处登录..."));
return false;
}
}
} else {
System.out.println("请登录...");
returnErrorResponse(response, new MyJSONResult().errorTokenMsg("请登录..."));
return false;
}
/**
* 返回 false:请求被拦截,返回
* 返回 true :请求OK,可以继续执行,放行
*/
return true;
}
public void returnErrorResponse(HttpServletResponse response, MyJSONResult result)
throws IOException{
OutputStream out = null;
try {
response.setCharacterEncoding("utf-8");
response.setContentType("text/json");
out = response.getOutputStream();
out.write(JsonUtils.objectToJson(result).getBytes("utf-8"));
out.flush();
} finally {
if (out != null) {
out.close();
}
}
}
}
用户登录用户注销代码
/*
提取出的方法
*/
public UsersVO setUserRedisSessionToken(Users user) {
String uniqueToken = UUID.randomUUID().toString();
String key = USER_REDIS_SESSION + ":" + user.getId();
redis.set(key, uniqueToken, 1000 * 60 * 30);
UsersVO userVO = new UsersVO();
BeanUtils.copyProperties(user, userVO);
userVO.setUserToken(uniqueToken);
return userVO;
}
@ApiOperation(value = "用户登录",notes = "用户登录接口")
@PostMapping("/login")
public MyJSONResult login(@RequestBody Users user) throws Exception {
String username = user.getUsername();
String password = user.getPassword();
if (StringUtils.isBlank(username) || StringUtils.isBlank(password)){
return MyJSONResult.errorMsg("用户名或者密码不能为空!");
}
boolean usernameIsExist = userService.queryUsernameIsExist(username);
if (usernameIsExist){
Users checkUser = userService.checkUser(username, MD5Utils.getMD5Str(password));
if (checkUser!=null){
checkUser.setPassword("");
UsersVO userVO = setUserRedisSessionToken(checkUser);
return MyJSONResult.ok(userVO);
}else {
return MyJSONResult.errorMsg("密码错误,请重试!");
}
}else {
return MyJSONResult.errorMsg("此用户未注册!请先注册");
}
}
@ApiOperation(value="用户注销", notes="用户注销接口")
@ApiImplicitParam(name="userId", value="用户id", required=true,
dataType="String", paramType="query")
@DeleteMapping("/logout")
public MyJSONResult logout(@RequestParam("userId") String userId) throws Exception {
redis.del(USER_REDIS_SESSION + ":" + userId);
return MyJSONResult.ok();
}
这就是用户登录功能
4. 自动截图功能
自动截图功能是根据FFmpeg来执行cmd命令生成的截图文件,怎么在程序运行过程中动态使用cmd命令呢?这就要牵扯到一个类的使用,那就是ProcessBuilder,他可以接受一个List数组逐条执行cmd命令。
具体代码如下
/**
*
* @Description: 获取视频的信息
*/
public class FetchVideoCover {
// 视频路径
private String ffmpegEXE;
public void getCover(String videoInputPath, String coverOutputPath) throws IOException, InterruptedException {
//cmd命令 ffmpeg.exe -ss 00:00:01 -i spring.mp4 -vframes 1 bb.jpg
List<String> command = new java.util.ArrayList<String>();
command.add(ffmpegEXE);
// 指定截取第1秒
command.add("-ss");
command.add("00:00:01");
command.add("-y");
command.add("-i");
//视频的路径
command.add(videoInputPath);
command.add("-vframes");
command.add("1");
//封面输出路径
command.add(coverOutputPath);
for (String c : command) {
System.out.print(c + " ");
}
ProcessBuilder builder = new ProcessBuilder(command);
Process process = builder.start();
InputStream errorStream = process.getErrorStream();
InputStreamReader inputStreamReader = new InputStreamReader(errorStream);
BufferedReader br = new BufferedReader(inputStreamReader);
String line = "";
while ( (line = br.readLine()) != null ) {
}
if (br != null) {
br.close();
}
if (inputStreamReader != null) {
inputStreamReader.close();
}
if (errorStream != null) {
errorStream.close();
}
}
public String getFfmpegEXE() {
return ffmpegEXE;
}
public void setFfmpegEXE(String ffmpegEXE) {
this.ffmpegEXE = ffmpegEXE;
}
public FetchVideoCover() {
super();
}
public FetchVideoCover(String ffmpegEXE) {
this.ffmpegEXE = ffmpegEXE;
}
public static void main(String[] args) {
// 获取视频信息。
FetchVideoCover videoInfo = new FetchVideoCover("D:\\我的软件\\ffmpeg\\ffmpeg\\bin\\ffmpeg.exe");
try {
videoInfo.getCover("C:\\Users\\weiao\\Music\\MV\\1.mp4","D:\\dev\\x.jpg");
} catch (Exception e) {
e.printStackTrace();
}
}
}
演示结果如下:
可以看到运行框显示的命令
这就是自动截图功能
5. 配音功能
配音功能还是根据FFmpeg和ProcessBuilder来完成的,但是配音功能有个坑,就是不能在没有进行消除音效的时候进行配音,必须要先消除原有的音效,然后在根据自己的BGM进行配音
代码如下:
/*
使用ffmpeg进行格式转化和视频音频合并(不能直接合并,踩坑,直接合并的前提是无声)
1.先消去原来的音
2.在合并新的音频
*/
public class MergeVideoMp3 {
private String ffmpegEXE;
public MergeVideoMp3(String ffmpegEXE) {
super();
this.ffmpegEXE = ffmpegEXE;
}
public void convertor(String videoInputPath, String mp3InputPath,
double seconds, String videoOutputPath) throws Exception {
// ffmpeg.exe -i xxx.mp4 -i bgm.mp3 -t 7 -y 新的视频.mp4
List<String> command = new ArrayList<>();
command.add(ffmpegEXE);
command.add("-i");
command.add(videoInputPath);
command.add("-i");
command.add(mp3InputPath);
command.add("-t");
command.add(String.valueOf(seconds));
command.add("-y");
command.add(videoOutputPath);
// for (String c : command) {
// System.out.print(c + " ");
// }
ProcessBuilder builder = new ProcessBuilder(command);
Process process = builder.start();
InputStream errorStream = process.getErrorStream();
InputStreamReader inputStreamReader = new InputStreamReader(errorStream);
BufferedReader br = new BufferedReader(inputStreamReader);
String line = "";
while ((line = br.readLine()) != null) {
}
if (br != null) {
br.close();
}
if (inputStreamReader != null) {
inputStreamReader.close();
}
if (errorStream != null) {
errorStream.close();
}
}
public static void main(String[] args) {
MergeVideoMp3 ffmpeg = new MergeVideoMp3("D:\\我的软件\\ffmpeg\\ffmpeg\\bin\\ffmpeg.exe");
try {
ffmpeg.convertor("C:\\Users\\weiao\\Music\\MV\\1.mp4", "C:\\Users\\weiao\\Music\\qys.mp3", 7.1, "C:\\Users\\weiao\\Music\\这是通过java生产的视频.mp4");
} catch (Exception e) {
e.printStackTrace();
}
}
}
这样就可以实现自定义的BGM了,在内部也会默认提供几款BGM供用户选择
6. Zookeeper实现BGM同步功能
为什么需要BGM同步呢?因为服务器和管理平台分布在两个主机上,但是后台可以对BGM的曲目进行管理,也就是上传和删除,但是服务器上也需要BGM供用户选择,因此这就牵扯到了统一资源的共享和资源一致性问题了。
解决方法:使用Zookeeper客户端进行事件的监听
在Zookeeper中有两种节点:一种是持久节点,就是你不手动释放它一直会在。另外一种是临时节点,只是临时使用
因此需要在后台创建一个持久节点,在开发环境创建临时节点,它受持久节点的制约
代码如下:
package com.wrial.ZKUtil;
import org.apache.curator.framework.CuratorFramework;
import org.apache.zookeeper.CreateMode;
import org.apache.zookeeper.ZooDefs.Ids;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class ZKCurator {
// zk客户端 用来简化Zookeeper操作的一个开源框架
private CuratorFramework client = null;
final static Logger log = LoggerFactory.getLogger(ZKCurator.class);
public ZKCurator(CuratorFramework client) {
this.client = client;
}
public void init() {
client = client.usingNamespace("admin");
try {
// 判断在admin命名空间下是否有bgm节点 /admin/bmg
if (client.checkExists().forPath("/bgm") == null) {
/**
* 对于zk来讲,有两种类型的节点:
* 持久节点: 当你创建一个节点的时候,这个节点就永远存在,除非你手动删除
* 临时节点: 你创建一个节点之后,会话断开,会自动删除,当然也可以手动删除
*/
client.create().creatingParentsIfNeeded()
.withMode(CreateMode.PERSISTENT) // 节点类型:持久节点
.withACL(Ids.OPEN_ACL_UNSAFE) // acl:匿名权限
.forPath("/bgm");
log.info("zookeeper初始化成功...");
log.info("zookeeper服务器状态:{}", client.isStarted());
}
} catch (Exception e) {
log.error("zookeeper客户端连接、初始化错误...");
e.printStackTrace();
}
}
/**
* @Description: 增加或者删除bgm,向zk-server创建子节点,供小程序后端监听
*/
public void sendBgmOperator(String bgmId, String operObj) {
try {
client.create().creatingParentsIfNeeded()
.withMode(CreateMode.PERSISTENT) // 节点类型:持久节点
.withACL(Ids.OPEN_ACL_UNSAFE) // acl:匿名权限
.forPath("/bgm/" + bgmId, operObj.getBytes());
} catch (Exception e) {
e.printStackTrace();
}
}
}
在dev环境下配置Zookeeper客户端,如果监听到是添加类型就讲BGM下载保存到事件对应的目录,如果是删除事件,就删除SpringBoot服务器中的BGM
package com.wrial;
import java.io.File;
import java.net.URL;
import java.net.URLEncoder;
import java.util.Map;
import com.wrial.config.ResourceConfig;
import com.wrial.utils.JsonUtils;
import enums.BGMOperatorTypeEnum;
import org.apache.commons.io.FileUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.curator.RetryPolicy;
import org.apache.curator.framework.CuratorFramework;
import org.apache.curator.framework.CuratorFrameworkFactory;
import org.apache.curator.framework.recipes.cache.PathChildrenCache;
import org.apache.curator.framework.recipes.cache.PathChildrenCacheEvent;
import org.apache.curator.framework.recipes.cache.PathChildrenCacheListener;
import org.apache.curator.retry.ExponentialBackoffRetry;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
/*
和在和SSM配置相同,只不过换了一种方式
*/
@Component
public class ZKCuratorClient {
// zk客户端
private CuratorFramework client = null;
final static Logger log = LoggerFactory.getLogger(ZKCuratorClient.class);
//从配置文件中加载配置项
@Autowired
private ResourceConfig resourceConfig;
/*
在Bean的init-method中调用
*/
public void init() {
if (client != null) {
return;
}
// 重试策略
RetryPolicy retryPolicy = new ExponentialBackoffRetry(1000, 5);
// 创建命名空间为admin的zk客户端
client = CuratorFrameworkFactory.builder().connectString(resourceConfig.getZookeeperServer())
.sessionTimeoutMs(10000).retryPolicy(retryPolicy).namespace("admin").build();
// 启动客户端
client.start();
try {
// String testNodeData = new String(client.getData().forPath("/bgm/18052674D26HH3X4"));
// log.info("测试的节点数据为: {}", testNodeData);
addChildWatch("/bgm");
} catch (Exception e) {
e.printStackTrace();
}
}
/*
添加监听事件
*/
public void addChildWatch(String nodePath) throws Exception {
final PathChildrenCache cache = new PathChildrenCache(client, nodePath, true);
cache.start();
cache.getListenable().addListener(new PathChildrenCacheListener() {
@Override
public void childEvent(CuratorFramework client, PathChildrenCacheEvent event)
throws Exception {
if (event.getType().equals(PathChildrenCacheEvent.Type.CHILD_ADDED)) {
log.info("监听到事件 CHILD_ADDED");
// 1. 从数据库查询bgm对象,获取路径path
String path = event.getData().getPath();
String operatorObjStr = new String(event.getData().getData());
Map<String, String> map = JsonUtils.jsonToPojo(operatorObjStr, Map.class);
//接收从管理系统的Map
String operatorType = map.get("operType");
// 1.1 bgm所在的相对路径
String songPath = map.get("path");
// 2. 定义保存到本地的bgm路径
String filePath = resourceConfig.getFileSpace() + songPath;
// 3. 定义下载的路径(播放url,因为是\\需要四个斜杠)
String arrPath[] = songPath.split("\\\\");
String finalPath = "";
// 3.1 处理url的斜杠以及编码
for (int i = 0; i < arrPath.length; i++) {
if (StringUtils.isNotBlank(arrPath[i])) {
finalPath += "/";
finalPath += URLEncoder.encode(arrPath[i], "UTF-8");
}
}
// String bgmUrl = "http://127.0.0.1:8080/mvc" + finalPath;
String bgmUrl = resourceConfig.getBgmServer() + finalPath;
if (operatorType.equals(BGMOperatorTypeEnum.ADD.type)) {
// 下载bgm到spingboot服务器
URL url = new URL(bgmUrl);
File file = new File(filePath);
//从指定URL下载到本地
FileUtils.copyURLToFile(url, file);
client.delete().forPath(path);
} else if (operatorType.equals(BGMOperatorTypeEnum.DELETE.type)) {
File file = new File(filePath);
FileUtils.forceDelete(file);
client.delete().forPath(path);
}
}
}
});
}
}