1.静态页面模块枚举

public enum ModuleEnum
{
    index, // 首页模块
    locale, // 现场课程模块
    video, // 视频模块
    announcement, // 资讯模块
    bwHelp, // 软件助手
    ;
}

2.新建页面处理器

import java.io.File;
import java.io.FileNotFoundException;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.util.ResourceUtils;

import cn.hutool.core.io.FileUtil;
import cn.hutool.core.lang.Assert;

/**
 * 页面处理器
 */
public abstract class PageProcess implements InitializingBean
{
    private static final Logger log = LoggerFactory.getLogger(PageProcess.class);

    /**
     * 页面处理集合
     *  -key moduleName
     */
    private static final Map<String, PageProcess> PROCESSES = new HashMap<>();

    /**
     * 指向static目录
     */
    protected static File staticFile;
    static
    {
        try
        {
            staticFile = FileUtil.file(ResourceUtils.getURL("classpath:").getPath(), "static");
        } catch (FileNotFoundException e)
        {
            log.error("指向static目录失败", e);
        }
    }

    /**
     * 初始化完毕之后注册到页面处理容器中
     * @throws Exception
     */
    @Override
    public void afterPropertiesSet() throws Exception
    {
        ModuleEnum module = this.getModule();
        if (module != null)
        {
            PageProcess.PROCESSES.put(module.toString(), this);
        }
    }

    /**
     * 设置静态页面的基础数据
     */
    protected Map<String, Object> baseData()
    {
        Map<String, Object> data = new HashMap<>();

        return data;
    }

    /*-------------对外方法---------------*/
    
    /**
     * 刷新所有静态页面
     */
    public static void refreshAll()
    {
        refresh(null);
    }
    
    /**
     * 刷新页面
     *  -模块为空刷新所有页面
     *  -ids为空刷新某个模块所有页面
     * @param id
     */
    public static void refresh(ModuleEnum module, Integer... ids)
    {
        if (module == null)
        {
            File htmlFile = FileUtil.file(staticFile, "html");
            FileUtil.clean(htmlFile);
        } else
        {
            String moduleName = module.toString();
            if (ids == null || ids.length == 0)
            {
                // 删除整个模块页面
                File moduleDir = FileUtil.file(staticFile, String.format("/html/%s", moduleName));
                FileUtil.clean(moduleDir);
            } else
            {
                // 精确删除某个页面
                for (Integer id : ids)
                {
                    File pageFile = FileUtil.file(staticFile, String.format("/html/%s/%s.html", moduleName, id));
                    FileUtil.del(pageFile);
                }
            }
        }
    }

    /**
     * 访问页面
     * @param module 模块
     * @param pageName 页面名称  例如1.html
     * @throws Exception 
     */
    public static String view(String moduleName, String pageName) throws Exception
    {
        // 参数校验
        Assert.notBlank(moduleName);
        Assert.notBlank(pageName);
        Assert.isTrue(staticFile != null && staticFile.exists(), "static目录不存在");
        // xhtml处理成html
        pageName = pageName.replace("xhtml", "html");
        // 获取对应页面处理器
        PageProcess pageProcess = PROCESSES.get(moduleName);
        Assert.notNull(pageProcess, "没有找到对应模块的页面处理器");
        // 对应静态页面url
        String pageUrl = "/html/" + moduleName + "/" + pageName;
        File pageFile = FileUtil.file(staticFile, pageUrl);
        if (!pageFile.exists())
        {
            // 生成静态化页面
            String pageHtml = pageProcess.createPage(pageName);
            FileUtil.writeString(pageHtml, pageFile, StandardCharsets.UTF_8);
        }
        // 为了处理资讯访问量类似问题增加访问钩子
        pageProcess.viewHooks(pageName);

        return "forward:" + pageUrl;
    }

    /*-------------子类实现---------------*/

    /**
     * 要处理的页面所属模块
     * @return
     */
    protected abstract ModuleEnum getModule();

    /**
     * 生成html页面
     * @param pageName
     * @return
     */
    protected abstract String createPage(String pageName) throws Exception;

    /**
     * 访问钩子,默认不做任何事,可以用来追加访问量
     * @param pageName
     */
    protected void viewHooks(String pageName)
    {

    }
}

3.新建子类继承页面处理器1

@Component
public class IndexPageProcess extends PageProcess
{
    @Autowired
    private CourseService courseService;

    @Override
    public ModuleEnum getModule()
    {
        return ModuleEnum.index;
    }

    @Override
    protected String createPage(String pageName) throws Exception
    {
        // 构建页面数据
        Map<String, Object> pageData = new HashedMap<>();
        this.courseService.indexDetail(pageData);
        // 模板加数据渲染成html页面
        String templateName = this.getModule().toString() + "/" + pageName;
        String html = FreemarkerUtil.parseTpl(templateName, pageData);
        return html;
    }
}

4.新建子类继承页面处理器2

@Component
public class VideoPageProcess extends PageProcess
{
    @Autowired
    private CourseVideoService courseVideoService;

    @Override
    public ModuleEnum getModule()
    {
        return ModuleEnum.video;
    }

    @Override
    protected String createPage(String pageName) throws Exception
    {
        // 课程id
        Integer courseId = Integer.parseInt(StringUtils.substringBeforeLast(pageName, "."));
        // 构建页面数据
        Map<String, Object> params = this.baseData();
        CourseVideoPortalInfoVO courseInfo = this.courseVideoService.getCourseVideoPortalInfo(courseId);
        params.put("courseInfo", courseInfo);

        // 模板加数据渲染成html页面
        String templateName = this.getModule().toString() + "/videodetail.html";
        String html = FreemarkerUtil.parseTpl(templateName, params);
        return html;
    }
}

5.新建自定义静态化页面注解

/**
 * 标注在控器器方法上,默认刷新首页模块,也可以指定刷新模块,刷新指定页面默认取方法的第一个参数的id属性
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RefreshPage
{
    /**
     * 刷新的模块
     * @return
     */
    ModuleEnum module() default ModuleEnum.index;
    
    /**
     * 是否刷新首页
     * @return
     */
    boolean refreshIndex() default true;
    
    /**
     * 标识所在参数下标
     * @return
     */
    int argIndex() default 0;
    
    /**
     * 标识取值表达式
     * @return
     */
    String idExpress() default "id";
}

6. 切面

@Aspect
@Component
@Slf4j
public class RefreshPageAspect
{
    @Pointcut(value = "@annotation(org.springframework.web.bind.annotation.RequestMapping)")
    private void pointcut()
    {
        throw new UnsupportedOperationException();
    }

    @Around(value = "@annotation(refreshPage)", argNames = "refreshPage")
    public Object refreshPage(ProceedingJoinPoint pjp, RefreshPage refreshPage) throws Throwable
    {
        // 执行方法
        Object result = pjp.proceed();
        // 刷新页面
        ModuleEnum module = refreshPage.module();
        try
        {
            if (module.equals(ModuleEnum.index))
            {
                PageProcess.refresh(ModuleEnum.index);
                log.info("刷新静态首页成功");
            } else
            {
                // 获取页面标识
                int argIndex = refreshPage.argIndex();
                Object arg = pjp.getArgs()[argIndex];
                String idExpress = StrUtil.emptyIfNull(refreshPage.idExpress());
                BeanPath resolver = new BeanPath(idExpress);
                Integer id = Integer.parseInt(resolver.get(arg).toString());
                // 清理页面
                PageProcess.refresh(module, id);
                // 根据标识判断是否级联刷新首页
                if (refreshPage.refreshIndex())
                {
                    PageProcess.refresh(ModuleEnum.index);
                    log.info("刷新静态首页成功");
                }
                log.info("刷新模块[{}]的静态页面成功,页面标识[{}]", module.toString(), id);
            }
        } catch (Exception e)
        {
            log.error("刷新模块[{}]的静态页面异常:", module.toString(), e);
        }
        return result;
    }
}

7.FreemarkerUtil

@Slf4j
public class FreemarkerUtil
{
    private static final Configuration CONFIG = SpringContextUtil.getBean(Configuration.class);

    /**
     * 根据模板和数据生成html页面
     * @param templateUrl 模板路径
     * @param params 页面参数
     * @return
     * @throws IOException 
     * @throws ParseException 
     * @throws MalformedTemplateNameException 
     * @throws TemplateNotFoundException 
     */
    public static String parseTpl(String templateUrl, Object params) throws Exception
    {
        log.info("渲染模板[{}],数据[{}]", templateUrl, JSON.toJSONString(params));
        Template template = CONFIG.getTemplate(templateUrl);
        return FreeMarkerTemplateUtils.processTemplateIntoString(template, params);
    }
}

8.CourseBannerDO

public class CourseBannerDO extends BaseDO<CourseBannerDO>
{
	private static final long serialVersionUID = 1L;

	/**
	 * id主键
	 */
	@TableId(type = IdType.AUTO)
	@ApiModelProperty("id主键")
	private Integer id;
	/**
	 * 图片名称
	 */
	@ApiModelProperty("图片名称")
	private String name;
	/**
	 * 图片路径
	 */
	@ApiModelProperty("图片路径")
	private String imgUrl;
	/**
	 * 跳转目标地址
	 */
	@ApiModelProperty("跳转目标地址")
	private String link;
}

9.CourseVideoForm

@Data
@ToString
@ApiModel("视频课程添加表单模型")
public class CourseVideoForm implements Serializable
{

	private static final long serialVersionUID = -9024302026705244796L;

	@ApiModelProperty("视频课程id")
	private Integer id;

	@ApiModelProperty("课程id")
	private Integer courseId;

	@ApiModelProperty("课程名称")
	@NotBlank
	@Size(min = 1, max = 100, message = "最小长度不低于1位,最大长度不超过100位")
	private String courseName;
}

10.添加注解刷新静态化页面

@PostMapping("/save")
	@ApiOperation(value = "新建")
	@RefreshPage
	public void save(@RequestBody CourseBannerDO courseBanner)
	{
		log.info("{}新建宣传图courseBanner={}:", getUid(), courseBanner.toString());
		courseBannerService.save(courseBanner);
	}

	@PostMapping("/update")
	@ApiOperation(value = "更新课程")
	@RefreshPage(module = ModuleEnum.video, argIndex = 0, refreshIndex = true, idExpress = "courseId")
	public R update(@Valid @RequestBody CourseVideoForm courseVideoForm)
	{
		log.info("更新课程courseId:" + courseVideoForm.getCourseId());
		courseVideoService.update(courseVideoForm);
		return R.success();
	}
  • @RefreshPage
    后面不跟参数,默认刷新某个模块所有页面
  • @RefreshPage(module = ModuleEnum.index, argIndex = 0, refreshIndex = true, idExpress = “courseId”)
    refreshIndex :代表是否刷新首页
    idExpress :代表即将更新的静态化页面的名称

11.路由跳转到静态页面

@Controller
@RequestMapping("/ke")
@Api(tags = "静态页面控制器")
public class StaticController
{
    private static final Logger log = LoggerFactory.getLogger(StaticController.class);

    @GetMapping("/{moduleName}/{pageName}")
    @ApiOperation(value = "跳转到对应静态页面")
    public String toStaticPage(@PathVariable String moduleName, @PathVariable String pageName)
    {
        try
        {
            return PageProcess.view(moduleName, pageName);
        } catch (Exception e)
        {
            log.error("跳转到静态页面[{}/{}]异常:", moduleName, pageName, e);
            // 对于静态页面出现业务错误跳转到404页面
            return CommonConstant.REDIRECT_PREFIX + CommonConstant.URL_404;
        }
    }

    @ResponseBody
    @GetMapping("/refresh.html")
    @ApiOperation(value = "刷新所有页面(供调试)")
    public R refresh()
    {
        PageProcess.refreshAll();
        return R.success();
    }
}