Skip to content

Instantly share code, notes, and snippets.

@arganzheng
Created April 25, 2014 11:56
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save arganzheng/11287077 to your computer and use it in GitHub Desktop.
Save arganzheng/11287077 to your computer and use it in GitHub Desktop.

优雅的Builder模式

package com.baidu.global.mobile.hao123.nantianmen.annotation;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * <pre>
 * 设置HTTP Response Header中关于Cache的控制头——Cache-Control和Expires(For HTTP 1.0)。
 * 
 * Cache-Control 
 * 	public = true | false 
 * 	max-age = delta-seconds (0 ==> no-cache | no-store )
 *  s-maxage = delta-seconds. 
 *  must-revalidate = true | false 
 *  no-transform = true | false
 *  Expires = 日期字符串,格式为yyyy-MM-dd HH:mm:ss。并且自动推算匹配的max-age。
 * 
 * 说明:使用annotation意味着只能支持静态配置。而不是动态的信息。所以如果value值需要动态设置的话就需要程序自己在handler method中处理了。
 * 比如 Last-Modified和Etag。max-age和Expires有可能也需要动态,这种情况下就不能使用这个annotation。
 * 可以考虑使用JAX-RS框架[HTTP Header Caching in JAX-RS](https://devcenter.heroku.com/articles/jax-rs-http-caching)。
 * </pre>
 * 
 * @author zhengzhibin
 * 
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface CacheControl {

	/**
	 * public和private都是关键字。。
	 * 
	 */
	boolean isPublic() default true;

	/**
	 * max-age = delta-seconds (0 ==> no-cache | no-store )。
	 * 
	 */
	int maxAge() default -1;

	/**
	 * s-maxage = delta-seconds.
	 */
	int sMaxAge() default -1;

	/**
	 * must-revalidate = true | false
	 */
	boolean mustRevalidate() default false;

	/**
	 * proxy-revalidate = true | false
	 */
	boolean proxyRevalidate() default false;

	/**
	 * no-transform = true | false
	 */
	boolean noTransform() default true;

	/**
	 * <pre>
	 * Expires = 日期字符串,并且自动推算匹配的max-age。
	 * HTTP标准是RFC 1123 date format, e.g., Thu, 01 Dec 1994 16:00:00 GMT.
	 * public static final String PATTERN_RFC1123 = "EEE, dd MMM yyyy HH:mm:ss zzz";
	 * 但是这个格式对于我们中国人非常不友好,还是改成这个格式方便些:yyyy-MM-dd HH:mm:ss。
	 * 
	 * </pre>
	 */
	String expires() default "";
}


package me.arganzheng.study.builder.common;

import org.apache.commons.lang.StringUtils;

/**
 * <pre>
 * Cache-Control Header
 *  
 * NOTE: 这个不会顺便增加Expires头。
 * </pre>
 * 
 * @author zhengzhibin
 * 
 */
public class CacheControlHeader {
	public enum CacheType {
		PUBLIC, PRIVATE;
	}

	/**
	 * public | private
	 * 
	 */
	CacheType cacheType = null;
	/**
	 * max-age = delta-seconds (0 ==> no-cache | no-store )。
	 * 
	 */
	boolean noCache;
	boolean noStore;

	int maxAge = -1;

	/**
	 * s-maxage = delta-seconds.
	 */
	int sMaxAge = -1;

	/**
	 * must-revalidate = true | false
	 */
	boolean mustRevalidate = false;

	/**
	 * proxy-revalidate = true | false
	 */
	boolean proxyRevalidate = false;

	/**
	 * no-transform = true | false
	 */
	boolean noTransform = false;

	private CacheControlHeader() {
	}

	public static Builder newBuilder() {
		return Builder.create();
	}

	public CacheType getCacheType() {
		return cacheType;
	}

	public boolean isNoCache() {
		return noCache;
	}

	public boolean isNoStore() {
		return noStore;
	}

	public int getMaxAge() {
		return maxAge;
	}

	public int getsMaxAge() {
		return sMaxAge;
	}

	public boolean isMustRevalidate() {
		return mustRevalidate;
	}

	public boolean isProxyRevalidate() {
		return proxyRevalidate;
	}

	public boolean isNoTransform() {
		return noTransform;
	}

	public String stringValue() {
		StringBuilder sb = new StringBuilder();

		if (cacheType == CacheType.PUBLIC) {
			append(sb, "public");
		} else if (cacheType == CacheType.PRIVATE) {
			append(sb, "private");
		}

		if (mustRevalidate) {
			append(sb, "must-revalidate");
		}

		if (noTransform) {
			append(sb, "no-transform");
		}

		if (proxyRevalidate) {
			append(sb, "proxy-revalidate");
		}

		if (maxAge == 0) {
			append(sb, "max-age=0");
			noCache = noStore = true;
		} else if (maxAge > 0) {
			append(sb, "max-age=" + maxAge);
		}

		if (sMaxAge == 0) {
			append(sb, "s-maxage=0");
			noCache = noStore = true;
		} else if (sMaxAge > 0) {
			append(sb, "s-maxage=" + sMaxAge);
		}

		if (noCache || noStore) {
			append(sb, "no-cache,no-store");
		}

		return sb.toString();
	}

	private StringBuilder append(StringBuilder sb, String str) {
		if (sb == null) {
			sb = new StringBuilder();
			sb.append(str);
		} else {
			String s = StringUtils.trimToEmpty(sb.toString());
			if (StringUtils.isEmpty(s) || s.charAt(s.length() - 1) == ',') {
				sb.append(str);
			} else {
				sb.append(',').append(str);
			}
		}

		return sb;
	}


package com.baidu.global.mobile.hao123.nantianmen.interceptor;

import java.text.ParseException;
import java.util.Date;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.commons.lang.StringUtils;
import org.apache.commons.lang.time.DateUtils;
import org.apache.commons.lang.time.FastDateFormat;
import org.apache.log4j.Logger;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;
import org.springframework.web.servlet.resource.ResourceHttpRequestHandler;

import com.baidu.global.mobile.hao123.nantianmen.annotation.CacheControl;
import com.baidu.global.mobile.hao123.nantianmen.common.CacheControlHeader;
import com.baidu.global.mobile.hao123.nantianmen.common.CacheControlHeader.CacheType;

/**
 * 处理@CacheControl注解
 * 
 * @author zhengzhibin
 * 
 */
public class CacheControlInterceptor extends HandlerInterceptorAdapter {
	public static final String EXPIRES_PATTERN = "yyyy-MM-dd HH:mm:ss";
	public static final String PATTERN_RFC1123 = "EEE, dd MMM yyyy HH:mm:ss zzz";

	private static final Logger logger = Logger.getLogger(CacheControlInterceptor.class);

	/**
	 * 根据@CacheControl注解设置对应的HTTP Response Header
	 */
	public void postHandle(HttpServletRequest request, HttpServletResponse response,
			Object handler, ModelAndView modelAndView) throws Exception {
		if (handler instanceof HandlerMethod) {
			HandlerMethod method = (HandlerMethod) handler;
			CacheControl cacheControlAnnotation = method.getMethodAnnotation(CacheControl.class);
			if (cacheControlAnnotation == null
					|| StringUtils.isNotEmpty(response.getHeader("Cache-Control"))) {
				return;
			}

			if (logger.isDebugEnabled()) {
				logger.debug("Response with CacheControl Set!");
			}

			CacheControlHeader.Builder cc = CacheControlHeader.newBuilder();

			if (cacheControlAnnotation.isPublic()) {
				cc.setCacheType(CacheType.PUBLIC);
			} else {
				cc.setCacheType(CacheType.PRIVATE);
			}

			Date expiresDate = null;
			if (cacheControlAnnotation.mustRevalidate()) {
				cc.setMustRevalidate(true);
			}

			if (cacheControlAnnotation.noTransform()) {
				cc.setNoTransform(true);
			}

			if (cacheControlAnnotation.proxyRevalidate()) {
				cc.setProxyRevalidate(true);
			}

			if (cacheControlAnnotation.maxAge() >= 0) {
				cc.setMaxAge(cacheControlAnnotation.maxAge());
			}
			if (cacheControlAnnotation.sMaxAge() >= 0) {
				cc.setsMaxAge(cacheControlAnnotation.sMaxAge());
			}

			if (StringUtils.isNotEmpty(cacheControlAnnotation.expires())) {
				try {
					expiresDate = DateUtils.parseDate(cacheControlAnnotation.expires(),
							new String[] { EXPIRES_PATTERN, PATTERN_RFC1123 });

					Date now = new Date();

					long age = expiresDate.getTime() - now.getTime();
					long maxAge = age / 1000;

					if (cacheControlAnnotation.maxAge() < 0) { // maxAge not set
						cc.setMaxAge(Long.valueOf(maxAge).intValue());
					}
					if (cacheControlAnnotation.sMaxAge() < 0) { // sMaxAge not
																// set
						cc.setsMaxAge(Long.valueOf(maxAge).intValue());
					}
				} catch (ParseException e) {
					logger.error("parse expires date failed! Expired Date = "
							+ cacheControlAnnotation.expires());
				}
			}

			String value = cc.build().stringValue();
			if (StringUtils.isNotBlank(value)) {
				response.addHeader("Cache-Control", value);
			}

			if (expiresDate != null) {
				response.addHeader("Expires",
						FastDateFormat.getInstance(PATTERN_RFC1123).format(expiresDate));
			}
		}
	}
}


	/**
	 * <pre>
	 * Builder for CacheControlHeader. 
	 * 
	 * 这里没有处理字段是否有被显示设置问题。可以考虑每个字段增加一个标志字段,表面是否被显示设值。
	 * </pre>
	 * 
	 * @author zhengzhibin
	 * 
	 */
	public static class Builder {
		private CacheControlHeader cacheControlHeader;

		public static Builder create() {
			return new Builder();
		}

		private Builder() {
			this.cacheControlHeader = new CacheControlHeader();
		}

		public Builder setCacheType(CacheType cacheType) {
			cacheControlHeader.cacheType = cacheType;
			return this;
		}

		public Builder setNoCache(boolean noCache) {
			cacheControlHeader.noCache = noCache;
			return this;
		}

		public Builder setNoStore(boolean noStore) {
			cacheControlHeader.noStore = noStore;
			return this;
		}

		public Builder setMaxAge(int maxAge) {
			cacheControlHeader.maxAge = maxAge;
			return this;
		}

		public Builder setsMaxAge(int sMaxAge) {
			cacheControlHeader.sMaxAge = sMaxAge;
			return this;
		}

		public Builder setMustRevalidate(boolean mustRevalidate) {
			cacheControlHeader.mustRevalidate = mustRevalidate;
			return this;
		}

		public Builder setProxyRevalidate(boolean proxyRevalidate) {
			cacheControlHeader.proxyRevalidate = proxyRevalidate;
			return this;
		}

		public Builder setNoTransform(boolean noTransform) {
			cacheControlHeader.noTransform = noTransform;
			return this;
		}

		public CacheControlHeader build() {
			return cacheControlHeader;
		}
	}
}


package com.baidu.global.mobile.hao123.nantianmen.web;

import java.util.ArrayList;
import java.util.Date;
import java.util.List;

import javax.servlet.http.HttpServletResponse;

import org.apache.commons.lang.StringUtils;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;

import com.baidu.global.mobile.hao123.nantianmen.common.CacheControlHeader;
import com.baidu.global.mobile.hao123.nantianmen.common.CacheControlHeader.CacheType;
import com.baidu.global.mobile.hao123.nantianmen.dto.BabyNews;
import com.baidu.global.mobile.hao123.nantianmen.dto.BabyRanking;

/**
 * 
 * @author zhengzhibin
 * 
 */
@Controller
@RequestMapping("/football")
public class FootballBabyRankingController {

	@RequestMapping(value = "/baby/rank", method = RequestMethod.GET)
	// @CacheControl(isPublic = true, maxAge = 300, sMaxAge = 300)
	public String footballBabyRanking(Model model, HttpServletResponse response) {
		List<BabyRanking> ranks = new ArrayList<BabyRanking>();

		// FIXME 这是测试数据,向君仔要数据
		for (int i = 1; i <= 20; i++) {
			BabyRanking rank = new BabyRanking();

			rank.setBust(90);
			rank.setWaist(90);
			rank.setHip(90);
			rank.setImageUrl("/resources/nantianmen/images/" + i + ".jpg");
			rank.setName("fb" + i);
			rank.setVotes(23 + i);
			rank.setOrder(i);

			ranks.add(rank);
		}

		model.addAttribute("ranks", ranks);

		List<BabyRanking> lastWeekRanks = new ArrayList<BabyRanking>();
		for (int i = 1; i <= 6; i++) {
			BabyRanking rank = new BabyRanking();

			rank.setBust(88);
			rank.setWaist(88);
			rank.setHip(88);
			rank.setImageUrl("/resources/nantianmen/images/" + i + ".jpg");
			rank.setName("fb" + i);
			rank.setVotes(32 + i);
			rank.setOrder(i);

			lastWeekRanks.add(rank);
		}
		model.addAttribute("lastWeekRanks", lastWeekRanks);

		String cacheControlValue = CacheControlHeader.newBuilder().setCacheType(CacheType.PUBLIC)
				.setMaxAge(300).setsMaxAge(600).build().stringValue();
		// String cacheControlValue =
		// CacheControlHeader.newBuilder().setNoCache(true)
		// .setMustRevalidate(true).build().stringValue();
		if (StringUtils.isNotBlank(cacheControlValue)) {
			response.addHeader("Cache-Control", cacheControlValue);
		}

		// TODO 需要前面几期的排名
		return "football-baby";
	}

	@RequestMapping(value = "/baby/news", method = RequestMethod.GET)
	public String footballBabyNews(Model model) {
		List<BabyNews> news = new ArrayList<BabyNews>();
		for (int i = 1; i <= 5; i++) {
			BabyNews oneNews = new BabyNews();

			oneNews.setContent("");
			oneNews.setDate(new Date());
			oneNews.setImageUrl("/resources/nantianmen/images/apple-touch-icon-57x57-precomposed.png");
			oneNews.setSource("Yahoo");
			oneNews.setTitle("Title");
			oneNews.setUrl("www.baidu.com");

			news.add(oneNews);
		}
		model.addAttribute("news", news);

		return "football-news";
	}
}

参考文章

  1. Another builder pattern for Java
  2. make-it-easy
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment