说明

利用Freemarker 与 SpringBoot 实现页面静态化,并在过滤器中,判断是否存在静态页、如果静态页面已经存在。直接使用静态页面、若不存在静态页使用动态页面渲染 使用@StaticPage 实现静态页,使用@ClearStaticPage 在页面数据更新时清理静态页面

1. 引入相关依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-freemarker</artifactId>
</dependency>

2. 基本的配置项

spring:
 freemarker:
    # 关闭模板缓存,方便测试
    cache: false  
    settings:
      #检查模板更新延迟时间,设置为0表示立即检查,如果时间大于0会有缓存不方便进行模板测试
      template_update_delay: 0  
    charset: UTF-8
    content-type: text/html; charset=utf-8
    suffix: .ftl
    template-loader-path: classpath:/templates
    check-template-location: true
    expose-request-attributes: true
    expose-session-attributes: true
    request-context-attribute: request

3. 自定义配置项

# 页面静态化配置
static-page:
  enabled: true
  localPath: D:/home/static

4. 自定义annotation

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface StaticPage {
}
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface ClearStaticPage {
    public String value() default "";
}

5. 基于切面实现静态页面的切换与清理

/**
 * 静态页处理
 */
@Aspect
@Component
public class StaticPageAspect
{
    private static final Logger log = LoggerFactory.getLogger(StaticPageAspect.class);

    @Value("${static-page.enabled:false}")
    private boolean enabled;

    @Value("${static-page.localPath:#{null}}")
    private String localPath;

    @Value("${spring.freemarker.suffix:#{null}}")
    private String templateSuffix;

    // 配置织入点
    @Pointcut("@annotation(net.tonggeng.api.annotation.StaticPage)")
    public void logPointCut() { }

    /**
     * 处理完请求后执行
     *
     * @param joinPoint 切点
     */
    @AfterReturning(pointcut = "logPointCut()", returning = "jsonResult")
    public void doAfterReturning(JoinPoint joinPoint, Object jsonResult)
    {
        handleLog(joinPoint, null, jsonResult);
    }

    /**
     * 拦截异常操作
     * 
     * @param joinPoint 切点
     * @param e 异常
     */
    @AfterThrowing(value = "logPointCut()", throwing = "e")
    public void doAfterThrowing(JoinPoint joinPoint, Exception e)
    {
        handleLog(joinPoint, e, null);
    }

    protected void handleLog(final JoinPoint joinPoint, final Exception e, Object jsonResult) {
        try {
            // 获得注解
            StaticPage annotation = getAnnotation(joinPoint);
            if (annotation == null || !enabled) { return; }
            if(jsonResult instanceof ModelAndView){
                ModelAndView modelAndView = (ModelAndView) jsonResult;
                HttpServletRequest request = Objects.requireNonNull(getHttpServletRequest());
                String requestUrl = request.getRequestURI();
                String save = localPath + "/" + Context.getLocaleStr(getHttpServletRequest()) +requestUrl;
                String templateFile = modelAndView.getViewName()+templateSuffix;
                // 调用异步任务生成静态文件
                // 这里是使用异步任务生产的静态页面、可直接调用generatingStaticFiles方法
                AsyncManager.me().execute(generatingStaticFiles(save, modelAndView.getModel(), templateFile,
                        Context.getRequestLocale(request)));
            }
        }
        catch (Exception exp) {
            // 记录本地异常日志
            log.error("==前置通知异常==");
            log.error("异常信息:{}", exp.getMessage());
            exp.printStackTrace();
        }
    }

    /**
     * 是否存在注解,如果存在就获取
     */
    private StaticPage getAnnotation(JoinPoint joinPoint) throws Exception {
        Signature signature = joinPoint.getSignature();
        MethodSignature methodSignature = (MethodSignature) signature;
        Method method = methodSignature.getMethod();
        if (method != null) {
            return method.getAnnotation(StaticPage.class);
        }
        return null;
    }

    /**
     * 生成静态文件
     * @return 任务task
     */
    public static TimerTask generatingStaticFiles(String save, Map<String, Object> datas, String templateName, Locale locale) {
        return new TimerTask() {
            @Override
            public void run() {
                //设置异步任务的本地化变量
                LocaleContextHolder.setLocale(locale);
                //生成静态页面
                FreemarkerUtils.geneFileStr(save, datas, templateName);
            }
        };
    }

    private HttpServletRequest getHttpServletRequest() {
        try {
            // 这种方式获取的HttpServletRequest是线程安全的
            return ((ServletRequestAttributes) (RequestContextHolder.currentRequestAttributes())).getRequest();
        } catch (Exception e) {
            return null;
        }
    }
}
/**
 * 清理静态页面
 */
@Aspect
@Component
public class ClearStaticPageAspect
{
    private static final Logger log = LoggerFactory.getLogger(ClearStaticPageAspect.class);

    @Value("${static-page.enabled:false}")
    private boolean enabled;

    @Value("${static-page.localPath:#{null}}")
    private String localPath;

    @Autowired
    private RemoteStaticPageService remoteStaticPageService;

    // 配置织入点
    @Pointcut("@annotation(net.tonggeng.api.annotation.ClearStaticPage)")
    public void logPointCut() { }

    /**
     * 处理完请求后执行
     *
     * @param joinPoint 切点
     */
    @AfterReturning(pointcut = "logPointCut()", returning = "jsonResult")
    public void doAfterReturning(JoinPoint joinPoint, Object jsonResult) {
        handleLog(joinPoint, null, jsonResult);
    }

    /**
     * 拦截异常操作
     * 
     * @param joinPoint 切点
     * @param e 异常
     */
    @AfterThrowing(value = "logPointCut()", throwing = "e")
    public void doAfterThrowing(JoinPoint joinPoint, Exception e) {
        handleLog(joinPoint, e, null);
    }

    protected void handleLog(final JoinPoint joinPoint, final Exception e, Object jsonResult) {
        try {
            // 获得注解
            ClearStaticPage annotation = getAnnotation(joinPoint);
            if (annotation == null || StrUtil.isEmpty(annotation.value())) { return; }
            // 这里是使用异步任务生产的静态页面、可直接调用clearStaticFile方法
            AsyncManager.me().execute(clearStaticFile(remoteStaticPageService, joinPoint, annotation.value()));
        }
        catch (Exception exp) {
            // 记录本地异常日志
            log.error("==前置通知异常==");
            log.error("异常信息:{}", exp.getMessage());
            exp.printStackTrace();
        }
    }

    /**
     * 清理静态文件
     * @return 任务task
     */
    public static TimerTask clearStaticFile(RemoteStaticPageService remoteStaticPageService,JoinPoint joinPoint , String path) {
        //设置request对象线程共享
        HttpServletRequest req = getHttpServletRequest();
        return new TimerTask() {
            @Override
            public void run(){
                AsyncManager.setAsyncLocalRequest(req);
                AjaxResult result = AjaxResult.error();
                if ("*".equals(path)) {
                    result = remoteStaticPageService.clearStaticPage();
                } else if(path.contains("{")) {
                    String tempPath = path;
                    HttpServletRequest request = AsyncManager.getAsyncLocalRequest();
                    List<Annotation> annotations = getMethodAnnotations(joinPoint);
                    if(annotations != null && annotations.size() > 0){
                        for(Annotation ann: annotations){
                            PathVariable pathVariable = (PathVariable) ann;
                            assert request != null;
                            tempPath = tempPath.replace("{"+pathVariable.value()+"}",
                                    request.getParameter(pathVariable.value()));
                        }
                    }else{
                        assert request != null;
                        try (InputStreamReader reader = new InputStreamReader(request.getInputStream(), StandardCharsets.UTF_8)) {
                            char[] buff = new char[1024];
                            int length = 0;
                            String x = null;
                            while ((length = reader.read(buff)) != -1) {
                                x = new String(buff, 0, length);
                            }
                            // 获取参数值
                            JSONObject json = JSONUtil.parseObj(x);
                            String fieldName = tempPath.substring(tempPath.indexOf("{") + 1, tempPath.indexOf("}"));
                            tempPath = tempPath.replace("{" + fieldName + "}",
                                    json.getStr(fieldName));
                        } catch (Exception e) {
                            e.printStackTrace();
                        }
                    }
                    result = remoteStaticPageService.delStaticPage(tempPath);
                } else {
                    result = remoteStaticPageService.delStaticPage(path);
                }
                log.info("清理["+path+"]缓存:"+ result.getMsg());
            }
        };
    }

    /**
     * 是否存在注解,如果存在就获取
     */
    private ClearStaticPage getAnnotation(JoinPoint joinPoint) throws Exception {
        Signature signature = joinPoint.getSignature();
        MethodSignature methodSignature = (MethodSignature) signature;
        Method method = methodSignature.getMethod();
        if (method != null) {
            return method.getAnnotation(ClearStaticPage.class);
        }
        return null;
    }

    private static List<Annotation> getMethodAnnotations(JoinPoint joinPoint){
        Signature signature = joinPoint.getSignature();
        MethodSignature methodSignature = (MethodSignature) signature;
        Method method = methodSignature.getMethod();
        if (method != null) {
            Annotation[][] annotations = method.getParameterAnnotations();
            List<Annotation> annList = new ArrayList<>();
            for(Annotation[] annArr: annotations){
                for(Annotation ann: annArr){
                    if(ann instanceof PathVariable){
                        annList.add(ann);
                    }
                }
            }
            return annList;
        }
        return null;
    }

    private static HttpServletRequest getHttpServletRequest() {
        try {
            // 这种方式获取的HttpServletRequest是线程安全的
            return ((ServletRequestAttributes) (RequestContextHolder.currentRequestAttributes())).getRequest();
        } catch (Exception e) {
            return null;
        }
    }
}

生产静态页的方法

public static void geneFileStr(String save, Map<String, Object> datas, String templateName){
    Configuration cfg = SpringUtils.getBean(Configuration.class);
    Writer file = null;
    try {
        //创建路径
        FileUtil.mkdir(new File(save).getParentFile());
        //加载具体模板文件,获得模板对象
        Template template = cfg.getTemplate(templateName);
        //编码格式
        cfg.setDefaultEncoding("utf-8");
        file = new FileWriter(new File(save));
        //生成文件
        template.process(datas, file);
    } catch (Exception e) {
        e.printStackTrace();
    }finally {
        if (file != null) {
            try{
                file.close();
            }catch (Exception ignored){ }
        }
    }
}

6. 把localPath加入SpringBoot 静态路径

/**
 * 静态资源配置
 */
@Configuration
public class StaticPageConfig implements WebMvcConfigurer {

    @Value("${static-page.enabled:false}")
    private boolean enabled;

    @Value("${static-page.localPath:#{null}}")
    private String localPath;

    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        if(enabled) {
            /** 本地生成的静态资源路径 */
            registry.addResourceHandler( "/static-page/**").addResourceLocations("file:" + localPath + "/");
        }
    }
}

7. 配置拦截器动态判断静态页面是否存在

@Component
public class StaticPageFilter extends OncePerRequestFilter {

    @Value("${static-page.enabled:false}")
    private boolean enabled;

    @Value("${static-page.localPath:#{null}}")
    private String localPath;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
            throws ServletException, IOException {
        if(enabled){
            String requestUri = request.getRequestURI().equals("/")
                    ? "/index.html":request.getRequestURI();
            String requestLocale =  Context.getLocaleStr(request);
            String localFile = localPath + "/" + requestLocale + requestUri;
            if(FileUtil.exist(localFile)){ //如果本地静态资源存在则跳转本地静态资源
                request.getRequestDispatcher("/static-page"+ "/" + requestLocale + requestUri)
                        .forward(request,response);
                return;
            }
        }
        chain.doFilter(request, response);
    }
}

8. 具体的使用方法

生成静态页面

@StaticPage
@RequestMapping("/news.html")
public ModelAndView index(){
    return new ModelAndView("notice/news")
        .addObject("list",remoteNewsService.page(new News()));
}

清理静态页面

@DeleteMapping("{id}")
// 指定清理的页面路径
@ClearStaticPage("/notice/news.html") 
public AjaxResult delete(@PathVariable("id") String ids){
    return toAjax(newsService.removeByIds(Arrays.asList(ids.split(","))));
}

@PutMapping()
// 带有参数的清理路径 其中newsId为News属性
@ClearStaticPage("/notice/news/{newsId}.html") 
public AjaxResult update(@RequestBody News news){
    return toAjax(newsService.updateById(news));
}