第7章 项目进阶,构建安全高效的企业服务(下)

7.6 任务执行和调度

image-20220730074407613

有些功能并不是通过浏览器主动的访问服务器的,有些功能是服务器定时启动定时运行的,比如:每隔一个小时算一下帖子的分数,每隔半个小时清理一下临时存的文件,这样的需求都需要任务调度的组件去解决。很显然这个任务调度的组件应该是基于多线程的,自动运行肯定是启动一个线程,那个线程独立的运行,我们在程序中但凡要用到多线程,一定是通过线程池使用的,因为我们创建一个线程是有开销的,并且开销比较大,使用线程池去管理这个线程能够让线程复用,提高处理能力,可以节约一些资源。

Spring中

  • ThreadPoolTaskExecutor 普通的线程池
  • ThreadPoolTaskScheduler 定时的线程池 (但是这个线程池分布式下可能存在问题)

​ 所以分布式下使用 Spring Quartz 做定时任务更多一点

image-20220730101507337

测试的时候记得启动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

image-20220730141229258

另外 ThreadPoolTaskScheduler 要生效还要写一个配置类 ThreadPoolConfig

@Configuration
@EnableScheduling                   // 启用定时任务
@EnableAsync                        // 加上这个注解可以使用简便方式调用
public class ThreadPoolConfig {
}

image-20220730141302542

测试

@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线程池环境下去运行,相当于把这个写的方法作为线程体,

image-20220730100005654

然后是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 时需要提前去创建,

image-20220730101803369

引入依赖

<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.");
    }
}

image-20220730141441772

对上面的Job实现类进行配置,这样quartz才能够读取底层信息,生成表里数据,让任务运行起来

JobDetail接口来配置Job,比如 名字是什么,是哪个组,描述,以及相关参数的配置,

Trigger是触发器的意思,用来配Job什么时候运行,以什么样的频率反复运行,

然后写一个配置类配置JobDetailTrigger

// 配置 -> 数据库 -> 调用
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;
    }
}

image-20220730141550487

然后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

image-20220730141629183

然后只要一启动项目就会输出上面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 热帖排行

image-20220730141821385

我们对于点赞、加精、评论的时候我们不去立刻算分,而是把分数变化的帖子先丢到一个缓存里,等定时的时间到了把缓存里这些产生变化的帖子算一下,其他没变的帖子不算,那这样每次算的数据量比较小,效率也比较高。

在能够影响帖子分数处将帖子id存到redis里

既然要往redis里存数据,先在 RedisKeyUtil 里定义一个 key

定义key的方法不需要传postId,因为产生变化的帖子是多个,不是一个,所以不要传帖子id进来

private static final String PREFIX_POST = "post";
// 帖子分数
public static String getPostScoreKey() {
  return PREFIX_POST + SPLIT + "score";
}

image-20220730154142874

image-20220730154157890

然后在那些能够影响帖子分数的操作发生的时候把帖子Id扔到 redis 里去,在存的时候我们应该存到 redis 的 set 里,因为我们只需要算一次(如果有多次,每次算都是一样的,重复了,效率不高)。

比如说:

  • 新增帖子的时候也给它一个初始的分

  • 加精的时候

  • 评论的时候

  • 点赞的时候

新增帖子时:

@Autowired
private RedisTemplate redisTemplate;

// 把帖子存到redis里
String redisKey = RedisKeyUtil.getPostScoreKey();
redisTemplate.opsForSet().add(redisKey, post.getId());

image-20220730154239898

image-20220730154323647

加精时:

image-20220730154401878

评论帖子时:

​ 注入RedisTemplate

@Autowired
private RedisTemplate redisTemplate;
// 将帖子id存到redis里
String redisKey = RedisKeyUtil.getPostScoreKey();
redisTemplate.opsForSet().add(redisKey, discussPostId);

image-20220730160316505

image-20220730160353445

点赞时:

image-20220730160757322

image-20220730160905868

下面要做的就是每隔一段时间算一下

要更新分数,先添加一下更改分数方法

image-20220730165750368

image-20220730165900362

image-20220730170007618

需要用到定时任务,就用到了 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);
    }

}

image-20220730183829362

image-20220730183901359

image-20220730183926030

然后这个任务要想正常运行我们还得做配置(配置一下JobDetailTrigger)

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;
    }
}

image-20220730183955346

image-20220730184011725

最后记得在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

image-20220730184042598

最后启动项目测试的时候一定要开启 kafkaelasticsearch

展现

在主页最新是按时间倒序排,不用管

我们要处理的是点最热,按照分数来排

最热的时候要给服务端传一个新的参数,让它做一个排序,有一个新的排序模式

所以我们要对之前的代码做一下重构,让它能够支持

DiscussPostMapper

image-20220730184109349

discusspost-mapper.xml

image-20220730184149359

DiscussPostService

image-20220730184231701

HomeController

image-20220730184334650

然后是 index.html 页面

image-20220730184457648

7.8 生成长图

实现分享功能

image-20220731065434263

下载 wkhtmltopdf 工具

image-20220731070314341

从官网下载对应版本然后安装,安装之后配置环境变量

image-20220731070859167

image-20220731071026087

image-20220731071106960

接下来演示一下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 

image-20220731075609588

使用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

image-20220731094439558

然后我们服务在启动的时候我们要检查图片存放路径存不存在,如果不存在我要用程序来创建(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);
        }
    }

}

image-20220731094548171

常量接口 CommunityConstant 中定义一个常量表示分享

/**
 * 主题: 分享
 */
String TOPIC_SHARE = "share";

image-20220731094628356

接下来模拟开发一个分享的功能

写一个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());
        }
    }

}

image-20220731094815110

image-20220731094848383

image-20220731094947063

写一下消费的事件

image-20220731095025781

测试:

image-20220731095045595

image-20220731095105985

7.9 将文件上传至云服务器

image-20220731141636195

将文件上传至云服务器有两种方式:

  • 客户端上传
    • 客户端将数据提交给云服务器,并等待其响应
    • 在我们的项目中,用户上传头像时,将表单数据提交给云服务器采用这种方式
  • 服务器(我们的项目)上传
    • 项目生成数据提交给云服务器
    • 在我们的项目中,我们的分享功能(项目生成的图片)采用这种方式

这里我们使用七牛云服务器

https://www.qiniu.com/

image-20220731145218703

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

image-20220801073435956

接下来处理上传头像的逻辑

UserController

image-20220801070349082

image-20220801070543812

image-20220801070605764

之前的 uploadHeadergetHeader 方法都废弃掉,因为我们不是将文件提交给项目了,而是提交给骑牛云服务器

image-20220801070804591

image-20220801070846883

然后就是处理对应的表单 setting.html 了,

image-20220801074020205

image-20220801074142577

image-20220801074203877

image-20220801074358691

测试
在项目中上传头像之后

image-20220801074447349

并且项目中头像也正确显示

2. 生成图片时 [项目上传]

接下来我们来重构分享相关的功能

ShareController

image-20220801084747186

image-20220801084823461

image-20220801084837806

然后处理这个事件

image-20220801085014774

image-20220801085039917

image-20220801085110709

image-20220801085134346

7.10 优化网站的性能

image-20220801112123295

image-20220801142701119

将优化热门帖子列表缓存到本地缓存

本地缓存

这里我们使用 Caffeine 做本地缓存

手册:

image-20220801151246800

引入依赖:

<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

image-20220801153333064

然后优化查询的方法,通常都是优化service

DiscussPostService

image-20220801163059457

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值