7.6 任务执行和调度
有些功能并不是通过浏览器主动的访问服务器的,有些功能是服务器定时启动定时运行的,比如:每隔一个小时算一下帖子的分数,每隔半个小时清理一下临时存的文件,这样的需求都需要任务调度的组件去解决。很显然这个任务调度的组件应该是基于多线程的,自动运行肯定是启动一个线程,那个线程独立的运行,我们在程序中但凡要用到多线程,一定是通过线程池使用的,因为我们创建一个线程是有开销的,并且开销比较大,使用线程池去管理这个线程能够让线程复用,提高处理能力,可以节约一些资源。
Spring中
- ThreadPoolTaskExecutor 普通的线程池
- ThreadPoolTaskScheduler 定时的线程池 (但是这个线程池分布式下可能存在问题)
所以分布式下使用 Spring Quartz 做定时任务更多一点
测试的时候记得启动kafka(虽然测试用不到,但是因为配置了kafka,不启动会导致测试失败)
JDK的线程池
@RunWith(SpringRunner.class)
@SpringBootTest
@ContextConfiguration(classes = CommunityApplication.class)
public class ThreadPoolTests {
private static final Logger logger = LoggerFactory.getLogger(ThreadPoolTests.class);
// JDK普通线程池
private ExecutorService executorService = Executors.newFixedThreadPool(5); // 包含5个线程,反复复用这5个
// JDK可执行定时任务的线程池
private ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(5);//5个线程
private void sleep(long m) { // 让当前线程阻塞一会,因为test方法和main方法不一样,在main方法中一个执行完会等另一个
try { // 而在test中是并发的,一个执行完直接就结束了,我们让线程阻塞一下等其他线程执行完
Thread.sleep(m);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 1.JDK普通线程池
@Test
public void testExecutorService() {
Runnable task = new Runnable() {
@Override
public void run() {
logger.debug("Hello ExecutorService");
}
};
for (int i = 0; i < 10; i++) {
executorService.submit(task); // 调submit方法分配一个线程执行task
}
sleep(10000);
}
// 2.JDK定时任务线程池
@Test
public void testScheduledExecutorService() {
Runnable task = new Runnable() {
@Override
public void run() {
logger.debug("Hello ScheduledExecutorService");
}
};
// 延迟 10000 毫秒执行,反复执行时的间间隔1000毫秒 后面的 TimeUnit.MILLISECONDS 表示单位是毫秒
scheduledExecutorService.scheduleAtFixedRate(task, 10000, 1000, TimeUnit.MILLISECONDS);
sleep(30000);
}
}
Spring的线程池
使用Spring线程池时直接注入并且在application.properties做一些配置才可以(配一下启动时候带几个线程)
Spring 普通的线程池 ThreadPoolTaskExecutor 比 JDK 自带的线程池更灵活
Spring 可以定时的线程池 ThreadPoolTaskScheduler
# TaskExecutionProperties Spring的普通线程池配置
# 下面的配置是 核心线程数量是5,不够用时最多扩容到15,到了15还是不够用会把任务先放到队列里然后空闲时再取,缓冲作用,队列内最多缓冲100个
spring.task.execution.pool.core-size=5
spring.task.execution.pool.max-size=15
spring.task.execution.pool.queue-capacity=100
# TaskSchedulingProperties Spring的能启动定时任务的线程池,下面的配置意思是线程池里装的数量是5
spring.task.scheduling.pool.size=5
另外 ThreadPoolTaskScheduler 要生效还要写一个配置类 ThreadPoolConfig
@Configuration
@EnableScheduling // 启用定时任务
@EnableAsync // 加上这个注解可以使用简便方式调用
public class ThreadPoolConfig {
}
测试
@RunWith(SpringRunner.class)
@SpringBootTest
@ContextConfiguration(classes = CommunityApplication.class)
public class ThreadPoolTests {
private static final Logger logger = LoggerFactory.getLogger(ThreadPoolTests.class);
// Spring普通线程池
@Autowired
private ThreadPoolTaskExecutor taskExecutor;
// Spring可执行定时任务的线程池
@Autowired
private ThreadPoolTaskScheduler taskScheduler;
private void sleep(long m) { // 让当前线程阻塞一会,因为test方法和main方法不一样,在main方法中一个执行完会等另一个
try { // 而在test中是并发的,一个执行完直接就结束了,我们让线程阻塞一下等其他线程执行完
Thread.sleep(m);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 3.Spring普通线程池
@Test
public void testThreadPoolTaskExecutor() {
Runnable task = new Runnable() {
@Override
public void run() {
logger.debug("Hello ThreadPoolTaskExecutor");
}
};
for (int i = 0; i < 10; i++) {
taskExecutor.submit(task);
}
sleep(10000);
}
// 4.Spring定时任务线程池
@Test
public void testThreadPoolTaskScheduler() {
Runnable task = new Runnable() {
@Override
public void run() {
logger.debug("Hello ThreadPoolTaskScheduler");
}
};
Date startTime = new Date(System.currentTimeMillis() + 10000);
taskScheduler.scheduleAtFixedRate(task, startTime, 1000);
// 开始的时间:当前+10000ms 1000表示时间间隔为1000ms
sleep(30000);
}
}
简化版调用方式(这种简化方式也要配置application.properties像上面那样)
意思是只要在任意一个类里写一个方法,在这个方法上加上一个注解,它就可以在Spring线程池环境下去运行,相当于把这个写的方法作为线程体,
然后是application.properties配置类就像上面那样(虽然是简化版,但是配置方式还是和Spring中配置是一样的),因为之前已经配过,所以这里不用再配了
AlphaService
@Service
public class AlphaService {
private static final Logger logger = LoggerFactory.getLogger(AlphaService.class);
// Spring普通线程池,让该方法在多线程环境下,被异步的调用,和主线程异步执行,并发执行
@Async
public void execute1() {
logger.debug("execute1");
}
// Spring定时线程池执行简化操作
@Scheduled(initialDelay = 10000, fixedRate = 1000) // 延迟10000ms执行,时间间隔1000ms
public void execute2() {
logger.debug("execute2");
}
}
测试
@RunWith(SpringRunner.class)
@SpringBootTest
@ContextConfiguration(classes = CommunityApplication.class)
public class ThreadPoolTests {
private static final Logger logger = LoggerFactory.getLogger(ThreadPoolTests.class);
// JDK普通线程池
private ExecutorService executorService = Executors.newFixedThreadPool(5); // 包含5个线程,反复复用这5个
// JDK可执行定时任务的线程池
private ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(5);//5个线程
// Spring普通线程池
@Autowired
private ThreadPoolTaskExecutor taskExecutor;
// Spring可执行定时任务的线程池
@Autowired
private ThreadPoolTaskScheduler taskScheduler;
@Autowired
private AlphaService alphaService;
private void sleep(long m) { // 让当前线程阻塞一会,因为test方法和main方法不一样,在main方法中一个执行完会等另一个
try { // 而在test中是并发的,一个执行完直接就结束了,我们让线程阻塞一下等其他线程执行完
Thread.sleep(m);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 5.Spring普通线程池(简化)
@Test
public void testThreadPoolTaskExecutorSimple() {
for (int i = 0; i < 10; i++) {
alphaService.execute1();
}
sleep(10000);
}
// 6.Spring定时任务线程池(简化)
@Test
public void testThreadPoolTaskSchedulerSimple() {
sleep(30000);
}
}
Spring Quartz
因为 Spring的定时任务线程池ThreadPoolTaskScheduler在分布式环境下存在问题,我们使用 Spring Quartz 来做定时任务线程池。
# 我们使用Quartz主要是三个方面
对Job实现类进行配置,这样quartz才能够读取底层信息,生成表里数据,让任务运行起来
用JobDetail接口来配置Job,比如 名字是什么,是哪个组,描述,以及相关参数的配置,
Trigger是触发器的意思,用来配Job什么时候运行,以什么样的频率反复运行,
Spring Quartz 依赖于数据库,它有一套表在用Spring Quartz 时需要提前去创建,
引入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-quartz</artifactId>
</dependency>
首先我们要新建一个类实现Job接口用来定义任务
public class AlphaJob implements Job {
@Override
public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException {
System.out.println(Thread.currentThread().getName() + ": execute a quartz job.");
}
}
对上面的Job实现类进行配置,这样quartz才能够读取底层信息,生成表里数据,让任务运行起来
用JobDetail接口来配置Job,比如 名字是什么,是哪个组,描述,以及相关参数的配置,
Trigger是触发器的意思,用来配Job什么时候运行,以什么样的频率反复运行,
然后写一个配置类配置JobDetail和Trigger
// 配置 -> 数据库 -> 调用
public class QuartzConfig {
// FactoryBean可简化Bean的实例化过程:
// 1.通过FactoryBean封装Bean的实例化过程.
// 2.将FactoryBean装配到Spring容器里.
// 3.将FactoryBean注入给其他的Bean.
// 4.该Bean得到的是FactoryBean所管理的对象实例.
// 配置JobDetail
@Bean
public JobDetailFactoryBean alphaJobDetail() {
JobDetailFactoryBean factoryBean = new JobDetailFactoryBean();
factoryBean.setJobClass(AlphaJob.class);
factoryBean.setName("alphaJob");
factoryBean.setGroup("alphaJobGroup");
factoryBean.setDurability(true); // 持久运行
factoryBean.setRequestsRecovery(true); // 任务可恢复
return factoryBean;
}
// 配置Trigger(SimpleTriggerFactoryBean, CronTriggerFactoryBean)
@Bean
public SimpleTriggerFactoryBean alphaTrigger(JobDetail alphaJobDetail) {
SimpleTriggerFactoryBean factoryBean = new SimpleTriggerFactoryBean();
factoryBean.setJobDetail(alphaJobDetail);
factoryBean.setName("alphaTrigger");
factoryBean.setGroup("alphaTriggerGroup");
factoryBean.setRepeatInterval(3000); // 多久执行一次
factoryBean.setJobDataMap(new JobDataMap());
return factoryBean;
}
}
然后Quartz还要做一个配置,在application.properties做配置
# QuartzProperties配置Quartz
# 下面配置的意思是
# 底层是jdbc
# communityScheduler是调度器名字
# 调度器id自动生成
# 用org.quartz.impl.jdbcjobstore.JobStoreTX将任务存到数据库
# 使用 org.quartz.impl.jdbcjobstore.StdJDBCDelegate 这个jdbc驱动,
# 采用集群方式
# 用org.quartz.simpl.SimpleThreadPool这个线程池
# 线程数量5
spring.quartz.job-store-type=jdbc
spring.quartz.scheduler-name=communityScheduler
spring.quartz.properties.org.quartz.scheduler.instanceId=AUTO
spring.quartz.properties.org.quartz.jobStore.class=org.quartz.impl.jdbcjobstore.JobStoreTX
spring.quartz.properties.org.quartz.jobStore.driverDelegateClass=org.quartz.impl.jdbcjobstore.StdJDBCDelegate
spring.quartz.properties.org.quartz.jobStore.isClustered=true
spring.quartz.properties.org.quartz.threadPool.class=org.quartz.simpl.SimpleThreadPool
spring.quartz.properties.org.quartz.threadPool.threadCount=5
然后只要一启动项目就会输出上面Job中定义的任务,会将数据存到数据库里,之后直接从数据库取,如果以后我们不想输出下面这句话,要么清空相应的表,要么执行程序清空
System.out.println(Thread.currentThread().getName() + ": execute a quartz job.");
下面我们演示一下删除任务的api是啥
@RunWith(SpringRunner.class)
@SpringBootTest
@ContextConfiguration(classes = CommunityApplication.class)
public class QuartzTests {
@Autowired
private Scheduler scheduler; // 注入调度器
@Test
public void testDeleteJob() {
try {
// 删除哪个名字的Job和以及这个Job在哪个组名
boolean result = scheduler.deleteJob(new JobKey("alphaJob", "alphaJobGroup"));
System.out.println(result);
} catch (SchedulerException e) {
e.printStackTrace();
}
}
}
注:如果上面方法删除不了直接清空相应的数据库表,然后注释掉相关的方法
7.7 热帖排行
我们对于点赞、加精、评论的时候我们不去立刻算分,而是把分数变化的帖子先丢到一个缓存里,等定时的时间到了把缓存里这些产生变化的帖子算一下,其他没变的帖子不算,那这样每次算的数据量比较小,效率也比较高。
在能够影响帖子分数处将帖子id存到redis里
既然要往redis里存数据,先在 RedisKeyUtil 里定义一个 key
定义key的方法不需要传postId,因为产生变化的帖子是多个,不是一个,所以不要传帖子id进来
private static final String PREFIX_POST = "post";
// 帖子分数
public static String getPostScoreKey() {
return PREFIX_POST + SPLIT + "score";
}
然后在那些能够影响帖子分数的操作发生的时候把帖子Id扔到 redis 里去,在存的时候我们应该存到 redis 的 set 里,因为我们只需要算一次(如果有多次,每次算都是一样的,重复了,效率不高)。
比如说:
-
新增帖子的时候也给它一个初始的分
-
加精的时候
-
评论的时候
-
点赞的时候
新增帖子时:
@Autowired
private RedisTemplate redisTemplate;
// 把帖子存到redis里
String redisKey = RedisKeyUtil.getPostScoreKey();
redisTemplate.opsForSet().add(redisKey, post.getId());
加精时:
评论帖子时:
注入RedisTemplate
@Autowired
private RedisTemplate redisTemplate;
// 将帖子id存到redis里
String redisKey = RedisKeyUtil.getPostScoreKey();
redisTemplate.opsForSet().add(redisKey, discussPostId);
点赞时:
下面要做的就是每隔一段时间算一下
要更新分数,先添加一下更改分数方法
需要用到定时任务,就用到了 Quartz
首先写一个Job (使用Quartz时需要写的)
PostScoreRefreshJob
public class PostScoreRefreshJob implements Job, CommunityConstant {
private static final Logger logger = LoggerFactory.getLogger(PostScoreRefreshJob.class);
@Autowired
private RedisTemplate redisTemplate; // 计算
@Autowired
private DiscussPostService discussPostService;
@Autowired
private LikeService likeService;
@Autowired
private ElasticsearchService elasticsearchService; // 数据变了,所以要同步到搜索引擎
// 牛客纪元
private static final Date epoch;
// 初始化一下牛客纪元
static {
try {
// SimpleDateFormat能把日期转成字符串,也能把字符串解析为日期,但前提是要指定格式
epoch = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").parse("2014-08-01 00:00:00");
} catch (ParseException e) {
throw new RuntimeException("初始化牛客纪元失败!", e); // 出错误时抛出异常
}
}
@Override
public void execute(JobExecutionContext context) throws JobExecutionException {
String redisKey = RedisKeyUtil.getPostScoreKey();
// 因为对一个key反复操作,所以使用Bound API绑定key
BoundSetOperations operations = redisTemplate.boundSetOps(redisKey);
// 如果没有数据就不用算了
if (operations.size() == 0) {
logger.info("[任务取消] 没有需要刷新的帖子!");
return;
}
logger.info("[任务开始] 正在刷新帖子分数: " + operations.size());
while (operations.size() > 0) {
// refresh是计算,refresh具体内容就在下面
this.refresh((Integer) operations.pop());
}
logger.info("[任务结束] 帖子分数刷新完毕!");
}
private void refresh(int postId) {
DiscussPost post = discussPostService.findDiscussPostById(postId);
if (post == null) {
logger.error("该帖子不存在: id = " + postId);
return;
}
// 是否精华
boolean wonderful = post.getStatus() == 1;
// 评论数量
int commentCount = post.getCommentCount();
// 点赞数量
long likeCount = likeService.findEntityLikeCount(ENTITY_TYPE_POST, postId);
// 计算权重,精华+75分 评论*10 + likeCount*2
double w = (wonderful ? 75 : 0) + commentCount * 10 + likeCount * 2;
// 分数 = 帖子权重 + 距离天数
double score = Math.log10(Math.max(w, 1)) // 权重求个对数,里面和1求max是为了防止取对数之后变成负分
+ (post.getCreateTime().getTime() - epoch.getTime()) / (1000 * 3600 * 24); //ms / (1000 * 3600 * 24)表示天
// 更新帖子分数
discussPostService.updateScore(postId, score);
// 同步搜索数据
post.setScore(score);
elasticsearchService.saveDiscussPost(post);
}
}
然后这个任务要想正常运行我们还得做配置(配置一下JobDetail和Trigger)
QuartzConfig
// 配置 -> 数据库 -> 调用
@Configuration
public class QuartzConfig {
// FactoryBean可简化Bean的实例化过程:
// 1.通过FactoryBean封装Bean的实例化过程.
// 2.将FactoryBean装配到Spring容器里.
// 3.将FactoryBean注入给其他的Bean.
// 4.该Bean得到的是FactoryBean所管理的对象实例.
// 配置JobDetail
//@Bean
public JobDetailFactoryBean alphaJobDetail() {
JobDetailFactoryBean factoryBean = new JobDetailFactoryBean();
factoryBean.setJobClass(AlphaJob.class);
factoryBean.setName("alphaJob");
factoryBean.setGroup("alphaJobGroup");
factoryBean.setDurability(true); // 持久运行
factoryBean.setRequestsRecovery(true); // 任务可恢复
return factoryBean;
}
// 配置Trigger(SimpleTriggerFactoryBean, CronTriggerFactoryBean)
//@Bean
public SimpleTriggerFactoryBean alphaTrigger(JobDetail alphaJobDetail) {
SimpleTriggerFactoryBean factoryBean = new SimpleTriggerFactoryBean();
factoryBean.setJobDetail(alphaJobDetail);
factoryBean.setName("alphaTrigger");
factoryBean.setGroup("alphaTriggerGroup");
factoryBean.setRepeatInterval(3000); // 多久执行一次
factoryBean.setJobDataMap(new JobDataMap());
return factoryBean;
}
// 刷新帖子分数任务
@Bean
public JobDetailFactoryBean postScoreRefreshJobDetail() {
JobDetailFactoryBean factoryBean = new JobDetailFactoryBean();
factoryBean.setJobClass(PostScoreRefreshJob.class);
factoryBean.setName("postScoreRefreshJob");
factoryBean.setGroup("communityJobGroup");
factoryBean.setDurability(true);
factoryBean.setRequestsRecovery(true);
return factoryBean;
}
@Bean
public SimpleTriggerFactoryBean postScoreRefreshTrigger(JobDetail postScoreRefreshJobDetail) {
SimpleTriggerFactoryBean factoryBean = new SimpleTriggerFactoryBean();
factoryBean.setJobDetail(postScoreRefreshJobDetail);
factoryBean.setName("postScoreRefreshTrigger");
factoryBean.setGroup("communityTriggerGroup");
factoryBean.setRepeatInterval(1000 * 60 * 5); // 5分钟执行一次
factoryBean.setJobDataMap(new JobDataMap());
return factoryBean;
}
}
最后记得在application.properties配置一下Quartz
# QuartzProperties配置Quartz
# 下面配置的意思是
# 底层是jdbc
# communityScheduler是调度器名字
# 调度器id自动生成
# 用org.quartz.impl.jdbcjobstore.JobStoreTX将任务存到数据库
# 使用 org.quartz.impl.jdbcjobstore.StdJDBCDelegate 这个jdbc驱动,
# 采用集群方式
# 用org.quartz.simpl.SimpleThreadPool这个线程池
# 线程数量5
spring.quartz.job-store-type=jdbc
spring.quartz.scheduler-name=communityScheduler
spring.quartz.properties.org.quartz.scheduler.instanceId=AUTO
spring.quartz.properties.org.quartz.jobStore.class=org.quartz.impl.jdbcjobstore.JobStoreTX
spring.quartz.properties.org.quartz.jobStore.driverDelegateClass=org.quartz.impl.jdbcjobstore.StdJDBCDelegate
spring.quartz.properties.org.quartz.jobStore.isClustered=true
spring.quartz.properties.org.quartz.threadPool.class=org.quartz.simpl.SimpleThreadPool
spring.quartz.properties.org.quartz.threadPool.threadCount=5
最后启动项目测试的时候一定要开启 kafka、elasticsearch
展现
在主页最新是按时间倒序排,不用管
我们要处理的是点最热,按照分数来排
点最热的时候要给服务端传一个新的参数,让它做一个排序,有一个新的排序模式
所以我们要对之前的代码做一下重构,让它能够支持
DiscussPostMapper
discusspost-mapper.xml
DiscussPostService
HomeController
然后是 index.html 页面
7.8 生成长图
实现分享功能
下载 wkhtmltopdf 工具
从官网下载对应版本然后安装,安装之后配置环境变量
接下来演示一下wkhtmltopdf的命令生成一个页面的pdf、生成一个图片
生成的话要指定本地存放那个生成文件的路径,而且它要求提前把路径建好,这个路径不会随着命令自动创建
提前创建好两个文件夹
D:\work\data\wk-images 生成的图片放这里
D:\work\data\wk-pdfs 生成的pdf放这里
使用命令行测试
wkhtmltopdf https://www.acwing.com D:/work/data/wk-pdfs/1.pdf
wkhtmltoimage https://www.acwing.com D:/work/data/wk-images/1.png
wkhtmltoimage --quality 75 https://www.acwing.com D:/work/data/wk-images/2.png
使用java程序测试
public class WkTests {
public static void main(String[] args) {
String cmd = "d:/work/wkhtmltopdf/bin/wkhtmltoimage --quality 75 https://www.nowcoder.com d:/work/data/wk-images/3.png";
try {
Runtime.getRuntime().exec(cmd); // 将命令交给本地的操作系统
System.out.println("ok.");
} catch (IOException e) {
e.printStackTrace();
}
}
}
接下来开发分享功能
上面我们使用java程序在字符串中写的是一个固定的命令,日后我们想要修改就很不方便,我们可以将生成文件的存放路径配置在application.properties里,日后方便修改
# wk 配置生成图片的命令和图片存放路径
wk.image.command=d:/work/wkhtmltopdf/bin/wkhtmltoimage
wk.image.storage=d:/work/data/wk-images
然后我们服务在启动的时候我们要检查图片存放路径存不存在,如果不存在我要用程序来创建(wk要求图片存放路径必须存在)
@Configuration
public class WkConfig {
private static final Logger logger = LoggerFactory.getLogger(WkConfig.class);
@Value("${wk.image.storage}")
private String wkImageStorage;
@PostConstruct
public void init() {
// 创建WK图片目录
File file = new File(wkImageStorage);
if (!file.exists()) {
file.mkdir(); // 图片存放的路径不存在那就创建这个路径
logger.info("创建WK图片目录: " + wkImageStorage);
}
}
}
常量接口 CommunityConstant 中定义一个常量表示分享
/**
* 主题: 分享
*/
String TOPIC_SHARE = "share";
接下来模拟开发一个分享的功能
写一个controller,处理前端的请求,在这个请求之内我要生成图片,生成图片之后还提供一个请求允许访问这个图片,生成图片的时候时间比较长,使用异步方式,controller只需要把这个事件丢给kafka,后续用kafka异步实现就可以了。
@Controller
public class ShareController implements CommunityConstant {
private static final Logger logger = LoggerFactory.getLogger(ShareController.class);
@Autowired
private EventProducer eventProducer;
@Value("${community.path.domain}")
private String domain; // 项目访问路径,生成图片时用不上,主要用户访问图片时需要一个访问路径
@Value("${server.servlet.context-path}") // 项目域名,生成图片时用不上,主要用户访问图片时需要一个访问路径
private String contextPath;
@Value("${wk.image.storage}") // 从配置文件读取存放路径
private String wkImageStorage;
@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);
return CommunityUtil.getJSONString(0, null, map);
}
// 废弃
// 获取长图
@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"); // 生成的是图片,格式是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());
}
}
}
写一下消费的事件
测试:
7.9 将文件上传至云服务器
将文件上传至云服务器有两种方式:
- 客户端上传
- 客户端将数据提交给云服务器,并等待其响应
- 在我们的项目中,用户上传头像时,将表单数据提交给云服务器采用这种方式
- 服务器(我们的项目)上传
- 项目生成数据提交给云服务器
- 在我们的项目中,我们的分享功能(项目生成的图片)采用这种方式
这里我们使用七牛云服务器
https://www.qiniu.com/
1. 将用户头像上传至七牛云服务器 [客户端上传]
首先七牛云的SDK要访问的话得先把包(直接在仓库搜 qiniu)导进来
<dependency>
<groupId>com.qiniu</groupId>
<artifactId>qiniu-java-sdk</artifactId>
<version>7.11.0</version>
</dependency>
然后在配置文件里再做一点配置,这个配置不是七牛云硬性要求的配置,是我们自定义的配置,是为了让我们的程序更灵活,配什么呢?主要配两个内容,配这个秘钥,一共有两个秘钥,
AccessKey、SecretKey,其中AccessKey是用来标识用户的身份的,就是说我们要往空间里传东西,不是谁都能传的,你只有有AccessKey表示你是当前空间的使用者才有权限传,SecretKey是用来在上传具体内容的时候为这个内容加密,所以需要两个key,这两个key最好把它配到配置文件里,万一将来这两个key有所变化好去改动而不是把它写死在程序里(注意:我们的这个域名只支持http访问,不支持https访问)
# qiniu
qiniu.key.access=Mi4FCwR0WqfDRBBDH-WXBuVPO7pCwppEVILZ6vPR
qiniu.key.secret=tWj3e9Y-nadF-r7NZT-PyWNrk0eO1CV8KN6mxRqG
qiniu.bucket.header.name=communityheader2022lb
quniu.bucket.header.url=http://rfvh53n45.hb-bkt.clouddn.com
qiniu.bucket.share.name=communityshare2022lb
qiniu.bucket.share.url=http://rfvhgbqkf.hb-bkt.clouddn.com
接下来处理上传头像的逻辑
UserController
之前的 uploadHeader、getHeader 方法都废弃掉,因为我们不是将文件提交给项目了,而是提交给骑牛云服务器
然后就是处理对应的表单 setting.html 了,
测试
在项目中上传头像之后
并且项目中头像也正确显示
2. 生成图片时 [项目上传]
接下来我们来重构分享相关的功能
ShareController
然后处理这个事件
7.10 优化网站的性能
将优化热门帖子列表缓存到本地缓存
本地缓存
这里我们使用 Caffeine 做本地缓存
手册:
引入依赖:
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
<version>2.7.0</version>
</dependency>
接下来我们还要设置一下参数,这个参数是我们自定义的参数,并不是 caffeine 强制要求的,为什么要自定义参数呢,因为我们在缓存帖子列表的时候,我们要声明缓存空间能存多少数据,多长时间要把数据释放掉,好让新的数据进来,这么一个自动更新机制。这个参数应该做成可配的,将来以便于修改。在application.properties中设置
# caffeine,posts表示缓存的是名字,这个缓存空间里最多能存15个对象(页),过期时间设置为180s
caffeine.posts.max-size=15
caffeine.posts.expire-seconds=180
然后优化查询的方法,通常都是优化service
DiscussPostService