利用Freemarker+SpringBoot实现页面静态化并实现动静态动态切换
说明
利用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));
}
评论
其他文章