先来描述一下vt(viewTag)问题的来源:
- 初始需求:
在开发的过程中有这样的一个处理时间显示的需求:
如果时间小于1分钟显示秒,
如果时间小于1小时显示分钟,
如果时间小于1天显示小时,
如果大于1天显示日期。 - 需求演化一:
每一个显示时间的地方后台开发人员都要编写一个处理这样的一个程序。
于是想起在后台创建工具类解决这个问题。 - 需求演化二:
但是使用工具类又带来了另外一个问题,每次都要取出这个字段显示调用工具类
然后再塞进去,这样造成代码重复臃肿。 - 需求演化三:
后台开发不想用代码实现就可以替换成功,换句话说显示和业务不要耦合在一起。 - 需求演化四:
如果整站出现类似于时间这种字符串处理的地方很多该如何处理?? - 最终需求:
到这里最终需求就确定啦,相同概念的字符串在页面展示时不希望展示的逻辑每个人都关注,有框架统一提供实现该如何实现???
分析问题:
我们需要确定在哪里进行字符串替换比较合适?
无论是json还是ftl还是jsp的字符串替换我们都必须在没有真正返回给浏览器之前来做字符串替换的工作。
大致思路:
发送一个http请求经过解析域名找到对应的tomcat服务,由tomcat决定调用哪一个应用程序响应,
然后应用程序找到对应的jsp或者html,我们通过某种方式获取到jsp或者html的内容通过规则匹配
就可以进行字符串替换。
其实,最简单的方式我们可以创建一个filter,在url请求过来的时候在doFilter()方法中根据某种url规则(比如以.jsp结尾的请求)
获取请求的文件(jsp,ftl,html), 然后读取文件内容进行匹配,最终可以完成替换。
我们要确定哪些内容需要被替换也就是说我们需要一个标识?
定义一个规则凡是符合规则的就进行字符串替换工作,
可以定义一个特殊的标签:比如:
那我们就规定在jsp或者ftl中以<vt"开头的标签都进行字符串替换。
那么多种类的vt我们怎么确定使用哪一个vt处理类进行处理呢??
其中中的xxx就是这个处理的类的ioc容器的key,
我们可以通过为每一个处理的类添加一个标识,通过正则匹配找到这个vt处理类
然后调用这个处理器的处理方法完成字符串替换。
如何让使用者用的简单??
我们可以抽取一个vt处理器接口,提供一个针对这个接口的抽象实现类,
让其处理相同逻辑的实现并约定好必需要实现的方法,
使用者可以通过实现这个抽象类进行字符串替换逻辑编写。
替换相同意义的字符串,那我们怎么确定哪些是相同意义的呢??
这个由使用者去做,去抽象。
如何在框架上进行集成??
在框架上使用这个的时候需找两个点:
1.框架在视图层有没有提供可以让我们对url访问的资源(文件)进行处理的入口?
2.框架有没有可以让我们的vt相关处理类放到ioc容器管理的方法??
类图设计
类图讲解:
IViewTag:实现这个接口,即可以根据标签替换成对应的视图显示内容
AbstractCacheViewTag:抽象的缓存视图标签
DateViewTag: vt引擎的日期处理类 处理 <vt:date uid="2013-12-13 12:31:54"/> 转化为 xx秒前,xx分钟前,xx天前
ViewTag: vt核心引擎,负责对外提供服务
具体代码
IViewTag
/**
* 视图标签
* <p>
* 实现这个接口,即可以根据标签替换成对应的视图显示内容
*
* @Date 2016年11月30日
*/
public interface IViewTag {
/**
* 转换
* <p>
* 所有需要转换的字符串进行过滤之后,将能进行转换的进行转换放入map
*
* @param cmsTags 所有的cms标签
* @param needChange 需要转换的map
*/
void change(final HttpServletRequest req, final Set<String> cmsTags, final Map<String, String> needChange);
/**
* 清除缓存
*
* @param type 数据类别
* @param id 唯一标示
*/
void rmCache(final String type, final String id);
}
AbstractCacheViewTag
/**
* 抽象的缓存视图标签
* <p>
* 增加了自动缓存的功能
*
* @author ZhuanJunxiang
* @Date 2016年11月25日
*/
public abstract class AbstractCacheViewTag extends AbstractViewTag {
/**
* Logger for this class
*/
private static final Logger logger = LoggerFactory.getLogger(AbstractCacheViewTag.class);
/**
* 原始的字符串
*/
private static final String ORIGIN = "origin";
/**
* 原始的字符串,()
*/
public static final String AFTER_CMS = "afterCms";
/**
* 是否开启
*/
@Value("#{configProperties['vtCache']}")
private boolean vtCache;
@Autowired
protected IRedisDao redisDao;
@Override
protected void changeMatch(final HttpServletRequest req, final Set<String> needDeal,
final Map<String, String> result) {
//获取每个cms参数的列表
List<Map<String, String>> props = getPropMap(needDeal);
//先从redis中获取
if (vtCache) {
loadFromRedis(props, result);
}
//剩余的从db中获取后,进行构造,并塞入缓存
loadFormDb(props, result);
//如果还有未处理的暂时不处理
}
protected List<Map<String, String>> getPropMap(final Set<String> needDeal) {
@SuppressWarnings("unchecked")
List<Map<String, String>> result = CollectionUtil.list();
for (String one : needDeal) {
Map<String, String> props = ParseUtil.getProps(one);
props.put(ORIGIN, one);
result.add(props);
}
return result;
}
public void loadFromRedis(final List<Map<String, String>> tagProps, final Map<String, String> result) {
List<String> keys = CollectionUtil.list();
for (Map<String, String> one : tagProps) {
keys.add(getRedisKey(one));
}
String[] collection2array = CollectionUtil.collection2array(keys);
List<String> re = redisDao.mget(collection2array);
if (Util.isEmpty(re)) {
return;
}
for (int i = re.size() - 1; i >= 0; i--) {
String string = re.get(i);
if (string == null) {
continue;
}
Map<String, String> remove = tagProps.remove(i);
result.put(remove.get(ORIGIN), string);
}
}
protected String getRedisKey(final Map<String, String> map) {
List<String> keys = CollectionUtil.list("vt", getType());
if (map.containsKey(UID)) {
keys.add(map.get(UID));
}
if (map.containsKey(SUB_TYPE)) {
keys.add(map.get(SUB_TYPE));
}
return StringUtil.join(":", keys);
}
protected void loadFormDb(final List<Map<String, String>> tagProps, final Map<String, String> result) {
if (Util.isEmpty(tagProps)) {
return;
}
Map<String, String> needCache = MapUtil.map();
for (Map<String, String> one : tagProps) {
String content;
try {
content = loadFormDb(one);
} catch (Exception e) {
logger.error(e.getMessage());
content = "";
}
result.put(one.get(ORIGIN), content);
needCache.put(getRedisKey(one), content);
}
// redis.set(needCache);
//设置过期时间
for (Entry<String, String> entry : needCache.entrySet()) {
redisDao.set(entry.getKey(), entry.getValue());
redisDao.expire(entry.getKey(), getExpireTime());
}
}
/**
* 过期时间
* <p>
* 默认的过期时间
*
* @return 过期时间
*/
protected int getExpireTime() {
return 1000;
}
/**
* 从数据库里面加载数据
* 处理完毕之后,直接放到tagProps的每个map的AFTER_CMS属性里面
*
* @param tagProp 待处理的数据
*/
protected abstract String loadFormDb(final Map<String, String> tagProp);
/**
* 如果 subType为空,则更新某个对象的所有类型的缓存
* <p>
* 如果id为空,则更新subType类型的所有对象的缓存
* <p>
* 如果,两个都为空,则表示更新该类getType()的所有缓存
*/
@Override
public void rmCache(final String subType, final String id) {
List<String> keys = CollectionUtil.list("vt", getType());
if (!Util.isEmpty(id)) {
keys.add(id);
}
if (!Util.isEmpty(subType)) {
keys.add(subType);
}
String key = StringUtil.join(":", keys) + "*";
redisDao.del(key);
}
/**
* 替换
* <p>
* 把标签替换成根据业务处理后的字符串
*
* @param tag 标签对象
* @param result 需要替换的字符串
*/
protected void replace(Map<String, String> tag, String result) {
tag.put(AFTER_CMS, result);
}
protected String getFtlPath(String subType) {
return getType() + "/" + subType + ".ftl";
}
}
ViewTag
/**
* vt核心引擎,负责对外提供服务
*
* @author ZhuangJunxiang
* @Date 2013-12-20
*/
@Data
public class ViewTag {
/**
* Logger for this class
*/
private static final Logger logger = LoggerFactory.getLogger(ViewTag.class);
private static int MAX_PARSE_NUM = 3;
private static String PATTERNSTR = "<vt:.*?/>";
private static Pattern PATTERN = Pattern.compile(PATTERNSTR);
private List<IViewTag> changes;
/**
* 使用模板解析并替换内容
*
* @param str 原字符串
* @param req http请求对象
* @param context ServletContext对象
* @return 待替换的字符传
*/
public String parse(final HttpServletRequest req, final String str) {
return parseTime(req, str, 0);
}
/**
* 防止存在嵌套标签,进行多次处理
* 在不超过最大次数的情况下进行多次处理
* @param req 请求对象
* @param str 待处理的字符串
* @param num 处理次数
* @return 处理后结果
*/
private String parseTime(final HttpServletRequest req, final String str, final int num) {
if (Util.isEmpty(str)) {
return "";
}
if (Util.isEmpty(changes)) {
logger.error("not set cms changes yet!"); //$NON-NLS-1$
return str;
}
Set<String> cmsTags = matchCms(str);
if (Util.isEmpty(cmsTags)) {
return str;
}
Map<String, String> needChange = MapUtil.map();
for (IViewTag cms : changes) {
cms.change(req, cmsTags, needChange);
}
String result = str;
for (Entry<String, String> en : needChange.entrySet()) {
String value = filterAjaxValue(req, en.getValue());
String key = filterAjaxKey(req, en.getKey());
result = StringUtil.replaceAll(result, key, value);
}
if (!Util.isEmpty(cmsTags)) {
logger.error("not changed cms: {0}", Json.toJson(cmsTags)); //$NON-NLS-1$
for (String one : cmsTags) {
result = StringUtil.replaceAll(result, one, "");
}
}
if (num < MAX_PARSE_NUM) {
return parseTime(req, result, num + 1);
}
return result;
}
private String filterAjaxKey(HttpServletRequest req, String inputKey) {
// if (!RequestUtil.isAjax(req)) {
// return inputKey;
//
// }
String key = inputKey;
if (key != null) {
key = key.replaceAll("\\\\", "\\\\\\\\");
}
return key;
}
/**
* 过滤ajax请求
* 如果是ajax,则需要把html代码转成json
* 如果不是,则直接返回即可。
*
* @param req
* @param value
* @return TODO(这里描述每个参数,如果有返回值描述返回值,如果有异常描述异常)
*/
private String filterAjaxValue(final HttpServletRequest req, String value) {
// if (!RequestUtil.isAjax(req)) {
// return value;
//
// }
String json = JsonUtil.toJson(JsonUtil.toJson(value));
return json.substring(3, json.length() - 3);
}
/**
* 匹配cms标签
*
* @param str 待匹配的字符串
* @return 匹配到的字符串
*/
private Set<String> matchCms(final String str) {
Matcher matcher = PATTERN.matcher(str);
Set<String> cmsTags = Lang.set();
while (matcher.find()) {
cmsTags.add(matcher.group(0));
}
return cmsTags;
}
/**
*根据类型清理缓存数据
*
* @param type 类型
* @param id 唯一标示
*/
public void rmCache(final String type, final String id) {
if (Util.isEmpty(changes)) {
return;
}
for (IViewTag cms : changes) {
cms.rmCache(type, id);
}
}
public ViewTag() {
}
}
DateViewTag
/**
* vt引擎的日期处理类
* <p>
* 处理 <vt:date uid="2013-12-13 12:31:54"/>
* 转化为 xx秒前,xx分钟前,xx天前
*
* @author ZhuangJunxiang
* @Date 2013-12-20
*/
@Component
public class DateViewTag extends AbstractViewTag {
/**
* Logger for this class
*/
private static final Logger logger = LoggerFactory.getLogger(DateViewTag.class);
@Override
protected String getType() {
return "date";
}
@Override
protected void changeMatch(final HttpServletRequest req, final Set<String> needDeal,
final Map<String, String> result) {
long now = DateTimeUtil.millis();
for (String s : needDeal) {
Map<String, String> props = ParseUtil.getProps(s);
String uid = props.get("uid");
result.put(s, showDate(uid, now));
}
}
/**
* 显示时间
*
* @param date 要处理的时间
* @param now 现在的时间戳
* @return 显示结果
*/
private String showDate(final String date, final long now) {
try {
DateTime dt = DateTimeUtil.string2DateTime(date, null);
long time = dt.getMillis();
long diff = now - time;
if (diff < 60 * 1000) {
return "刚刚";
}
if (diff < 60 * 60 * 1000) {
return new StringBuilder(diff / (60 * 1000) + "").append("分钟前").toString();
}
if (diff < 24 * 60 * 60 * 1000) {
return new StringBuilder(diff / (60 * 1000 * 60) + "").append("小时前").toString();
}
if (diff < (30L * 24L * 60L * 60L * 1000L)) {
return new StringBuilder(diff / (24L * 60L * 60L * 1000L) + "").append("天前").toString();
}
if (diff < (12L * 31L * 24L * 60L * 60L * 1000L)) {
return new StringBuilder(diff / (30L * 24L * 60L * 60L * 1000L) + "").append("个月前").toString();
}
return DateTimeUtil.format(DateTimeUtil.toDate(dt), "yyyy-MM-dd HH:mm:ss");
} catch (Exception e) {
logger.error("日期格式异常", e); //$NON-NLS-1$
return "";
}
}
}
集成到spring mvc中去
自定义jsp视图
继承InternalResourceView重写renderMergedOutputModel方法
添加对ViewTag类的调用,添加vt处理逻辑。
/**
* jsp视图
*
* @author ZhuangJunxiang(529272571@qq.com)
* @Date 2017年4月10日
*/
public class JspView extends InternalResourceView {
/**
* (non-Javadoc)
* @see
*/
@Override
protected void renderMergedOutputModel(final Map<String, Object> model, final HttpServletRequest request,
final HttpServletResponse response) throws Exception {
// Expose the model object as request attributes.
exposeModelAsRequestAttributes(model, request);
// Expose helpers as request attributes, if any.
exposeHelpers(request);
// Determine the path for the request dispatcher.
String dispatcherPath = prepareForRendering(request, response);
// Obtain a RequestDispatcher for the target resource (typically a JSP).
RequestDispatcher rd = getRequestDispatcher(request, dispatcherPath);
if (rd == null) {
throw new ServletException("Could not get RequestDispatcher for [" + getUrl()
+ "]: Check that the corresponding file exists within your web application archive!");
}
if (logger.isDebugEnabled()) {
logger.debug("Including resource [" + getUrl() + "] in InternalResourceView '" + getBeanName() + "'");
}
ResponseWrapper rw = new ResponseWrapper(response);
rd.include(request, rw);
String result = "";
try {
result = rw.getContent();
} catch (Throwable e) {
logger.error(e.getMessage());
}
//result = getTransfer().transfer(request, response, result);
result = getViewTag().parse(request, result);
PrintWriter writer = response.getWriter();
writer.write(HtmlCompressorUtil.compress(result));
response.flushBuffer();
}
public ViewTag getViewTag() {
ViewTag vt = SpringContextUtil.getBean("viewTag");
return vt;
}
}
配置spring-mvc.xml
<bean id="viewTag" class="com.we.core.vt.ViewTag">
<property name="changes">
<array>
<ref bean="dateViewTag"/>
</array>
</property>
</bean>
<!-- 配置视图解析器 如何把handler 方法返回值解析为实际的物理视图 -->
<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<!-- 添加自定义jsp视图 -->
<property name="viewClass" value="com.we.core.web.view.JspView" />
<property name="prefix" value="/WEB-INF/"></property>
<property name="suffix" value=".jsp"></property>
</bean>
jsp中使用
<vt:date uid="2016-12-13 12:31:54"/>