2024-10-31 00:00:00
本文介绍了一种通过自己拓展的浏览器插件,便捷地将微信公众号文章同步到阿里云开发者社区的方法。
先上效果图:
半个多月前接到 讨厌菠萝 同学的盛情邀请,入驻阿里云开发者社区,他也不时地监督我将以前的文章同步过来,奈何我懒癌晚期,一直没怎么动。
但心里其实一直记着这个事的,答应了人家,总得做点什么。
一篇一篇地手动复制粘贴实在太费劲了,也不是程序员做事的风格,于是就想着能不能找个什么工具,能帮我简化这个过程。
最后没有找到什么现成的,只好自己扩展了一个,下面就来介绍一下我的方案。
一文多发的需求其实我一直都有,以前也陆续用过 OpenWrite、ArtiPub 等工具,后来因为种种原因,只留下了 Wechatsync 这个浏览器插件。
我现在发布文章一般是先在微信公众号发布,然后再通过 Wechatsync 同步到知乎、掘金等平台。
看了下 Wechatsync 插件的原始代码仓库,目前并不支持 阿里云开发者平台。源码的最后一次更新时间停留在 2023 年 9 月,Issues 里也有人反馈了一些问题,但是作者好像没怎么回复了。
那就自己动手,丰衣足食吧,扩展一下 Wechatsync,让它支持阿里云开发者社区。
这个在 Wechatsync 的官方 API 文档里有介绍:
https://github.com/wechatsync/Wechatsync/blob/master/API.md
简而言之,如果要新增一个平台的支持,需要添加一个适配器,适配器需实现以下方法来完成工作流程:
打开阿里云开发者社区的网站 https://developer.aliyun.com/ ,登录后,打开浏览器的开发者工具,尝试进行发布文章必要的操作,查看网络请求,找到了一些接口:
有了这些,基本就够了。
作为一名前端初学者,对照着 Wechatsync 的源码里面其它的适配器,最终编写和调试完成了阿里云开发者社区的适配器。
就不把源码直接贴出来水篇幅了,有兴趣的可以去我的 GitHub 仓库查看,适配器源码直达链接:
写完适配器之后,再在 packages/web-extension/src/drivers/driver.js 文件里作少量修改,将其集成,然后就可以正常使用了。
Wechatsync 的原始作者是在 Chrome 商店上架了该插件的,只是版本不是最新。我 fork 出来做的一些修改主要自己使用,所以只是在自己的 fork 仓库里发布了最新版本,如果想要使用的话,可以用开发者模式加载:
以我将微信公众号的文章同步到阿里云开发者社区为例:
整个操作如文首的 gif 所示。
这个方案虽然不算完美,但是对我来说已经足够了,省去了很多重复的劳动,也算是一种效率提升吧。
如果你也有类似的需求,可以参考我的方案,或者自己动手扩展 Wechatsync,让它支持更多的平台。
本文所述相关源码已经提交到了我 fork 出来的 GitHub 仓库,供参考:
2024-10-11 00:00:00
本文记录了一个 GitHub Pages 博客网页上文章标题以 @
开头导致的问题,并分析了原因,提供了解决方法。
TL;NR:因为 YAML 的语法规则,GitHub Pages 的文章标题不能直接以 ,[]{}#&*!|>'"%@`
或 -?:加空格
开头。可以用引号将标题括起来,或者修改标题,将这些字符不放在开头。
接网友提问:
有一篇文章在 GitHub Pages 博客网页上不显示,初步排查可能与 title 有关——替换成其它文章的 title 可以正常显示,并附上了原始文件的头部:
---
layout: post
title: @EnableconfigurationProperties注解使用方式与作用
categories: [Java]
乍一看看不出什么问题,我在本地启动 Jekyll 预览,以本文件作为测试,也复现了该现象。
使用 title「为什么 GitHub Pages 的文章标题不能以 @ 开头?」时,正常:
使用 title「@EnableconfigurationProperties注解使用方式与作用」时,文章标题与摘要显示空白:
并可以在控制台看到如下错误:
Error: YAML Exception reading /Users/mazhuang/github/mzlogin.github.io/_posts/2024-10-11-why-github-pages-post-title-cannot-start-with.md: (<unknown>): found character that cannot start any token while scanning for the next token at line 3 column 8
报错信息里提到是 YAML Exception——Jekyll 的文章头部是 YAML Front Matter,是 Jekyll 用来定义文章元数据的部分。报错提示 line 3 column 8,即 title 的第一个字符 @
,is character that cannot start any token。
根据这个信息,其实已经可以想办法规避这个问题——将 title 里的 @
去掉,或者换个位置,经验证可以正常显示了。
继续深究一下,为什么 YAML 里的 title 不能以 @
开头呢?
然后找到了如下链接:
提炼一下要点:
-?:,[]{}#&*!|>'"%@`
;,[]{}#&*!|>'"%@`
;?:-
后面如果跟着非空格字符,可以放在字符串的开头,但 YAML 处理器的不同实现可能带来不同行为,稳妥起见最好也用引号括起来。title: "@EnableconfigurationProperties注解使用方式与作用"
;(推荐)2024-09-25 00:00:00
本文介绍如何通过自定义 IDEA 的 JUnit4 Test Class 模板,实现生成测试类时自动注入 logger 和被测 Service。
在 IntelliJ IDEA 中,通过快捷键可以快速生成 JUnit4 测试类,但是生成测试类以后,总是需要手动添加 logger 和被测 Service 的注入。虽然这是一个很小的「重复动作」,但程序员还是不能忍(其实已经忍了很多年了)。
以给如下简单的 Service 生成测试类为例:
package com.test.data.user.service;
import com.test.common.base.BaseService;
import com.test.data.user.entity.UserSource;
/**
* @author mazhuang
*/
public interface UserSourceService extends BaseService<UserSource> {
/**
* 记录用户来源
* @param userId -
* @param threadId -
*/
void recordUserSource(Long userId, Long threadId);
}
command + n 调出 Generate 菜单,然后选择 Test,配置测试类的名称、基类和包:
默认生成的测试类如下:
package com.test.data.user.service;
import static org.junit.Assert.*;
import com.test.BaseTests;
import org.junit.Test;
/**
* @author mazhuang
*/
public class UserSourceServiceTest extends BaseTests {
@Test
public void recordUserSource() {
}
}
然而在写测试用例的过程中,总是需要用到 logger 和 Service,所以期望中的测试类默认长这样:
package com.test.data.user.service;
import static org.junit.Assert.*;
import com.test.BaseTests;
import org.junit.Test;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
/**
* @author mazhuang
*/
@Slf4j
public class UserSourceServiceTest extends BaseTests {
@Autowired
private UserSourceService userSourceService;
@Test
public void recordUserSource() {
}
}
经过一番 search,发现 IDEA 的 Preference - Editor - File and Code Templates 的 Code 里有一个 JUnit4 Test Class,可以自定义生成 JUnit4 测试类的模板。
这个模板原始内容是这样的:
import static org.junit.Assert.*;
#parse("File Header.java")
public class ${NAME} {
${BODY}
}
基于我们的需求,将其修改为以下内容即可:
#set( $LastDotIndex = $CLASS_NAME.lastIndexOf(".") + 1 )
#set( $CamelCaseName = "$CLASS_NAME.substring($LastDotIndex)" )
#set( $CamelCaseName = "$CamelCaseName.substring(0, 1).toLowerCase()$CamelCaseName.substring(1)")
import static org.junit.Assert.*;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
#parse("File Header.java")
@Slf4j
public class ${NAME} {
@Autowired
private ${CLASS_NAME} ${CamelCaseName};
${BODY}
}
其中,${CLASS_NAME}
是被测试类的全限定名,${CamelCaseName}
是根据 ${CLASS_NAME}
生成的被测试类的驼峰命名。
至此,经过一点微小的努力,我们实现了一个小小的自动化,工作效率又提高了一点点,程序员又开心了一点点。
察觉到重复动作,并消除——也许可以称之为「偷懒」,这是程序员的日常小乐趣,也是 人类进步的动力 吧。
文中完整脚本已上传至 GitHub,仓库地址:https://github.com/mzlogin/code-generator ,以后如果有更新,或者新的代码生成脚本,也会放在这个仓库里。
2024-09-24 00:00:00
基于 MyBatis 开发的项目,新增数据库表以后,总是需要编写对应的 Entity、Mapper 和 Service 等等 Class 的代码,这些都是重复的工作,我们可以想一些办法来自动生成这些代码。
一种可选的方案是使用 MyBatis Generator,官方支持,常见需求一般也都能满足。但是它的配置文件比较繁琐,如果有一些项目相关的个性化需求,不一定很好处理。
这里介绍另外一种我觉得更为简便灵活的方法。
近几年版本的 IDEA 里已经自带了 Database Tools and SQL 插件,可以连接数据库进行常用的操作,并且,它还自带了数据库表对应 POJO 类的代码生成器:在 Database 面板里配置好数据源以后,右键表名,依次选择 Scripted Extensions、Generate POJOs.groovy,选择生成路径后,即可生成对应的 Entity 类。
既然能够生成 Entity,那么我们可以基于它进行修改,让它一次性生成我们需要的 Entity、Mapper 和 Service。
基于项目情况,我们对生成的代码有如下要求:
以 t_promotion_channel 表为例,指定该表和对应的代码目录之后,生成的目录结构如下:
.
├── entity
│ └── PromotionChannel.java
├── mapper
│ └── PromotionChannelMapper.java
└── service
├── PromotionChannelService.java
└── impl
└── PromotionChannelServiceImpl.java
需要生成的代码如下:
entity/PromotionChannel.java
package com.test.data.promotion.entity;
import com.test.common.base.BaseEntity;
import lombok.Data;
import lombok.EqualsAndHashCode;
import javax.persistence.Table;
/**
* @author mazhuang
*/
@EqualsAndHashCode(callSuper = true)
@Data
@Table(name = "t_promotion_channel")
public class PromotionChannel extends BaseEntity {
private static final long serialVersionUID = 5495175453870776988L;
/**
* 用户ID
*/
private Long fkUserId;
/**
* 渠道名称
*/
private String channelName;
}
mapper/PromotionChannelMapper.java
package com.test.data.promotion.mapper;
import com.test.common.base.BaseMapper;
import com.test.data.promotion.entity.PromotionChannel;
/**
* @author mazhuang
*/
public interface PromotionChannelMapper extends BaseMapper<PromotionChannel> {
}
service/PromotionChannelService.java
package com.test.data.promotion.service;
import com.test.common.base.BaseService;
import com.test.data.promotion.entity.PromotionChannel;
/**
* @author mazhuang
*/
public interface PromotionChannelService extends BaseService<PromotionChannel> {
}
service/impl/PromotionChannelServiceImpl.java
package com.test.data.promotion.service.impl;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import com.test.common.base.BaseServiceImpl;
import com.test.data.promotion.entity.PromotionChannel;
import com.test.data.promotion.mapper.PromotionChannelMapper;
import com.test.data.promotion.service.PromotionChannelService;
/**
* @author mazhuang
*/
@Slf4j
@Service
public class PromotionChannelServiceImpl extends BaseServiceImpl<PromotionChannelMapper, PromotionChannel> implements PromotionChannelService {
}
右键一个数据库表,依次选择 Scripted Extensions、Go to Scripts Directory,进入生成的脚本目录,找到 Generate POJOs.groovy,复制一份,重命名为 Generate MyBatis Code.groovy,然后修改内容如下:
import com.intellij.database.model.DasTable
import com.intellij.database.util.Case
import com.intellij.database.util.DasUtil
/*
* Available context bindings:
* SELECTION Iterable<DasObject>
* PROJECT project
* FILES files helper
*/
typeMapping = [
(~/(?i)int/) : "Long",
(~/(?i)float|double|decimal|real/): "BigDecimal",
(~/(?i)datetime|timestamp/) : "java.util.Date",
(~/(?i)date/) : "java.sql.Date",
(~/(?i)time/) : "java.sql.Time",
(~/(?i)/) : "String"
]
FILES.chooseDirectoryAndSave("Choose directory", "Choose where to store generated files") { dir ->
SELECTION.filter { it instanceof DasTable }.each { generate(it, dir) }
}
def generate(table, dir) {
def className = javaName(table.getName().replaceFirst('t_', ''), true)
def fields = calcFields(table)
dirPath = dir.getAbsolutePath()
packageName = calcPackageName(dirPath)
// Generate POJO
new File(dirPath + File.separator + "entity", className + ".java").withPrintWriter("utf-8") { out -> generateEntity(out, table.getName(), className, fields, packageName) }
// Generate Mapper
new File(dirPath + File.separator + "mapper", className + "Mapper.java").withPrintWriter("utf-8") { out -> generateMapper(out, className, packageName) }
// Generate Service
new File(dirPath + File.separator + "service", className + "Service.java").withPrintWriter("utf-8") { out -> generateService(out, className, packageName) }
// Generate ServiceImpl
new File(dirPath + File.separator + "service" + File.separator + "impl", className + "ServiceImpl.java").withPrintWriter("utf-8") { out -> generateServiceImpl(out, className, packageName) }
}
static def generateEntity(out, tableName, className, fields, packageName) {
out.println "package $packageName" + ".entity;"
out.println ""
out.println "import com.test.common.base.BaseEntity;"
out.println "import lombok.Data;"
out.println "import lombok.EqualsAndHashCode;"
out.println "import javax.persistence.Table;"
out.println ""
out.println "/**\n * @author mazhuang\n */"
out.println "@EqualsAndHashCode(callSuper = true)"
out.println "@Data"
out.println "@Table(name = \"$tableName\")"
out.println "public class $className extends BaseEntity {"
out.println ""
def baseEntityFields = ['pkid', 'addedBy', 'addedTime', 'lastModifiedBy', 'lastModifiedTime', 'valid']
fields.each() {
if (baseEntityFields.contains(it.name)) {
return
}
if (it.annos != "") out.println " ${it.annos}"
if (it.comment != null) out.println " /**\n * ${it.comment}\n */"
out.println " private ${it.type} ${it.name};\n"
}
out.println "}"
}
static def generateMapper(out, className, packageName) {
out.println "package $packageName" + ".mapper;"
out.println ""
out.println "import com.test.common.base.BaseMapper;"
out.println "import $packageName" + ".entity.$className;"
out.println ""
out.println "/**\n * @author mazhuang\n */"
out.println "public interface $className" + "Mapper extends BaseMapper<$className> {"
out.println ""
out.println "}"
}
static def generateService(out, className, packageName) {
out.println "package $packageName" + ".service;"
out.println ""
out.println "import com.test.common.base.BaseService;"
out.println "import $packageName" + ".entity.$className;"
out.println ""
out.println "/**\n * @author mazhuang\n */"
out.println "public interface $className" + "Service extends BaseService<$className> {"
out.println ""
out.println "}"
}
static def generateServiceImpl(out, className, packageName) {
out.println "package $packageName" + ".service.impl;"
out.println ""
out.println "import lombok.extern.slf4j.Slf4j;"
out.println "import org.springframework.stereotype.Service;"
out.println "import com.test.common.base.BaseServiceImpl;"
out.println "import $packageName" + ".entity.$className;"
out.println "import $packageName" + ".mapper.$className" + "Mapper;"
out.println "import $packageName" + ".service.$className" + "Service;"
out.println ""
out.println "/**\n * @author mazhuang\n */"
out.println "@Slf4j"
out.println "@Service"
out.println "public class $className" + "ServiceImpl extends BaseServiceImpl<$className" + "Mapper, $className> implements $className" + "Service {"
out.println ""
out.println "}"
}
def calcFields(table) {
DasUtil.getColumns(table).reduce([]) {
fields, col ->
def spec = Case.LOWER.apply(col.getDataType().getSpecification())
def typeStr = typeMapping.find { p, t -> p.matcher(spec).find() }.value
fields += [[
name : javaName(col.getName(), false),
type : typeStr,
comment: col.getComment(), // 注释
default: col.getDefault(), // 默认值
annos : ""]]
}
}
static def calcPackageName(dirPath) {
def startPos = dirPath.indexOf('com')
return dirPath.substring(startPos).replaceAll(File.separator, ".")
}
def javaName(str, capitalize) {
def s = com.intellij.psi.codeStyle.NameUtil.splitNameIntoWords(str)
.collect { Case.LOWER.apply(it).capitalize() }
.join("")
.replaceAll(/[^\p{javaJavaIdentifierPart}[_]]/, "_")
capitalize || s.length() == 1 ? s : Case.LOWER.apply(s[0]) + s[1..-1]
}
大功告成,现在右键一个数据库表,依次选择 Scripted Extensions、Generate MyBatis Code.groovy,在弹出的目录选择框里选择想要放置代码的目录,即可生成期望的模板代码了。
后续如果有一些个性化的代码生成需求,可以根据实际情况修改、新增脚本来完成。
本文代码生成器脚本已上传至 GitHub,仓库地址:https://github.com/mzlogin/code-generator,以后如果有更新,或者新的代码生成脚本,也会放在这个仓库里。
2024-08-13 00:00:00
本文记录了一种使用没有原生支持多路推流的阿里云推流 Android SDK,实现同时推送两路不同画面的流的方法。
项目上有一个用于直播的 APP,可以在 Android 平板上录屏进行推流直播,然后通过阿里云的直播转 VOD 方式形成录播视频。屏幕上的内容分为 A、B 两个区域,如图所示:
原本是只推送 A 区域的画面,也就是用户观看直播和录播时,都只能看到 A 区域。
现在要求变成用户观看直播时,可以看到 A 区域的内容;而观看录播时,可以同时看到 A 区域和 B 区域的内容。
大致思考后,有两种思路:
一种是推流时,推送 A 区域 加 B 区域的画面,然后在观看直播时,将画面进行截取展示,在录播时则直接展示完整画面;
另一种是推流时,分别推送两路流,一路只推送 A 区域,另一路则推送 A 区域 加 B 区域,观看直播时拉第一路流,观看录播时展示第二路流转的 VOD 视频。
基于项目的现状——推流只有一个 Android 端,而播放则需要适配 Web PC、Web Mobile、Android 和 iOS,所以选择了第二种方案,这样只需对推流端进行改造,然后服务端接口进行简单调整即可。
一个残酷的现实是,阿里云推流 Android SDK 并没有原生支持多路推流的功能,官方文档有提到:
AlivcLivePusher目前不支持多实例
经过尝试,发现在一个进程里创建两个 AlivcLivePusher 实例,确实无法同时正常使用。
那只能自己想点黑科技了……
最终的实现方案:
简单来讲,就是在推流时,启动位于另一个进程的 Service,初始化另一个 AlivcLivePusher,进行第二路推流。
每当有新的视频帧时,写入 MemoryFile,然后通过 AIDL 调用将 ParcelFileDescriptor 传递给 Service,Service 读取 MemoryFile,进行处理后推给第二路流。
音频帧同理。
经过一些调试和优化,最终实现了需求里想要的双路推流不同画面的效果。
这种方式虽然有点绕,但是在没有原生支持的情况下,也算是一种可用的方案了。
如果没有项目和业务的历史包袱,可以优先考虑使用原生支持多路推流的 SDK,比如大牛直播等,这样可能会更加方便和稳定。