BBS论坛项目相关-19:零碎功能补充与项目部署
生成长图:wkhtmltopdf
Runtime.getRuntime().exec(cmd);
为了保证wk生成长图放入的文件夹存在,配置一个初始化回调方法,判断创建文件夹。
@PostConstruct
public void init() {
// 创建WK图片目录
File file = new File(wkImageStorage);
if (!file.exists()) {
file.mkdir();
logger.info("创建WK图片目录: " + wkImageStorage);
}
}
为了用户体验,可以异步生成长图,添加一个Kafka分享topic
@RequestMapping(path = "/share", method = RequestMethod.GET)
@ResponseBody
public String share(String htmlUrl) {
// 文件名
String fileName = CommunityUtil.generateUUID();
// 异步生成长图
Event event = new Event()
.setTopic(TOPIC_SHARE)
.setData("htmlUrl", htmlUrl)
.setData("fileName", fileName)
.setData("suffix", ".png");
eventProducer.fireEvent(event);
// 返回访问路径
Map<String, Object> map = new HashMap<>();
// map.put("shareUrl", domain + contextPath + "/share/image/" + fileName);
map.put("shareUrl", shareBucketUrl + "/" + fileName);
return CommunityUtil.getJSONString(0, null, map);
}
点击分享后,将分享事件发送出去,然后返回访问路径
消费者分享事件
@KafkaListener(topics = TOPIC_SHARE)
public void handleShareMessage(ConsumerRecord record) {
if (record == null || record.value() == null) {
logger.error("消息的内容为空!");
return;
}
Event event = JSONObject.parseObject(record.value().toString(), Event.class);
if (event == null) {
logger.error("消息格式错误!");
return;
}
String htmlUrl = (String) event.getData().get("htmlUrl");
String fileName = (String) event.getData().get("fileName");
String suffix = (String) event.getData().get("suffix");
String cmd = wkImageCommand + " --quality 75 "
+ htmlUrl + " " + wkImageStorage + "/" + fileName + suffix;
try {
Runtime.getRuntime().exec(cmd);
logger.info("生成长图成功: " + cmd);
} catch (IOException e) {
logger.error("生成长图失败: " + e.getMessage());
}
}
获取长图
@RequestMapping(path = "/share/image/{fileName}", method = RequestMethod.GET)
public void getShareImage(@PathVariable("fileName") String fileName, HttpServletResponse response) {
if (StringUtils.isBlank(fileName)) {
throw new IllegalArgumentException("文件名不能为空!");
}
response.setContentType("image/png");
File file = new File(wkImageStorage + "/" + fileName + ".png");
try {
OutputStream os = response.getOutputStream();
FileInputStream fis = new FileInputStream(file);
byte[] buffer = new byte[1024];
int b = 0;
while ((b = fis.read(buffer)) != -1) {
os.write(buffer, 0, b);
}
} catch (IOException e) {
logger.error("获取长图失败: " + e.getMessage());
}
}
将文件上传到云服务器
客户端上传:客户端将数据提交给云服务器,并等待器响应
服务器直传:应用服务器将数据直接提交给云服务器,并等待其响应。
上传到七牛云,暂时不想写~ 下次再说吧~~
优化网站性能
本地缓存:将数据缓存在应用服务器上,性能最好
常用缓存工具:Ehacache,Guava,Caffeine等
分布式缓存:
将数据缓存在NoSQL数据库上,跨服务器
常用缓存工具:MemCache,Redis等
多级缓存:
一级缓存(本地缓存)>二级缓存(分布式缓存)>DB
避免缓存雪崩(缓存失效,大量请求直达DB),提高系统的可用性
缓存热门帖子
使用本地缓存Caffeine
Caffeine配置
caffeine.posts.max-size=15
caffeine.posts.expire-seconds=180
Caffeine核心接口:Cache,LoadingCache,AsyncLoadingCache
在初始化回调方法中进行缓存,以分页数据作为key进行缓存,调用缓存接口时将分页详情传入,在实现缓存初始化时根据分页数据查询
// Caffeine核心接口: Cache, LoadingCache, AsyncLoadingCache
// 帖子列表缓存
private LoadingCache<String, List<DiscussPost>> postListCache;
// 帖子总数缓存
private LoadingCache<Integer, Integer> postRowsCache;
@PostConstruct
public void init() {
// 初始化帖子列表缓存
postListCache = Caffeine.newBuilder()
.maximumSize(maxSize)
.expireAfterWrite(expireSeconds, TimeUnit.SECONDS)
.build(new CacheLoader<String, List<DiscussPost>>() {
@Nullable
@Override
public List<DiscussPost> load(@NonNull String key) throws Exception {
if (key == null || key.length() == 0) {
throw new IllegalArgumentException("参数错误!");
}
String[] params = key.split(":");
if (params == null || params.length != 2) {
throw new IllegalArgumentException("参数错误!");
}
int offset = Integer.valueOf(params[0]);
int limit = Integer.valueOf(params[1]);
// 二级缓存: Redis -> mysql
logger.debug("load post list from DB.");
return discussPostMapper.selectDiscussPosts(0, offset, limit, 1);
}
});
// 初始化帖子总数缓存
postRowsCache = Caffeine.newBuilder()
.maximumSize(maxSize)
.expireAfterWrite(expireSeconds, TimeUnit.SECONDS)
.build(new CacheLoader<Integer, Integer>() {
@Nullable
@Override
public Integer load(@NonNull Integer key) throws Exception {
logger.debug("load post rows from DB.");
return discussPostMapper.selectDiscussPostRows(key);
}
});
}
在返回帖子列表的方法中插入缓存,对于热门帖子的查询先查找缓存,再查找数据库。热门帖子userid=0,位于首页,mode=1,按热度排序,此时从缓存中取数据
public List<DiscussPost> findDiscussPosts(int userId, int offset, int limit, int orderMode) {
if (userId == 0 && orderMode == 1) {
return postListCache.get(offset + ":" + limit);
}
logger.debug("load post list from DB.");
return discussPostMapper.selectDiscussPosts(userId, offset, limit, orderMode);
}
单元测试
保证测试方法的独立性
初始化数据,执行测试代码,验证测试结果,清理测试数据
常用注解:@BeforeClass @AfterClass @Before @After
项目监控
Spring boot Actuator:
Endpoints:监控应用的入口,springboot内置了很多端点,也支持自定义端点
监控方式:HTTP/JMX
访问路径:"/actuator/health"
注意事项:按需配置暴露的端点,并对所有端点进行权限控制
默认打开两个端口"/health","/info"
配置暴露端点:
management.endpoints.web.exposure.include=*
management.endpoints.web.exposure.exclude=info,caches
监控数据库连接是否正常
自定义端点监控数据库连接
@Component
@Endpoint(id = "database")
public class DatabaseEndpoint {
private static final Logger logger = LoggerFactory.getLogger(DatabaseEndpoint.class);
@Autowired
private DataSource dataSource;
@ReadOperation
public String checkConnection() {
try (
Connection conn = dataSource.getConnection();
) {
return CommunityUtil.getJSONString(0, "获取连接成功!");
} catch (SQLException e) {
logger.error("获取连接失败:" + e.getMessage());
return CommunityUtil.getJSONString(1, "获取连接失败!");
}
}
}
项目部署
写不动了,以后更新~~