黑马程序员-学成在线项目-课程发布笔记

引入sdk,实现分布式事务

在引入消息sdk,并完成存储课程发布表的同时插入消息表这一步骤后,下面就应该调用sdk的接口扫描消息表(也就是执行查询操作),课程缓存,课程所有,上传课程静态页面,删除消息表(也就是执行删除操作)这几步。

查询和删除sdk中已经写好,我们只需要重写sdk中的execute方法,实现中间三步即可。而execute方法是在sdk的service包下的MessageProcessAbstract抽象类中。所以我们在content-service的service包下定义一个jobhandler包,里面放执行任务调度的类。而现在我们需要执行的任务调度就是课程发布:

@Slf4j
@Component
/**
 * 课程发布任务类
 */
public class CoursePublishTask extends MessageProcessAbstract {

    @Autowired
    CoursePublishService coursePublishService;

    @Autowired
    SearchServiceClient searchServiceClient;

    @Autowired
    CoursePublishMapper coursePublishMapper;

    //任务调度入口
    @XxlJob("CoursePublishJobHandler")
    public void coursePublishJobHandler() throws Exception{
        // 分片参数
        int shardIndex = XxlJobHelper.getShardIndex();
        int shardTotal = XxlJobHelper.getShardTotal();
        log.debug("shardIndex="+shardIndex+",shardTotal="+shardTotal);
        //调用抽象类的方法执行任务
        //参数:分片序号、分片总数、消息类型、一次最多取到的任务数量、一次任务调度执行的超时时间
        process(shardIndex,shardTotal,"course_publish",30,60);
    }

    //课程发布任务处理
    @Override
    public boolean execute(MqMessage mqMessage) {
        //获取消息相关的业务信息(拿到课程id)
        String businessKey1 = mqMessage.getBusinessKey1();
        long courseId = Integer.parseInt(businessKey1);
        //课程静态化
        generateCourseHtml(mqMessage,courseId);
        //课程索引
        saveCourseIndex(mqMessage,courseId);
        //课程缓存
        saveCourseCache(mqMessage,courseId);
        return true;
    }


    //生成课程静态化页面并上传至文件系统
    public void generateCourseHtml(MqMessage mqMessage,long courseId){
        log.debug("开始进行课程静态化,课程id:{}",courseId);
        //消息id
        Long id = mqMessage.getId();
        //消息处理的service
        MqMessageService mqMessageService = this.getMqMessageService();
        //消息幂等性处理
        int stageOne = mqMessageService.getStageOne(id);
        if(stageOne == 1){
            log.debug("课程静态化已处理直接返回,课程id:{}",courseId);
            return ;
        }

        //生成静态化页面
        File file = coursePublishService.generateCourseHtml(courseId);
        //上传静态化页面
        if(file!=null){
            coursePublishService.uploadCourseHtml(courseId,file);
        }
        //保存第一阶段状态
        mqMessageService.completedStageOne(id);

    }

    //将课程信息缓存至redis
    public void saveCourseCache(MqMessage mqMessage,long courseId){
        log.debug("将课程信息缓存至redis,课程id:{}",courseId);
        //消息id
        Long id = mqMessage.getId();
        //消息处理的service
        MqMessageService mqMessageService = this.getMqMessageService();
        //消息幂等性处理
        //取出阶段三的执行状态
        int stageThree = mqMessageService.getStageThree(id);
        if(stageThree >0){
            log.debug("课程静态化已处理直接返回,课程id:{}",courseId);
            return ;
        }
        try {
            TimeUnit.SECONDS.sleep(10);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        //保存第三阶段状态
        mqMessageService.completedStageThree(id);


    }
    //保存课程索引信息
    public void saveCourseIndex(MqMessage mqMessage, long courseId){
        log.debug("保存课程索引信息,课程id:{}",courseId);
        //消息id
        Long id = mqMessage.getId();
        //消息处理的service
        MqMessageService mqMessageService = this.getMqMessageService();
        //取出阶段二的执行状态
        int stageTwo = mqMessageService.getStageTwo(id);
        //消息幂等性处理
        if(stageTwo >0){
            log.debug("课程索引已处理直接返回,课程id:{}",courseId);
            return ;
        }
        //查询课程信息,调用搜索服务添加索引接口
        //取出课程发布信息
        CoursePublish coursePublish = coursePublishMapper.selectById(courseId);
        //拷贝至课程索引对象
        CourseIndex courseIndex = new CourseIndex();
        BeanUtils.copyProperties(coursePublish,courseIndex);
        //远程调用搜索服务api添加课程信息到索引
        Boolean add = searchServiceClient.add(courseIndex);
        if(!add){
            XueChengPlusException.cast("远程调用搜索服务添加索引失败");
        }
        //完成本阶段的任务
        mqMessageService.completedStageTwo(id);
    }

}

@XxlJob("CoursePublishJobHandler"):

这个注解标记了一个方法作为一个XXL-Job任务处理器,当任务调度器触发 CoursePublishJobHandler 任务时,将会执行 coursePublishJobHandler 方法。

coursePublishJobHandler 方法是任务调度的入口,它获取了分片参数,然后调用了 sdk提供的抽象方法process 执行任务。

/**
     * @description 扫描消息表多线程执行任务
     * @param shardIndex 分片序号
     * @param shardTotal 分片总数
     * @param messageType  消息类型
     * @param count  一次取出任务总数
     * @param timeout 预估任务执行时间,到此时间如果任务还没有结束则强制结束 单位秒
     * @return void
    */
    public void process(int shardIndex, int shardTotal,  String messageType,int count,long timeout) {

        try {
            //扫描消息表获取任务清单
            List<MqMessage> messageList = mqMessageService.getMessageList(shardIndex, shardTotal,messageType, count);
            //任务个数
            int size = messageList.size();
            log.debug("取出待处理消息"+size+"条");
            if(size<=0){
                return ;
            }

            //创建线程池
            ExecutorService threadPool = Executors.newFixedThreadPool(size);
            //计数器
            CountDownLatch countDownLatch = new CountDownLatch(size);
            messageList.forEach(message -> {
                threadPool.execute(() -> {
                    log.debug("开始任务:{}",message);
                    //处理任务
                    try {
                        boolean result = execute(message);
                        if(result){
                            log.debug("任务执行成功:{})",message);
                            //更新任务状态,删除消息表记录,添加到历史表
                            int completed = mqMessageService.completed(message.getId());
                            if (completed>0){
                                log.debug("任务执行成功:{}",message);
                            }else{
                                log.debug("任务执行失败:{}",message);
                            }
                        }
                    } catch (Exception e) {
                        e.printStackTrace();
                        log.debug("任务出现异常:{},任务:{}",e.getMessage(),message);
                    }finally {
                        //计数
                        countDownLatch.countDown();
                    }
                    log.debug("结束任务:{}",message);

                });
            });

            //等待,给一个充裕的超时时间,防止无限等待,到达超时时间还没有处理完成则结束任务
            countDownLatch.await(timeout,TimeUnit.SECONDS);
            System.out.println("结束....");
        } catch (InterruptedException e) {
           e.printStackTrace();

        }


    }

执行逻辑:

1.首先,通过调用 mqMessageService.getMessageList() 方法从消息表中获取待处理的任务清单。
然后,创建一个固定大小的线程池 threadPool,线程池的大小为待处理任务的数量。
2.使用 CountDownLatch 来控制任务的执行,在每个任务执行完成后,调用 countDownLatch.countDown() 方法进行计数。
3.对于每个任务,通过 threadPool.execute() 提交到线程池中执行,执行的具体任务由 execute() 方法实现。
4.等待所有任务执行完成,调用 countDownLatch.await(timeout, TimeUnit.SECONDS) 方法,等待一段预估的超时时间,如果超过了这个时间仍有任务没有执行完成,则强制结束任务。

1.实现生成课程静态化页面并上传至文件系统

调用service层的CoursePublishService接口如下:

/**
     * @description 课程静态化
     * @param courseId  课程id
     * @return File 静态化文件
     */
    public File generateCourseHtml(Long courseId);
    /**
     * @description 上传课程静态化页面
     * @param file  静态化文件
     * @return void
     */
    public void  uploadCourseHtml(Long courseId,File file);

接口实现如下:

@Override
    public File generateCourseHtml(Long courseId) {

        //静态化文件
        File htmlFile  = null;

        try {
            //配置freemarker
            Configuration configuration = new Configuration(Configuration.getVersion());

            //加载模板
            //选指定模板路径,classpath下templates下
            //得到classpath路径
            String classpath = this.getClass().getResource("/").getPath();
            configuration.setDirectoryForTemplateLoading(new File(classpath + "/templates/"));
            //设置字符编码
            configuration.setDefaultEncoding("utf-8");

            //指定模板文件名称
            Template template = configuration.getTemplate("course_template.ftl");

            //准备数据
            CoursePreviewDto coursePreviewInfo = this.getCoursePreviewInfo(courseId);

            Map<String, Object> map = new HashMap<>();
            map.put("model", coursePreviewInfo);

            //静态化
            //参数1:模板,参数2:数据模型
            String content = FreeMarkerTemplateUtils.processTemplateIntoString(template, map);
//            System.out.println(content);
            //将静态化内容输出到文件中
            InputStream inputStream = IOUtils.toInputStream(content);
            //创建静态化文件
            htmlFile = File.createTempFile("course",".html");
            log.debug("课程静态化,生成静态文件:{}",htmlFile.getAbsolutePath());
            //输出流
            FileOutputStream outputStream = new FileOutputStream(htmlFile);
            IOUtils.copy(inputStream, outputStream);
        } catch (Exception e) {
            log.error("课程静态化异常:{}",e.toString());
            XueChengPlusException.cast("课程静态化异常");
        }

        return htmlFile;
    }

    @Override
    public void uploadCourseHtml(Long courseId, File file) {
        MultipartFile multipartFile = MultipartSupportConfig.getMultipartFile(file);
        String course = mediaServiceClient.uploadFile(multipartFile, "course/"+courseId+".html");
        if(course==null){
            XueChengPlusException.cast("上传静态文件异常");
        }
    }

这两个方法分别是 `generateCourseHtml` 和 `uploadCourseHtml`,用于生成课程静态化页面并上传至文件系统。

1. `generateCourseHtml(Long courseId)`: 这个方法用于生成课程静态化页面。它的实现逻辑包括:
   - 配置Freemarker模板引擎,加载模板文件。
   - 准备数据,调用 `getCoursePreviewInfo` 方法获取课程预览信息。
   - 将模板和数据模型传入Freemarker引擎中进行静态化,得到静态化后的内容。
   - 将静态化内容写入临时文件中,并返回该文件。

2. `uploadCourseHtml(Long courseId, File file)`: 这个方法用于上传课程静态化页面至文件系统。它的实现逻辑包括:
   - 将文件转换为 `MultipartFile` 对象。
   - 调用 `mediaServiceClient.uploadFile` 方法上传文件至文件系统,其中 `mediaServiceClient` 是一个远程服务的客户端,用于与文件系统交互。
   - 如果上传成功,则返回文件路径,否则抛出异常。

在uploadCourseHtml方法中调用了config包下的MultipartSupportConfig类:

@Configuration
public class MultipartSupportConfig {

    @Autowired
    private ObjectFactory<HttpMessageConverters> messageConverters;

    @Bean
    @Primary//注入相同类型的bean时优先使用
    @Scope("prototype")
    public Encoder feignEncoder() {
        return new SpringFormEncoder(new SpringEncoder(messageConverters));
    }

    //将file转为Multipart
    public static MultipartFile getMultipartFile(File file) {
        FileItem item = new DiskFileItemFactory().createItem("file", MediaType.MULTIPART_FORM_DATA_VALUE, true, file.getName());
        try (FileInputStream inputStream = new FileInputStream(file);
             OutputStream outputStream = item.getOutputStream();) {
            IOUtils.copy(inputStream, outputStream);

        } catch (Exception e) {
            e.printStackTrace();
        }
        return new CommonsMultipartFile(item);
    }
}

这段代码是一个Spring配置类,其中定义了一个 `MultipartSupportConfig` 类,用于配置多部分数据支持(Multipart)。让我们逐一解释其中的内容:

 @Configuration

这个注解表示这是一个配置类,在Spring应用程序上下文中会被加载和解析。

 @Autowired
 private ObjectFactory<HttpMessageConverters> messageConverters;

这行代码使用了`@Autowired`注解,将一个 `ObjectFactory<HttpMessageConverters>` 类型的实例注入到了 `messageConverters` 字段中。`ObjectFactory` 是Spring的一个工厂接口,用于创建对象的实例。

@Bean 
@Primary
@Scope("prototype") 
public Encoder feignEncoder() {
        return new SpringFormEncoder(new SpringEncoder(messageConverters));
    }

这个方法定义了一个名为 `feignEncoder` 的Bean,用于配置Feign的编码器。`@Primary` 注解表示在注入相同类型的Bean时优先使用这个Bean,`@Scope("prototype")` 注解表示每次调用这个Bean时都会创建一个新的实例。这个Bean返回了一个 `SpringFormEncoder` 对象,它接受一个 `SpringEncoder` 对象作为参数,并且使用了 `messageConverters` 字段注入的对象。

 public static MultipartFile getMultipartFile(File file) {...}

这个方法定义了一个静态方法,用于将一个 `File` 对象转换为 `MultipartFile` 对象。在方法中,它使用了 `DiskFileItemFactory` 和 `FileItem` 来创建一个临时文件项,然后将文件的内容复制到这个临时文件项中,并最终将其包装为一个 `CommonsMultipartFile` 对象返回。

总的来说,这段代码主要是用于配置Multipart支持,并且提供了一个静态方法来将 `File` 对象转换为 `MultipartFile` 对象,以便在应用程序中方便地处理文件上传。

在uploadCourseHtml方法中还调用了feignclient包下的MediaServiceClient类:

@FeignClient(value = "media-api",configuration = MultipartSupportConfig.class)
/**
 * 媒资管理服务远程接口
 */
public interface MediaServiceClient {

    @RequestMapping(value = "/media/upload/coursefile",consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
    String uploadFile(@RequestPart("filedata") MultipartFile upload, @RequestParam(value = "objectName",required=false) String objectName);
}

这段代码定义了一个名为 `MediaServiceClient` 的Feign客户端接口,用于远程调用媒资管理服务的上传文件接口。

@FeignClient(value = "media-api",configuration = MultipartSupportConfig.class)

这个注解标记了一个Feign客户端接口,指定了要调用的远程服务的名称为 `media-api`,并且指定了 `MultipartSupportConfig` 类作为配置类。Feign客户端接口中的方法会被动态代理实现,并通过HTTP请求调用远程服务的相应接口。

@RequestMapping(value = "/media/upload/coursefile",consumes = MediaType.MULTIPART_FORM_DATA_VALUE)

这个注解标记了一个方法,指定了要调用的远程服务的路径为 `/media/upload/coursefile`,并且指定了请求的 `Content-Type` 为 `multipart/form-data`。这个方法用于上传文件到媒资管理服务。

String uploadFile(@RequestPart("filedata") MultipartFile upload, @RequestParam(value = "objectName",required=false) String objectName)

这个方法定义了一个远程调用的接口方法,用于上传文件到媒资管理服务。方法参数包括:
   - `@RequestPart("filedata") MultipartFile upload`:用于接收上传的文件,通过 `filedata` 参数名来指定。
   - `@RequestParam(value = "objectName",required=false) String objectName`:用于接收文件在文件系统中的对象名称,这是一个可选参数。

package org.springframework.web.multipart;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;

import org.springframework.core.io.InputStreamSource;
import org.springframework.core.io.Resource;
import org.springframework.lang.Nullable;
import org.springframework.util.FileCopyUtils;

/**
 * A representation of an uploaded file received in a multipart request.
 *
 * <p>The file contents are either stored in memory or temporarily on disk.
 * In either case, the user is responsible for copying file contents to a
 * session-level or persistent store as and if desired. The temporary storage
 * will be cleared at the end of request processing.
 *
 * @author Juergen Hoeller
 * @author Trevor D. Cook
 * @since 29.09.2003
 * @see org.springframework.web.multipart.MultipartHttpServletRequest
 * @see org.springframework.web.multipart.MultipartResolver
 */
public interface MultipartFile extends InputStreamSource {

	/**
	 * Return the name of the parameter in the multipart form.
	 * @return the name of the parameter (never {@code null} or empty)
	 */
	String getName();

	/**
	 * Return the original filename in the client's filesystem.
	 * <p>This may contain path information depending on the browser used,
	 * but it typically will not with any other than Opera.
	 * @return the original filename, or the empty String if no file has been chosen
	 * in the multipart form, or {@code null} if not defined or not available
	 * @see org.apache.commons.fileupload.FileItem#getName()
	 * @see org.springframework.web.multipart.commons.CommonsMultipartFile#setPreserveFilename
	 */
	@Nullable
	String getOriginalFilename();

	/**
	 * Return the content type of the file.
	 * @return the content type, or {@code null} if not defined
	 * (or no file has been chosen in the multipart form)
	 */
	@Nullable
	String getContentType();

	/**
	 * Return whether the uploaded file is empty, that is, either no file has
	 * been chosen in the multipart form or the chosen file has no content.
	 */
	boolean isEmpty();

	/**
	 * Return the size of the file in bytes.
	 * @return the size of the file, or 0 if empty
	 */
	long getSize();

	/**
	 * Return the contents of the file as an array of bytes.
	 * @return the contents of the file as bytes, or an empty byte array if empty
	 * @throws IOException in case of access errors (if the temporary store fails)
	 */
	byte[] getBytes() throws IOException;

	/**
	 * Return an InputStream to read the contents of the file from.
	 * <p>The user is responsible for closing the returned stream.
	 * @return the contents of the file as stream, or an empty stream if empty
	 * @throws IOException in case of access errors (if the temporary store fails)
	 */
	@Override
	InputStream getInputStream() throws IOException;

	/**
	 * Return a Resource representation of this MultipartFile. This can be used
	 * as input to the {@code RestTemplate} or the {@code WebClient} to expose
	 * content length and the filename along with the InputStream.
	 * @return this MultipartFile adapted to the Resource contract
	 * @since 5.1
	 */
	default Resource getResource() {
		return new MultipartFileResource(this);
	}

	/**
	 * Transfer the received file to the given destination file.
	 * <p>This may either move the file in the filesystem, copy the file in the
	 * filesystem, or save memory-held contents to the destination file. If the
	 * destination file already exists, it will be deleted first.
	 * <p>If the target file has been moved in the filesystem, this operation
	 * cannot be invoked again afterwards. Therefore, call this method just once
	 * in order to work with any storage mechanism.
	 * <p><b>NOTE:</b> Depending on the underlying provider, temporary storage
	 * may be container-dependent, including the base directory for relative
	 * destinations specified here (e.g. with Servlet 3.0 multipart handling).
	 * For absolute destinations, the target file may get renamed/moved from its
	 * temporary location or newly copied, even if a temporary copy already exists.
	 * @param dest the destination file (typically absolute)
	 * @throws IOException in case of reading or writing errors
	 * @throws IllegalStateException if the file has already been moved
	 * in the filesystem and is not available anymore for another transfer
	 * @see org.apache.commons.fileupload.FileItem#write(File)
	 * @see javax.servlet.http.Part#write(String)
	 */
	void transferTo(File dest) throws IOException, IllegalStateException;

	/**
	 * Transfer the received file to the given destination file.
	 * <p>The default implementation simply copies the file input stream.
	 * @since 5.1
	 * @see #getInputStream()
	 * @see #transferTo(File)
 	 */
	default void transferTo(Path dest) throws IOException, IllegalStateException {
		FileCopyUtils.copy(getInputStream(), Files.newOutputStream(dest));
	}

}

这是Spring框架中 `MultipartFile` 接口的源代码。这个接口代表了一个在多部分请求中接收到的上传文件。

让我们来看一下其中定义的主要方法:

- `String getName()`: 返回上传文件在多部分表单中的参数名称。

- `String getOriginalFilename()`: 返回客户端文件系统中上传文件的原始文件名。

- `String getContentType()`: 返回上传文件的内容类型。

- `boolean isEmpty()`: 返回上传文件是否为空。

- `long getSize()`: 返回上传文件的大小(字节数)。

- `byte[] getBytes() throws IOException`: 返回上传文件的内容字节数组。

- `InputStream getInputStream() throws IOException`: 返回一个输入流,用于读取上传文件的内容。

- `void transferTo(File dest) throws IOException, IllegalStateException`: 将上传文件传输到指定的目标文件中。

- `Resource getResource()`: 将上传文件转换为Spring的 `Resource` 对象,用于进一步处理文件内容。

`MultipartFile` 接口提供了一系列方法,用于获取上传文件的相关信息和内容,并且提供了将上传文件传输到目标文件的方法,以及获取文件内容的方法。这些方法使得在Spring应用程序中处理文件上传变得更加方便和灵活。

2.实现将课程信息缓存至redis

见后续

3.实现保存课程索引信息

见后续

  • 10
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值