重构Cache实现

This commit is contained in:
jinyu
2016-05-28 11:33:05 +08:00
parent bc93a22563
commit b797580384
116 changed files with 1932 additions and 1313 deletions
+1 -1
View File
@@ -42,7 +42,7 @@
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>2.6.0</version>
<version>2.8.1</version>
<optional>true</optional>
</dependency>
<dependency>
@@ -632,7 +632,8 @@ public class PayApi extends MchApi {
String fileName = String.format("weixin4j_bill_%s_%s_%s.txt",
formatBillDate, billType.name().toLowerCase(),
weixinAccount.getId());
File file = new File(String.format("%s/%s", billPath, fileName));
File file = new File(String.format("%s%s%s", billPath, File.separator,
fileName));
if (file.exists()) {
return file;
}
@@ -1,4 +1,4 @@
package com.foxinmy.weixin4j.token;
package com.foxinmy.weixin4j.cache;
import com.foxinmy.weixin4j.exception.WeixinException;
@@ -11,7 +11,7 @@ import com.foxinmy.weixin4j.exception.WeixinException;
* @since JDK 1.6
* @see
*/
public interface CacheCreator<T> {
public interface CacheCreator<T extends Cacheable> {
/**
* CacheKey
*
@@ -0,0 +1,69 @@
package com.foxinmy.weixin4j.cache;
import com.foxinmy.weixin4j.exception.WeixinException;
/**
* 缓存管理类
*
* @className CacheManager
* @author jinyu(foxinmy@gmail.com)
* @date 2016年5月27日
* @since JDK 1.7
* @see
*/
public class CacheManager<T extends Cacheable> {
protected final CacheCreator<T> cacheCreator;
protected final CacheStorager<T> cacheStorager;
public CacheManager(CacheCreator<T> cacheCreator,
CacheStorager<T> cacheStorager) {
this.cacheCreator = cacheCreator;
this.cacheStorager = cacheStorager;
}
/**
* 获取缓存对象
*
* @return 缓存对象
* @throws WeixinException
*/
public T getCache() throws WeixinException {
String cacheKey = cacheCreator.key();
T cache = cacheStorager.lookup(cacheKey);
if (cache == null) {
cache = cacheCreator.create();
cacheStorager.caching(cacheKey, cache);
}
return cache;
}
/**
* 刷新缓存对象
*
* @return 缓存对象
* @throws WeixinException
*/
public T refreshCache() throws WeixinException {
String cacheKey = cacheCreator.key();
T cache = cacheCreator.create();
cacheStorager.caching(cacheKey, cache);
return cache;
}
/**
* 移除缓存
*
* @return 被移除的缓存对象
*/
public T evictCache() {
String cacheKey = cacheCreator.key();
return cacheStorager.evict(cacheKey);
}
/**
* 清除所有的缓存(<font color="red">请慎重</font>)
*/
public void clearCache() {
cacheStorager.clear();
}
}
@@ -1,4 +1,4 @@
package com.foxinmy.weixin4j.token;
package com.foxinmy.weixin4j.cache;
/**
* Cache的存储
@@ -9,7 +9,17 @@ package com.foxinmy.weixin4j.token;
* @since JDK 1.6
* @see
*/
public interface CacheStorager<T> {
public interface CacheStorager<T extends Cacheable> {
/**
* 考虑到临界情况,实际缓存的有效时间减去该毫秒数(60秒)
*/
long CUTMS = 60 * 1000l;
/**
* 所有的缓存KEY
*/
String ALLKEY = "weixin4j_cache_keys";
/**
* 查找缓存中的对象
*
@@ -42,8 +52,6 @@ public interface CacheStorager<T> {
/**
* 清除所有缓存对象(<font color="red">请慎重</font>)
*
* @param prefix
* 缓存key的前缀
*/
void clear(String prefix);
void clear();
}
@@ -0,0 +1,28 @@
package com.foxinmy.weixin4j.cache;
import java.io.Serializable;
/**
* 可缓存的对象
*
* @className Cacheable
* @author jinyu(foxinmy@gmail.com)
* @date 2016年5月26日
* @since JDK 1.6
* @see
*/
public interface Cacheable extends Serializable {
/**
* 过期时间(单位:毫秒),值小于0时视为永不过期
*
* @return 缓存过期时间
*/
public long getExpires();
/**
* 创建时间(单位:毫秒)
*
* @return 缓存对象创建时间
*/
public long getCreateTime();
}
@@ -0,0 +1,87 @@
package com.foxinmy.weixin4j.cache;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import com.foxinmy.weixin4j.util.SerializationUtils;
/**
* 用File保存缓存对象
*
* @className FileCacheStorager
* @author jinyu(foxinmy@gmail.com)
* @date 2016年5月27日
* @since JDK 1.6
* @see
*/
public class FileCacheStorager<T extends Cacheable> implements CacheStorager<T> {
private final File tmpdir;
private final String SEPARATOR = File.separator;
public FileCacheStorager(String cachePath) {
this.tmpdir = new File(String.format("%s%sweixin4j_token_temp",
cachePath, SEPARATOR));
this.tmpdir.mkdirs();
}
@Override
public T lookup(String cacheKey) {
File cacheFile = new File(String.format("%s%s%s",
tmpdir.getAbsolutePath(), SEPARATOR, cacheKey));
try {
if (cacheFile.exists()) {
T cache = SerializationUtils.deserialize(new FileInputStream(
cacheFile));
if (cache.getCreateTime() < 0) {
return cache;
}
if ((cache.getCreateTime() + cache.getExpires() - CUTMS) > System
.currentTimeMillis()) {
return cache;
}
}
return null;
} catch (IOException e) {
throw new RuntimeException(e);
}
}
@Override
public void caching(String cacheKey, T cache) {
try {
SerializationUtils.serialize(
cache,
new FileOutputStream(new File(String.format("%s%s%s",
tmpdir.getAbsolutePath(), SEPARATOR, cacheKey))));
} catch (IOException e) {
throw new RuntimeException(e);
}
}
@Override
public T evict(String cacheKey) {
T cache = null;
File cacheFile = new File(String.format("%s%s%s",
tmpdir.getAbsolutePath(), SEPARATOR, cacheKey));
try {
if (cacheFile.exists()) {
cache = SerializationUtils.deserialize(new FileInputStream(
cacheFile));
cacheFile.delete();
}
} catch (IOException e) {
; // ingore
}
return cache;
}
@Override
public void clear() {
for (File cache : tmpdir.listFiles()) {
cache.delete();
}
}
}
@@ -1,61 +1,82 @@
package com.foxinmy.weixin4j.token;
package com.foxinmy.weixin4j.cache;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import com.foxinmy.weixin4j.model.Token;
import com.whalin.MemCached.MemCachedClient;
import com.whalin.MemCached.SockIOPool;
/**
* 用Memcache保存Token信息(推荐使用)
* 用Memcache保存缓存对象(推荐使用)
*
* @className MemcacheTokenStorager
* @className MemcacheCacheStorager
* @author jinyu(foxinmy@gmail.com)
* @date 2016年5月11日
* @since JDK 1.6
* @see
*/
public class MemcacheTokenStorager extends TokenStorager {
public class MemcacheCacheStorager<T extends Cacheable> implements
CacheStorager<T> {
private final MemCachedClient mc;
public MemcacheTokenStorager(MemcachePoolConfig poolConfig) {
public MemcacheCacheStorager() {
this(new MemcachePoolConfig());
}
public MemcacheCacheStorager(MemcachePoolConfig poolConfig) {
mc = new MemCachedClient();
poolConfig.initSocketIO();
mc.set(ALLKEY, new HashSet<String>());
}
@SuppressWarnings("unchecked")
@Override
public Token lookup(String cacheKey) {
return (Token) mc.get(cacheKey);
public T lookup(String cacheKey) {
return (T) mc.get(cacheKey);
}
@SuppressWarnings("unchecked")
@Override
public void caching(String cacheKey, Token token) {
if (token.getExpiresIn() > 0) {
mc.set(cacheKey, token,
new Date(token.getCreateTime() + token.getExpiresIn()
* 1000 - ms()));
public void caching(String cacheKey, T cache) {
if (cache.getCreateTime() > 0l) {
mc.set(cacheKey,
cache,
new Date(cache.getCreateTime() + cache.getExpires() - CUTMS));
} else {
mc.set(cacheKey, token);
mc.set(cacheKey, cache);
}
Set<String> all = (Set<String>) mc.get(ALLKEY);
all.add(cacheKey);
mc.set(ALLKEY, all);
}
@SuppressWarnings("unchecked")
@Override
public Token evict(String cacheKey) {
Token token = lookup(cacheKey);
public T evict(String cacheKey) {
T cache = lookup(cacheKey);
mc.delete(cacheKey);
return token;
Set<String> all = (Set<String>) mc.get(ALLKEY);
all.remove(cacheKey);
mc.set(ALLKEY, all);
return cache;
}
@SuppressWarnings("unchecked")
@Override
public void clear(String prefix) {
throw new UnsupportedOperationException();
public void clear() {
Set<String> all = (Set<String>) mc.get(ALLKEY);
for (String key : all) {
mc.delete(key);
}
mc.delete(ALLKEY);
}
public static class MemcachePoolConfig {
public final static String HOST = "localhost";
public final static String HOST = "127.0.0.1";
public final static int PORT = 11211;
public final static int WEIGHT = 1;
@@ -0,0 +1,50 @@
package com.foxinmy.weixin4j.cache;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* 用内存保存缓存对象(不推荐使用)
*
* @className MemoryCacheStorager
* @author jinyu(foxinmy@gmail.com)
* @date 2016年1月24日
* @since JDK 1.6
* @see
*/
public class MemoryCacheStorager<T extends Cacheable> implements
CacheStorager<T> {
private final Map<String, T> CONMAP;
public MemoryCacheStorager() {
this.CONMAP = new ConcurrentHashMap<String, T>();
}
@Override
public T lookup(String cacheKey) {
T cache = this.CONMAP.get(cacheKey);
if (cache != null) {
if ((cache.getCreateTime() + cache.getExpires() - CUTMS) > System
.currentTimeMillis()) {
return cache;
}
}
return null;
}
@Override
public void caching(String cacheKey, T cache) {
this.CONMAP.put(cacheKey, cache);
}
@Override
public T evict(String cacheKey) {
return this.CONMAP.remove(cacheKey);
}
@Override
public void clear() {
this.CONMAP.clear();
}
}
@@ -0,0 +1,13 @@
### CACHE的实现
* CacheCreator 负责创建新的缓存对象
* CacheStorager 负责查找已缓存的对象或者缓存新的对象
* TokenManager 负责对缓存对象的管理(屏蔽细节)
* FileCacheStorager 是系统默认的缓存存储策略实现
* RedisCacheStorager(RedisClusterCacheStorager) 使用redis保存缓存对象(需要自行添加客户端包,[jedis](https://github.com/xetorthio/jedis))
* MemcacheCacheStorager 使用memcache保存缓存对象(需要自行添加客户端包,[Memcached-Java-Client](https://github.com/gwhalin/Memcached-Java-Client))
@@ -0,0 +1,128 @@
package com.foxinmy.weixin4j.cache;
import java.util.Set;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPoolConfig;
import com.foxinmy.weixin4j.model.Consts;
import com.foxinmy.weixin4j.util.SerializationUtils;
/**
* 用Redis保存缓存对象(推荐使用)
*
* @className RedisCacheStorager
* @author jinyu(foxinmy@gmail.com)
* @date 2015年1月9日
* @since JDK 1.6
*/
public class RedisCacheStorager<T extends Cacheable> implements
CacheStorager<T> {
private JedisPool jedisPool;
private final static String HOST = "127.0.0.1";
private final static int PORT = 6379;
private final static int TIMEOUT = 5000;
private final static int MAX_TOTAL = 50;
private final static int MAX_IDLE = 5;
private final static int MAX_WAIT_MILLIS = 5000;
private final static boolean TEST_ON_BORROW = false;
private final static boolean TEST_ON_RETURN = true;
public RedisCacheStorager() {
this(HOST, PORT, TIMEOUT);
}
public RedisCacheStorager(String host, int port, int timeout) {
JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
jedisPoolConfig.setMaxTotal(MAX_TOTAL);
jedisPoolConfig.setMaxIdle(MAX_IDLE);
jedisPoolConfig.setMaxWaitMillis(MAX_WAIT_MILLIS);
jedisPoolConfig.setTestOnBorrow(TEST_ON_BORROW);
jedisPoolConfig.setTestOnReturn(TEST_ON_RETURN);
this.jedisPool = new JedisPool(jedisPoolConfig, host, port, timeout);
}
public RedisCacheStorager(JedisPoolConfig jedisPoolConfig) {
this(new JedisPool(jedisPoolConfig, HOST, PORT, TIMEOUT));
}
public RedisCacheStorager(String host, int port, int timeout,
JedisPoolConfig jedisPoolConfig) {
this(new JedisPool(jedisPoolConfig, host, port, timeout));
}
public RedisCacheStorager(JedisPool jedisPool) {
this.jedisPool = jedisPool;
}
@SuppressWarnings("unchecked")
@Override
public T lookup(String cacheKey) {
Jedis jedis = null;
try {
jedis = jedisPool.getResource();
byte[] value = jedis.get(cacheKey.getBytes(Consts.UTF_8));
return value != null ? (T) SerializationUtils.deserialize(value)
: null;
} finally {
if (jedis != null) {
jedis.close();
}
}
}
@Override
public void caching(String cacheKey, T cache) {
Jedis jedis = null;
try {
jedis = jedisPool.getResource();
byte[] key = cacheKey.getBytes(Consts.UTF_8);
byte[] value = SerializationUtils.serialize(cache);
jedis.set(key, value);
if (cache.getExpires() > 0) {
jedis.expire(key, (int) (cache.getExpires() - CUTMS) / 1000);
}
jedis.sadd(ALLKEY, cacheKey);
} finally {
if (jedis != null) {
jedis.close();
}
}
}
@Override
public T evict(String cacheKey) {
T cache = lookup(cacheKey);
Jedis jedis = null;
try {
jedis = jedisPool.getResource();
jedis.del(cacheKey);
jedis.srem(ALLKEY, cacheKey);
} finally {
if (jedis != null) {
jedis.close();
}
}
return cache;
}
@Override
public void clear() {
Jedis jedis = null;
try {
jedis = jedisPool.getResource();
Set<String> cacheKeys = jedis.smembers(ALLKEY);
if (!cacheKeys.isEmpty()) {
cacheKeys.add(ALLKEY);
jedis.del(cacheKeys.toArray(new String[cacheKeys.size()]));
}
} finally {
if (jedis != null) {
jedis.close();
}
}
}
}
@@ -0,0 +1,94 @@
package com.foxinmy.weixin4j.cache;
import java.util.Set;
import redis.clients.jedis.HostAndPort;
import redis.clients.jedis.JedisCluster;
import redis.clients.jedis.JedisPoolConfig;
import com.foxinmy.weixin4j.model.Consts;
import com.foxinmy.weixin4j.util.SerializationUtils;
/**
* 用Redis(集群)保存缓存对象(推荐使用)
*
* @className RedisCacheStorager
* @author jinyu(foxinmy@gmail.com)
* @date 2015年1月9日
* @since JDK 1.6
*/
public class RedisClusterCacheStorager<T extends Cacheable> implements
CacheStorager<T> {
private final static int CONNECTION_TIMEOUT = 5000;
private final static int SO_TIMEOUT = 5000;
private final static int MAX_REDIRECTIONS = 5;
private final static int MAX_TOTAL = 50;
private final static int MAX_IDLE = 5;
private final static int MAX_WAIT_MILLIS = 5000;
private final static boolean TEST_ON_BORROW = false;
private final static boolean TEST_ON_RETURN = true;
private final JedisCluster jedisCluster;
public RedisClusterCacheStorager(Set<HostAndPort> nodes) {
JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
jedisPoolConfig.setMaxTotal(MAX_TOTAL);
jedisPoolConfig.setMaxIdle(MAX_IDLE);
jedisPoolConfig.setMaxWaitMillis(MAX_WAIT_MILLIS);
jedisPoolConfig.setTestOnBorrow(TEST_ON_BORROW);
jedisPoolConfig.setTestOnReturn(TEST_ON_RETURN);
this.jedisCluster = new JedisCluster(nodes, CONNECTION_TIMEOUT,
SO_TIMEOUT, MAX_REDIRECTIONS, jedisPoolConfig);
}
public RedisClusterCacheStorager(Set<HostAndPort> nodes,
JedisPoolConfig poolConfig) {
this(nodes, CONNECTION_TIMEOUT, SO_TIMEOUT, MAX_REDIRECTIONS,
poolConfig);
}
public RedisClusterCacheStorager(Set<HostAndPort> nodes,
int connectionTimeout, int soTimeout, int maxRedirections,
JedisPoolConfig poolConfig) {
this(new JedisCluster(nodes, connectionTimeout, soTimeout,
maxRedirections, poolConfig));
}
public RedisClusterCacheStorager(JedisCluster jedisCluster) {
this.jedisCluster = jedisCluster;
}
@SuppressWarnings("unchecked")
@Override
public T lookup(String cacheKey) {
byte[] value = jedisCluster.get(cacheKey.getBytes(Consts.UTF_8));
return value != null ? (T) SerializationUtils.deserialize(value) : null;
}
@Override
public void caching(String cacheKey, T cache) {
byte[] key = cacheKey.getBytes(Consts.UTF_8);
byte[] value = SerializationUtils.serialize(cache);
jedisCluster.set(key, value);
if (cache.getExpires() > 0) {
jedisCluster.expire(key, (int) (cache.getExpires() - CUTMS) / 1000);
}
jedisCluster.sadd(ALLKEY, cacheKey);
}
@Override
public T evict(String cacheKey) {
T cache = lookup(cacheKey);
jedisCluster.del(cacheKey);
jedisCluster.srem(ALLKEY, cacheKey);
return cache;
}
@Override
public void clear() {
Set<String> cacheKeys = jedisCluster.smembers(ALLKEY);
if (!cacheKeys.isEmpty()) {
cacheKeys.add(ALLKEY);
jedisCluster.del(cacheKeys.toArray(new String[cacheKeys.size()]));
}
}
}
@@ -1431,6 +1431,10 @@
<code>81003</code>
<text>邀请额度已用完</text>
</error>
<error>
<code>81004</code>
<text>部门数量超过上限</text>
</error>
<error>
<code>82001</code>
<text>发送消息或者邀请的参数全部为空或者全部不合法</text>
@@ -7,7 +7,7 @@ import java.util.Set;
import com.alibaba.fastjson.JSONObject;
import com.foxinmy.weixin4j.exception.WeixinException;
import com.foxinmy.weixin4j.token.TokenHolder;
import com.foxinmy.weixin4j.token.TokenManager;
import com.foxinmy.weixin4j.util.DateUtil;
import com.foxinmy.weixin4j.util.DigestUtil;
import com.foxinmy.weixin4j.util.MapUtil;
@@ -17,7 +17,7 @@ import com.foxinmy.weixin4j.util.Weixin4jConfigUtil;
/**
* JSSDK配置类
*
*
* @className JSSDKConfigurator
* @author jinyu(foxinmy@gmail.com)
* @date 2015年12月23日
@@ -25,17 +25,17 @@ import com.foxinmy.weixin4j.util.Weixin4jConfigUtil;
* @see
*/
public class JSSDKConfigurator {
private final TokenHolder ticketTokenHolder;
private final TokenManager ticketTokenManager;
private JSONObject config;
private Set<JSSDKAPI> apis;
/**
* ticket保存类 可调用WeixinProxy#getTicketHolder获取
*
* @param ticketTokenHolder
* ticket保存类 可调用WeixinProxy#getTicketManager获取
*
* @param ticketTokenManager
*/
public JSSDKConfigurator(TokenHolder ticketTokenHolder) {
this.ticketTokenHolder = ticketTokenHolder;
public JSSDKConfigurator(TokenManager ticketTokenManager) {
this.ticketTokenManager = ticketTokenManager;
this.config = new JSONObject();
this.apis = new HashSet<JSSDKAPI>();
}
@@ -43,7 +43,7 @@ public class JSSDKConfigurator {
/**
* 开启调试模式,调用的所有api的返回值会在客户端alert出来,若要查看传入的参数,可以在pc端打开,参数信息会通过log打出,
* 仅在pc端时才会打印。
*
*
* @return
*/
public JSSDKConfigurator debugMode() {
@@ -53,7 +53,7 @@ public class JSSDKConfigurator {
/**
* 公众号的唯一标识 不填则获取weixin4j.properties#account中的id
*
*
* @param appId
* @return
*/
@@ -64,7 +64,7 @@ public class JSSDKConfigurator {
/**
* 需要使用的JS接口列表
*
*
* @see JSSDKAPI
* @param apis
* @return
@@ -78,7 +78,7 @@ public class JSSDKConfigurator {
/**
* 需要使用的JS接口列表
*
*
* @see JSSDKAPI
* @param apis
* @return
@@ -94,7 +94,7 @@ public class JSSDKConfigurator {
/**
* 生成config配置JSON串
*
*
* @param url
* 当前网页的URL,不包含#及其后面部分
* @return jssdk配置JSON字符串
@@ -113,7 +113,7 @@ public class JSSDKConfigurator {
String noncestr = RandomUtil.generateString(24);
signMap.put("timestamp", timestamp);
signMap.put("noncestr", noncestr);
signMap.put("jsapi_ticket", this.ticketTokenHolder.getAccessToken());
signMap.put("jsapi_ticket", this.ticketTokenManager.getAccessToken());
signMap.put("url", url);
String sign = DigestUtil.SHA1(MapUtil.toJoinString(signMap, false,
false));
@@ -6,6 +6,7 @@ import java.util.Arrays;
import java.util.List;
import com.alibaba.fastjson.annotation.JSONField;
import com.foxinmy.weixin4j.tuple.MpArticle;
import com.foxinmy.weixin4j.type.ButtonType;
/**
@@ -38,13 +39,20 @@ public class Button implements Serializable {
/**
* 菜单KEY值,根据type的类型而定</p> 通过公众平台设置的自定义菜单:</br> <li>text:保存文字; <li>
* img、voice:保存媒体ID <li>video:保存视频URL <li>
* news:保存图文消息:List#com.foxinmy.weixin4j.tuple.MpArticle# <li>view:保存链接URL
* <p>使用API设置的自定义菜单:</p> <li>
* news:保存图文消息媒体ID <li>view:保存链接URL
* <p>
* 使用API设置的自定义菜单:
* </p> <li>
* click、scancode_push、scancode_waitmsg、pic_sysphoto、pic_photo_or_album、
* pic_weixin、location_select:保存key <li>view:保存链接URL; <li>
* media_id、view_limited:保存媒体ID
*/
private Serializable content;
private String content;
/**
* 图文列表 只有在公众平台设置的菜单才有
*/
@JSONField(serialize = false, deserialize = false)
private List<MpArticle> articles;
/**
* 二级菜单数组,个数应为1~5个
*/
@@ -101,14 +109,27 @@ public class Button implements Serializable {
this.type = type;
}
public Serializable getContent() {
public String getContent() {
return content;
}
public void setContent(Serializable content) {
public void setContent(String content) {
this.content = content;
}
public List<MpArticle> getArticles() {
return articles;
}
/**
* <font color="red">创建菜单设置无效</font>
*
* @param articles
*/
public void setArticles(List<MpArticle> articles) {
this.articles = articles;
}
public List<Button> getSubs() {
return subs;
}
@@ -125,6 +146,6 @@ public class Button implements Serializable {
@Override
public String toString() {
return "Button [name=" + name + ", type=" + type + ", content="
+ content + ", subs=" + subs + "]";
+ content + ", articles=" + articles + ", subs=" + subs + "]";
}
}
@@ -4,7 +4,7 @@ import java.nio.charset.Charset;
/**
* 常量类
*
*
* @className Consts
* @author jinyu(foxinmy@gmail.com)
* @date 2014年12月3日
@@ -1,15 +1,13 @@
package com.foxinmy.weixin4j.model;
import com.alibaba.fastjson.annotation.JSONCreator;
import com.alibaba.fastjson.annotation.JSONField;
import com.foxinmy.weixin4j.type.MediaType;
import java.io.Serializable;
import java.util.Date;
import com.foxinmy.weixin4j.type.MediaType;
/**
* 媒体文件上传结果
*
*
* @className MediaUploadResult
* @author jinyu(foxinmy@gmail.com)
* @date 2015年7月25日
@@ -27,11 +25,8 @@ public class MediaUploadResult implements Serializable {
*/
private String url;
@JSONCreator
public MediaUploadResult(@JSONField(name = "media_id") String mediaId,
@JSONField(name = "type") MediaType mediaType,
@JSONField(name = "created_at") Date createdAt,
@JSONField(name = "url") String url) {
public MediaUploadResult(String mediaId, MediaType mediaType,
Date createdAt, String url) {
this.mediaId = mediaId;
this.mediaType = mediaType;
this.createdAt = createdAt;
@@ -1,13 +1,14 @@
package com.foxinmy.weixin4j.model;
import java.io.Serializable;
import java.util.HashMap;
import java.util.Map;
import com.alibaba.fastjson.annotation.JSONField;
import com.foxinmy.weixin4j.cache.Cacheable;
/**
* access_token是公众号的全局唯一票据,公众号调用各接口时都需使用access_token,正常情况下access_token有效期为7200秒,
* 重复获取将导致上次获取的access_token失效
*
*
* @className Token
* @author jinyu(foxinmy@gmail.com)
* @date 2014年4月5日
@@ -17,75 +18,95 @@ import com.alibaba.fastjson.annotation.JSONField;
* @see <a
* href="http://qydev.weixin.qq.com/wiki/index.php?title=%E4%B8%BB%E5%8A%A8%E8%B0%83%E7%94%A8">微信企业号的主动模式</a>
*/
public class Token implements Serializable {
public class Token implements Cacheable {
private static final long serialVersionUID = -7564855472419104084L;
/**
* 获取到的凭证
*/
@JSONField(name = "access_token")
private String accessToken;
/**
* 凭证有效时间,单位:秒
* 凭证有效时间,单位:
*/
@JSONField(name = "expires_in")
private int expiresIn;
private long expires;
/**
* token创建的时间,单位:毫秒
*/
@JSONField(name = "create_time")
private long createTime;
/**
* 请求返回的原始结果
* 扩展信息
*/
@JSONField(name = "original_result")
private String originalResult;
private Map<String, String> extra;
protected Token() {
// jaxb required
/**
* 永不过期、创建时间为当前时间戳的token对象
*
* @param accessToken
* 凭证字符串
*/
public Token(String accessToken) {
this(accessToken, -1);
}
public Token(String accessToken) {
/**
* 有过期时间、创建时间为当前时间戳的token对象
*
* @param accessToken
* 凭证字符串
* @param expires
* 过期时间 单位毫秒
*/
public Token(String accessToken, long expires) {
this(accessToken, expires, System.currentTimeMillis());
}
/**
*
* @param accessToken
* 凭证字符串
* @param expires
* 过期时间 单位毫秒
* @param createTime
* 创建时间戳 单位毫秒
*/
public Token(String accessToken, long expires, long createTime) {
this.accessToken = accessToken;
this.createTime = System.currentTimeMillis();
this.expires = expires;
this.createTime = createTime;
this.extra = new HashMap<String, String>();
}
public String getAccessToken() {
return accessToken;
}
public void setAccessToken(String accessToken) {
this.accessToken = accessToken;
}
public int getExpiresIn() {
return expiresIn;
}
public void setExpiresIn(int expiresIn) {
this.expiresIn = expiresIn;
@Override
public long getExpires() {
return expires;
}
@Override
public long getCreateTime() {
return createTime;
}
public void setCreateTime(long createTime) {
this.createTime = createTime;
public Map<String, String> getExtra() {
return extra;
}
public String getOriginalResult() {
return originalResult;
public void setExtra(Map<String, String> extra) {
this.extra = extra;
}
public void setOriginalResult(String originalResult) {
this.originalResult = originalResult;
public Token pushExtra(String name, String value) {
this.extra.put(name, value);
return this;
}
@Override
public String toString() {
return "Token [accessToken=" + accessToken + ", expiresIn=" + expiresIn
+ ", createTime=" + createTime + "]";
return "Token [accessToken=" + accessToken + ", expires=" + expires
+ ", createTime=" + createTime + ", extra=" + extra + "]";
}
}
@@ -1,8 +1,6 @@
package com.foxinmy.weixin4j.setting;
import com.foxinmy.weixin4j.http.HttpParams;
import com.foxinmy.weixin4j.token.FileTokenStorager;
import com.foxinmy.weixin4j.token.TokenStorager;
/**
* 系统配置相关
@@ -22,10 +20,6 @@ public abstract class SystemSettings<T> {
* Http参数
*/
private HttpParams httpParams;
/**
* token存储方式 默认为FileTokenStorager
*/
private TokenStorager tokenStorager;
/**
* 系统临时目录
*/
@@ -59,17 +53,6 @@ public abstract class SystemSettings<T> {
public abstract String getTmpdir0();
public TokenStorager getTokenStorager() {
return tokenStorager;
}
public TokenStorager getTokenStorager0() {
if (tokenStorager == null) {
return new FileTokenStorager(getTmpdir0());
}
return tokenStorager;
}
public void setHttpParams(HttpParams httpParams) {
this.httpParams = httpParams;
}
@@ -78,13 +61,9 @@ public abstract class SystemSettings<T> {
this.tmpdir = tmpdir;
}
public void setTokenStorager(TokenStorager tokenStorager) {
this.tokenStorager = tokenStorager;
}
@Override
public String toString() {
return "account=" + account + ", httpParams=" + httpParams
+ ",tokenStorager=" + tokenStorager + ", tmpdir=" + tmpdir;
+ ", tmpdir=" + tmpdir;
}
}
}
@@ -1,6 +1,9 @@
package com.foxinmy.weixin4j.setting;
import com.alibaba.fastjson.JSON;
import com.foxinmy.weixin4j.cache.CacheStorager;
import com.foxinmy.weixin4j.cache.FileCacheStorager;
import com.foxinmy.weixin4j.model.Token;
import com.foxinmy.weixin4j.model.WeixinAccount;
import com.foxinmy.weixin4j.model.WeixinPayAccount;
import com.foxinmy.weixin4j.util.StringUtil;
@@ -20,6 +23,10 @@ public class Weixin4jSettings extends SystemSettings<WeixinAccount> {
* 微信支付账号信息
*/
private WeixinPayAccount weixinPayAccount;
/**
* Token的存储方式 默认为FileCacheStorager
*/
private CacheStorager<Token> cacheStorager;
/**
* 支付接口需要的证书文件(*.p12)
*/
@@ -81,6 +88,21 @@ public class Weixin4jSettings extends SystemSettings<WeixinAccount> {
return getTmpdir();
}
public CacheStorager<Token> getCacheStorager() {
return cacheStorager;
}
public CacheStorager<Token> getCacheStorager0() {
if (cacheStorager == null) {
return new FileCacheStorager<Token>(getTmpdir0());
}
return cacheStorager;
}
public void setCacheStorager(CacheStorager<Token> cacheStorager) {
this.cacheStorager = cacheStorager;
}
public String getCertificateFile() {
return certificateFile;
}
@@ -4,7 +4,7 @@ import com.foxinmy.weixin4j.util.MapUtil;
/**
* 微信签名
*
*
* @className AbstractWeixinSignature
* @author jinyu(foxinmy@gmail.com)
* @date 2016年3月26日
@@ -14,25 +14,27 @@ import com.foxinmy.weixin4j.util.MapUtil;
public abstract class AbstractWeixinSignature implements WeixinSignature {
/**
* 是否编码
*
* @return
*
* @return 默认false不进行编码
*/
@Override
public boolean encoder() {
return false;
}
/**
* 是否转换小写
*
* @return
*
* @return 默认false不转换小写
*/
@Override
public boolean lowerCase() {
return false;
}
/**
* 拼接字符串
*
*
* @param obj
* @return
*/
@@ -1,95 +0,0 @@
package com.foxinmy.weixin4j.token;
import java.io.File;
import java.io.FileFilter;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import com.foxinmy.weixin4j.model.Token;
import com.foxinmy.weixin4j.util.FileUtil;
import com.foxinmy.weixin4j.xml.XmlStream;
/**
* 用File形式保存Token信息
*
* @className FileTokenStorager
* @author jinyu(foxinmy@gmail.com)
* @date 2015年1月9日
* @since JDK 1.6
*/
public class FileTokenStorager extends TokenStorager {
private final String cachePath;
public FileTokenStorager(String cachePath) {
this.cachePath = cachePath;
}
@Override
public Token lookup(String cacheKey) {
File token_file = new File(String.format("%s/%s.xml", cachePath,
cacheKey));
try {
if (token_file.exists()) {
Token token = XmlStream.fromXML(
new FileInputStream(token_file), Token.class);
if (token.getCreateTime() < 0) {
return token;
}
if ((token.getCreateTime() + (token.getExpiresIn() * 1000l) - ms()) > System
.currentTimeMillis()) {
return token;
}
}
return null;
} catch (IOException e) {
throw new RuntimeException(e);
}
}
@Override
public void caching(String cacheKey, Token token) {
try {
XmlStream.toXML(
token,
new FileOutputStream(new File(String.format("%s/%s.xml",
cachePath, cacheKey))));
} catch (IOException e) {
throw new RuntimeException(e);
}
}
@Override
public Token evict(String cacheKey) {
Token token = null;
File token_file = new File(String.format("%s/%s.xml", cachePath,
cacheKey));
try {
if (token_file.exists()) {
token = XmlStream.fromXML(new FileInputStream(token_file),
Token.class);
token_file.delete();
}
} catch (IOException e) {
; // ingore
}
return token;
}
@Override
public void clear(final String prefix) {
File[] files = new File(cachePath).listFiles(new FileFilter() {
@Override
public boolean accept(File file) {
return file.isFile()
&& file.getName().startsWith(prefix)
&& "xml".equals(FileUtil.getFileExtension(file
.getName()));
}
});
for (File token : files) {
token.delete();
}
}
}
@@ -1,51 +0,0 @@
package com.foxinmy.weixin4j.token;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import com.foxinmy.weixin4j.model.Token;
/**
* 用内存保存Token信息(不推荐使用)
*
* @className MemoryTokenStorager
* @author jinyu(foxinmy@gmail.com)
* @date 2016年1月24日
* @since JDK 1.6
* @see
*/
public class MemoryTokenStorager extends TokenStorager {
private final Map<String, Token> CONMAP;
public MemoryTokenStorager() {
this.CONMAP = new ConcurrentHashMap<String, Token>();
}
@Override
public Token lookup(String cacheKey) {
Token token = this.CONMAP.get(cacheKey);
if (token != null) {
if ((token.getCreateTime() + (token.getExpiresIn() * 1000l) - ms()) > System
.currentTimeMillis()) {
return token;
}
}
return null;
}
@Override
public void caching(String cacheKey, Token token) {
this.CONMAP.put(cacheKey, token);
}
@Override
public Token evict(String cacheKey) {
return this.CONMAP.remove(cacheKey);
}
@Override
public void clear(String prefix) {
this.CONMAP.clear();
}
}
@@ -1,13 +0,0 @@
### TOKEN的实现
* TokenCreator 负责创建新的token
* TokenStorager 负责查找已缓存的token或者缓存新的token
* TokenHolder 负责获取token(屏蔽了获取细节)
* FileTokenStorager 是系统默认的token存储策略实现
* RedisTokenStorager 使用redis保存token(需要自行添加客户端包,[jedis](https://github.com/xetorthio/jedis))
* MemcacheTokenStorager 使用memcache保存token(需要自行添加客户端包,[Memcached-Java-Client](https://github.com/gwhalin/Memcached-Java-Client))
@@ -1,147 +0,0 @@
package com.foxinmy.weixin4j.token;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPoolConfig;
import redis.clients.jedis.Pipeline;
import com.foxinmy.weixin4j.model.Token;
/**
* 用Redis保存Token信息(推荐使用)
*
* @className RedisTokenStorager
* @author jinyu(foxinmy@gmail.com)
* @date 2015年1月9日
* @since JDK 1.6
*/
public class RedisTokenStorager extends TokenStorager {
private JedisPool jedisPool;
public final static String HOST = "localhost";
public final static int PORT = 6379;
public final static int MAX_TOTAL = 50;
public final static int MAX_IDLE = 5;
public final static int MAX_WAIT_MILLIS = 3000;
public final static boolean TEST_ON_BORROW = false;
public final static boolean TEST_ON_RETURN = true;
public RedisTokenStorager() {
this(HOST, PORT);
}
public RedisTokenStorager(String host, int port) {
JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
jedisPoolConfig.setMaxTotal(MAX_TOTAL);
jedisPoolConfig.setMaxIdle(MAX_IDLE);
jedisPoolConfig.setMaxWaitMillis(MAX_WAIT_MILLIS);
jedisPoolConfig.setTestOnBorrow(TEST_ON_BORROW);
jedisPoolConfig.setTestOnReturn(TEST_ON_RETURN);
this.jedisPool = new JedisPool(jedisPoolConfig, host, port);
}
public RedisTokenStorager(String host, int port,
JedisPoolConfig jedisPoolConfig) {
this(new JedisPool(jedisPoolConfig, host, port));
}
public RedisTokenStorager(JedisPool jedisPool) {
this.jedisPool = jedisPool;
}
@Override
public Token lookup(String cacheKey) {
Jedis jedis = null;
try {
jedis = jedisPool.getResource();
Map<String, String> map = jedis.hgetAll(cacheKey);
if (map != null && !map.isEmpty()) {
return map2token(map);
}
} finally {
if (jedis != null) {
jedis.close();
}
}
return null;
}
@Override
public void caching(String cacheKey, Token token) {
Jedis jedis = null;
try {
jedis = jedisPool.getResource();
jedis.hmset(cacheKey, token2map(token));
if (token.getExpiresIn() > 0) {
jedis.expire(cacheKey, token.getExpiresIn()
- (int) (ms() / 1000l));
}
} finally {
if (jedis != null) {
jedis.close();
}
}
}
private final static String ACCESSTOKEN_KEY = "accessToken";
private final static String EXPIRESIN_KEY = "expiresIn";
private final static String CREATETIME_KEY = "createTime";
private final static String ORIGINAL_KEY = "originalResult";
protected Map<String, String> token2map(Token token) {
Map<String, String> map = new HashMap<String, String>();
map.put(ACCESSTOKEN_KEY, token.getAccessToken());
map.put(EXPIRESIN_KEY, Integer.toString(token.getExpiresIn()));
map.put(CREATETIME_KEY, Long.toString(token.getCreateTime()));
map.put(ORIGINAL_KEY, token.getOriginalResult());
return map;
}
protected Token map2token(Map<String, String> map) {
Token token = new Token(map.get(ACCESSTOKEN_KEY));
token.setExpiresIn(Integer.parseInt(map.get(EXPIRESIN_KEY)));
token.setCreateTime(Long.parseLong(map.get(CREATETIME_KEY)));
token.setOriginalResult(map.get(ORIGINAL_KEY));
return token;
}
@Override
public Token evict(String cacheKey) {
Token token = lookup(cacheKey);
Jedis jedis = null;
try {
jedis = jedisPool.getResource();
jedis.del(cacheKey);
} finally {
if (jedis != null) {
jedis.close();
}
}
return token;
}
@Override
public void clear(String prefix) {
Jedis jedis = null;
try {
jedis = jedisPool.getResource();
Set<String> cacheKeys = jedis.keys(String.format("%s*", prefix));
if (!cacheKeys.isEmpty()) {
Pipeline pipeline = jedis.pipelined();
for (String cacheKey : cacheKeys) {
pipeline.del(cacheKey);
}
pipeline.sync();
}
} finally {
if (jedis != null) {
jedis.close();
}
}
}
}
@@ -1,5 +1,6 @@
package com.foxinmy.weixin4j.token;
import com.foxinmy.weixin4j.cache.CacheCreator;
import com.foxinmy.weixin4j.http.weixin.WeixinRequestExecutor;
import com.foxinmy.weixin4j.model.Token;
@@ -14,21 +15,17 @@ import com.foxinmy.weixin4j.model.Token;
*/
public abstract class TokenCreator implements CacheCreator<Token> {
/**
* 缓存KEY前缀
*/
public final static String CACHEKEY_PREFIX = "weixin4j_";
protected final WeixinRequestExecutor weixinExecutor;
public TokenCreator() {
this.weixinExecutor = new WeixinRequestExecutor();
}
/**
* 缓存key的前缀
*
* @return 默认为weixin4j_
*/
public String prefix() {
return "weixin4j_";
}
/**
* 缓存key:附加key前缀
*
@@ -36,7 +33,7 @@ public abstract class TokenCreator implements CacheCreator<Token> {
*/
@Override
public String key() {
return String.format("%s%s", prefix(), key0());
return String.format("%s%s", CACHEKEY_PREFIX, key0());
}
/**
@@ -1,95 +0,0 @@
package com.foxinmy.weixin4j.token;
import com.foxinmy.weixin4j.exception.WeixinException;
import com.foxinmy.weixin4j.model.Token;
/**
* 对token的缓存获取
*
* @className TokenHolder
* @author jinyu(foxinmy@gmail.com)
* @date 2015年6月12日
* @since JDK 1.6
* @see TokenCreator
* @see TokenStorager
*/
public class TokenHolder {
/**
* token的创建
*/
private final TokenCreator tokenCreator;
/**
* token的存储
*/
private final TokenStorager tokenStorager;
/**
*
* @param tokenCreator
* token创建器
* @param tokenStorager
* token保存器
*/
public TokenHolder(TokenCreator tokenCreator, TokenStorager tokenStorager) {
this.tokenCreator = tokenCreator;
this.tokenStorager = tokenStorager;
}
/**
* 获取token对象
*
* @return
* @throws WeixinException
*/
public Token getToken() throws WeixinException {
String cacheKey = tokenCreator.key();
Token token = tokenStorager.lookup(cacheKey);
if (token == null) {
token = tokenCreator.create();
tokenStorager.caching(cacheKey, token);
}
return token;
}
/**
* 获取token字符串
*
* @return
* @throws WeixinException
*/
public String getAccessToken() throws WeixinException {
return getToken().getAccessToken();
}
/**
* 手动刷新token
*
* @return 刷新后的token
* @throws WeixinException
*/
public Token refreshToken() throws WeixinException {
String cacheKey = tokenCreator.key();
Token token = tokenCreator.create();
tokenStorager.caching(cacheKey, token);
return token;
}
/**
* 移除token
*
* @return 被移除的token
*/
public Token evictToken() {
String cacheKey = tokenCreator.key();
return tokenStorager.evict(cacheKey);
}
/**
* 清除所有的token(<font color="red">请慎重</font>)
*/
public void clearToken() {
String prefix = tokenCreator.prefix();
tokenStorager.clear(prefix);
}
}
@@ -0,0 +1,41 @@
package com.foxinmy.weixin4j.token;
import com.foxinmy.weixin4j.cache.CacheManager;
import com.foxinmy.weixin4j.cache.CacheStorager;
import com.foxinmy.weixin4j.exception.WeixinException;
import com.foxinmy.weixin4j.model.Token;
/**
* 对token的缓存获取
*
* @className TokenManager
* @author jinyu(foxinmy@gmail.com)
* @date 2015年6月12日
* @since JDK 1.6
* @see TokenCreator
* @see com.foxinmy.weixin4j.cache.CacheStorager
*/
public class TokenManager extends CacheManager<Token> {
/**
*
* @param tokenCreator
* 负责微信各种token的创建
* @param cacheStorager
* 负责token的存储
*/
public TokenManager(TokenCreator tokenCreator,
CacheStorager<Token> cacheStorager) {
super(tokenCreator, cacheStorager);
}
/**
* 获取token字符串
*
* @return
* @throws WeixinException
*/
public String getAccessToken() throws WeixinException {
return super.getCache().getAccessToken();
}
}
@@ -1,27 +0,0 @@
package com.foxinmy.weixin4j.token;
import com.foxinmy.weixin4j.model.Token;
/**
* Token的存储
*
* @className TokenStorager
* @author jinyu(foxinmy@gmail.com)
* @date 2014年9月27日
* @since JDK 1.6
* @see com.foxinmy.weixin4j.model.Token
* @see MemoryTokenStorager
* @see FileTokenStorager
* @see RedisTokenStorager
* @see MemcacheTokenStorager
*/
public abstract class TokenStorager implements CacheStorager<Token> {
/**
* 考虑到临界情况,实际token的有效时间减去该毫秒数
*
* @return 默认为60秒
*/
public long ms() {
return 60 * 1000l;
}
}
@@ -9,7 +9,7 @@ import com.alibaba.fastjson.annotation.JSONField;
/**
* 客服消息图文
*
*
* @className Article
* @author jinyu(foxinmy@gmail.com)
* @date 2014年9月29日
@@ -43,6 +43,17 @@ public class Article implements Serializable {
@XmlElement(name = "Url")
private String url;
/**
*
* @param title
* 标题
* @param desc
* 描述
* @param picUrl
* 图片链接
* @param url
* 跳转URL
*/
@JSONCreator
public Article(@JSONField(name = "title") String title,
@JSONField(name = "desc") String desc,
@@ -2,12 +2,12 @@ package com.foxinmy.weixin4j.tuple;
import java.io.Serializable;
import com.alibaba.fastjson.annotation.JSONCreator;
import com.alibaba.fastjson.annotation.JSONField;
/**
* 群发消息图文(消息内容存储在微信后台)
*
* @className MpArticle
* @author jinyu(foxinmy@gmail.com)
* @date 2014年4月26日
* @since JDK 1.6
@@ -22,7 +22,7 @@ public class MpArticle implements Serializable {
@JSONField(name = "thumb_media_id")
private String thumbMediaId;
/**
* 图文消息的封面图片的地址,第三方开发者也可以使用这个URL下载图片到自己服务器中,然后显示在自己网站上
* 图文消息的封面图片的地址(不一定有,请关注thumbMediaId)
*/
@JSONField(name = "thumb_url")
private String thumbUrl;
@@ -34,6 +34,10 @@ public class MpArticle implements Serializable {
* 图文消息的标题 非空
*/
private String title;
/**
* 图文页的URL 获取图文消息时
*/
private String url;
/**
* 在图文消息页面点击“阅读原文”后的页面 可为空
*/
@@ -52,50 +56,40 @@ public class MpArticle implements Serializable {
*/
@JSONField(name = "show_cover_pic")
private String showCoverPic;
/**
* 正文的URL 可为空
*/
@JSONField(name = "content_url")
private String contentUrl;
/**
* 封面图片的URL 可为空
*/
@JSONField(name = "cover_url")
private String coverUrl;
protected MpArticle() {
}
/**
* @param thumbMediaId
* 缩略图
* @param title
* 标题
* @param content
* 内容
*/
public MpArticle(String thumbMediaId, String title, String content) {
this.thumbMediaId = thumbMediaId;
this.title = title;
this.content = content;
}
@JSONCreator
public MpArticle(@JSONField(name = "thumbMediaId") String thumbMediaId,
@JSONField(name = "thumbUrl") String thumbUrl,
@JSONField(name = "author") String author,
@JSONField(name = "title") String title,
@JSONField(name = "sourceUrl") String sourceUrl,
@JSONField(name = "content") String content,
@JSONField(name = "digest") String digest,
@JSONField(name = "showCoverPic") String showCoverPic,
@JSONField(name = "contentUrl") String contentUrl,
@JSONField(name = "coverUrl") String coverUrl) {
this.thumbMediaId = thumbMediaId;
this.thumbUrl = thumbUrl;
this.author = author;
this.title = title;
this.sourceUrl = sourceUrl;
this.content = content;
this.digest = digest;
this.showCoverPic = showCoverPic;
this.contentUrl = contentUrl;
this.coverUrl = coverUrl;
}
public String getThumbMediaId() {
return thumbMediaId;
}
public void setThumbMediaId(String thumbMediaId) {
this.thumbMediaId = thumbMediaId;
}
public String getThumbUrl() {
return thumbUrl;
}
public void setThumbUrl(String thumbUrl) {
this.thumbUrl = thumbUrl;
}
public String getAuthor() {
return author;
}
@@ -108,6 +102,18 @@ public class MpArticle implements Serializable {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public String getUrl() {
return url;
}
public void setUrl(String url) {
this.url = url;
}
public String getSourceUrl() {
return sourceUrl;
}
@@ -120,6 +126,10 @@ public class MpArticle implements Serializable {
return content;
}
public void setContent(String content) {
this.content = content;
}
public String getDigest() {
return digest;
}
@@ -132,36 +142,25 @@ public class MpArticle implements Serializable {
return showCoverPic;
}
public void setShowCoverPic(String showCoverPic) {
this.showCoverPic = showCoverPic;
}
public void setShowCoverPic(boolean showCoverPic) {
this.showCoverPic = showCoverPic ? "1" : "0";
}
public String getContentUrl() {
return contentUrl;
}
public void setContentUrl(String contentUrl) {
this.contentUrl = contentUrl;
}
public String getCoverUrl() {
return coverUrl;
}
public void setCoverUrl(String coverUrl) {
this.coverUrl = coverUrl;
}
public String getThumbUrl() {
return thumbUrl;
@JSONField(serialize = false)
public boolean getFormatShowCoverPic() {
return this.showCoverPic != null && this.showCoverPic.equals("1");
}
@Override
public String toString() {
return "MpArticle [thumbMediaId=" + thumbMediaId + ",thumbUrl="
return "MpArticle [thumbMediaId=" + thumbMediaId + ", thumbUrl="
+ thumbUrl + ", author=" + author + ", title=" + title
+ ", sourceUrl=" + sourceUrl + ", content=" + content
+ ", digest=" + digest + ", showCoverPic=" + showCoverPic
+ ", contentUrl=" + contentUrl + ", coverUrl=" + coverUrl + "]";
+ ", url=" + url + ", digest=" + digest + ", showCoverPic="
+ showCoverPic + "]";
}
}
@@ -1,20 +1,22 @@
package com.foxinmy.weixin4j.tuple;
import java.util.Arrays;
import java.util.LinkedList;
import java.util.List;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlTransient;
import com.alibaba.fastjson.annotation.JSONCreator;
import com.alibaba.fastjson.annotation.JSONField;
/**
* 图文对象(mpnews消息与news消息类似,不同的是图文消息内容存储在微信后台,并且支持保密选项。每个应用每天最多可以发送100次)
* <p>
* <font color="red">可用于「群发消息(其中mediaId与articles请至少保持一个有值)」「企业号的客服消息」</font>
* <font color="red">可用于「公众平台的群发消息」「客服消息」</font>
* </p>
*
* <li>当用于发送公众平台的群发消息和客服消息时:其中mediaId与articles请至少保持一个有值 <li>
* 当用于发送企业号的客服消息时:其中articles必须有值
*
* @className MpNews
* @author jinyu(foxinmy@gmail.com)
* @date 2014年9月29日
@@ -47,16 +49,35 @@ public class MpNews implements MassTuple, NotifyTuple {
@XmlTransient
private LinkedList<MpArticle> articles;
public MpNews() {
this(null);
}
@JSONCreator
public MpNews(@JSONField(name = "mediaId") String mediaId) {
/**
* 群发消息、客服消息 预先上传List#MpArticle得到mediaId
*
* @param mediaId
* 群发素材的媒体ID
*/
public MpNews(String mediaId) {
this.mediaId = mediaId;
this.articles = new LinkedList<MpArticle>();
}
/**
* 群发消息 自动上传List#MpArticle得到mediaId
*
* @param articles
* 文章列表
*/
public MpNews(MpArticle... articles) {
this.articles = new LinkedList<MpArticle>(Arrays.asList(articles));
}
/**
* @param thumbMediaId
* 缩略图
* @param title
* 标题
* @param content
* 内容
*/
public MpNews addArticle(String thumbMediaId, String title, String content) {
return addArticle(new MpArticle(thumbMediaId, title, content));
}
@@ -9,7 +9,7 @@ import com.alibaba.fastjson.annotation.JSONField;
* <p>
* <font color="red">可用于「客服消息」</font>
* </p>
*
*
* @className Music
* @author jinyu(foxinmy@gmail.com)
* @date 2014年9月29日
@@ -55,10 +55,32 @@ public class Music implements NotifyTuple {
@XmlElement(name = "ThumbMediaId")
private String thumbMediaId;
/**
*
* @param musicUrl
* 音乐链接
* @param hqMusicUrl
* 高品质音乐链接
* @param thumbMediaId
* 缩略图
*/
public Music(String musicUrl, String hqMusicUrl, String thumbMediaId) {
this(null, null, musicUrl, hqMusicUrl, thumbMediaId);
}
/**
*
* @param title
* 标题
* @param desc
* 描述
* @param musicUrl
* 音乐链接
* @param hqMusicUrl
* 高品质音乐链接
* @param thumbMediaId
* 缩略图
*/
public Music(@JSONField(name = "title") String title,
@JSONField(name = "desc") String desc,
@JSONField(name = "musicUrl") String musicUrl,
@@ -13,7 +13,7 @@ import com.alibaba.fastjson.annotation.JSONField;
* <p>
* <font color="red">可用于「客服消息」</font>
* </p>
*
*
* @className News
* @author jinyu(foxinmy@gmail.com)
* @date 2014年11月21日
@@ -36,7 +36,7 @@ public class News implements NotifyTuple {
/**
* 图文列表
*
*
* @see com.foxinmy.weixin4j.tuple.Article
*/
@JSONField(name = "articles")
@@ -47,6 +47,17 @@ public class News implements NotifyTuple {
this.articles = new LinkedList<Article>();
}
/**
*
* @param title
* 标题
* @param desc
* 描述
* @param picUrl
* 图片链接
* @param url
* 跳转URL
*/
public News addArticle(String title, String desc, String picUrl, String url) {
return addArticle(new Article(title, desc, picUrl, url));
}
@@ -11,7 +11,7 @@ import com.alibaba.fastjson.annotation.JSONField;
* <p>
* <font color="red">可用于「客服消息」</font>
* </p>
*
*
* @className Video
* @author jinyu(foxinmy@gmail.com)
* @date 2014年9月29日
@@ -53,7 +53,7 @@ public class Video implements NotifyTuple {
/**
* 企业号的视频消息不需要缩略图
*
*
* @param mediaId
* 视频媒体文件id,可以调用上传临时素材或者永久素材接口获取
* @param title
@@ -65,6 +65,18 @@ public class Video implements NotifyTuple {
this(mediaId, null, title, desc);
}
/**
* 公众平台发送视频消息
*
* @param mediaId
* 视频媒体文件id,可以调用上传临时素材或者永久素材接口获取
* @param thumbMediaId
* 视频缩略图
* @param title
* 视频标题
* @param desc
* 视频描述
*/
@JSONCreator
public Video(@JSONField(name = "mediaId") String mediaId,
@JSONField(name = "thumbMediaId") String thumbMediaId,
@@ -0,0 +1,339 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.foxinmy.weixin4j.util;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.ObjectStreamClass;
import java.io.OutputStream;
import java.io.Serializable;
import java.util.HashMap;
import java.util.Map;
/**
* <p>Assists with the serialization process and performs additional functionality based
* on serialization.</p>
*
* <ul>
* <li>Deep clone using serialization
* <li>Serialize managing finally and IOException
* <li>Deserialize managing finally and IOException
* </ul>
*
* <p>This class throws exceptions for invalid {@code null} inputs.
* Each method documents its behaviour in more detail.</p>
*
* <p>#ThreadSafe#</p>
* @since 1.0
* @version $Id: SerializationUtils.java 1583482 2014-03-31 22:54:57Z niallp $
*/
public class SerializationUtils {
/**
* <p>SerializationUtils instances should NOT be constructed in standard programming.
* Instead, the class should be used as {@code SerializationUtils.clone(object)}.</p>
*
* <p>This constructor is public to permit tools that require a JavaBean instance
* to operate.</p>
* @since 2.0
*/
public SerializationUtils() {
super();
}
// Clone
//-----------------------------------------------------------------------
/**
* <p>Deep clone an {@code Object} using serialization.</p>
*
* <p>This is many times slower than writing clone methods by hand
* on all objects in your object graph. However, for complex object
* graphs, or for those that don't support deep cloning this can
* be a simple alternative implementation. Of course all the objects
* must be {@code Serializable}.</p>
*
* @param <T> the type of the object involved
* @param object the {@code Serializable} object to clone
* @return the cloned object
* @throws SerializationException (runtime) if the serialization fails
*/
public static <T extends Serializable> T clone(final T object) {
if (object == null) {
return null;
}
final byte[] objectData = serialize(object);
final ByteArrayInputStream bais = new ByteArrayInputStream(objectData);
ClassLoaderAwareObjectInputStream in = null;
try {
// stream closed in the finally
in = new ClassLoaderAwareObjectInputStream(bais, object.getClass().getClassLoader());
/*
* when we serialize and deserialize an object,
* it is reasonable to assume the deserialized object
* is of the same type as the original serialized object
*/
@SuppressWarnings("unchecked") // see above
final
T readObject = (T) in.readObject();
return readObject;
} catch (final ClassNotFoundException ex) {
throw new RuntimeException("ClassNotFoundException while reading cloned object data", ex);
} catch (final IOException ex) {
throw new RuntimeException("IOException while reading cloned object data", ex);
} finally {
try {
if (in != null) {
in.close();
}
} catch (final IOException ex) {
throw new RuntimeException("IOException on closing cloned object data InputStream.", ex);
}
}
}
/**
* Performs a serialization roundtrip. Serializes and deserializes the given object, great for testing objects that
* implement {@link Serializable}.
*
* @param <T>
* the type of the object involved
* @param msg
* the object to roundtrip
* @return the serialized and deseralized object
* @since 3.3
*/
public static <T extends Serializable> T roundtrip(final T msg) {
return SerializationUtils.deserialize(SerializationUtils.serialize(msg));
}
// Serialize
//-----------------------------------------------------------------------
/**
* <p>Serializes an {@code Object} to the specified stream.</p>
*
* <p>The stream will be closed once the object is written.
* This avoids the need for a finally clause, and maybe also exception
* handling, in the application code.</p>
*
* <p>The stream passed in is not buffered internally within this method.
* This is the responsibility of your application if desired.</p>
*
* @param obj the object to serialize to bytes, may be null
* @param outputStream the stream to write to, must not be null
* @throws IllegalArgumentException if {@code outputStream} is {@code null}
* @throws SerializationException (runtime) if the serialization fails
*/
public static void serialize(final Serializable obj, final OutputStream outputStream) {
if (outputStream == null) {
throw new IllegalArgumentException("The OutputStream must not be null");
}
ObjectOutputStream out = null;
try {
// stream closed in the finally
out = new ObjectOutputStream(outputStream);
out.writeObject(obj);
} catch (final IOException ex) {
throw new RuntimeException(ex);
} finally {
try {
if (out != null) {
out.close();
}
} catch (final IOException ex) { // NOPMD
// ignore close exception
}
}
}
/**
* <p>Serializes an {@code Object} to a byte array for
* storage/serialization.</p>
*
* @param obj the object to serialize to bytes
* @return a byte[] with the converted Serializable
* @throws SerializationException (runtime) if the serialization fails
*/
public static byte[] serialize(final Serializable obj) {
final ByteArrayOutputStream baos = new ByteArrayOutputStream(512);
serialize(obj, baos);
return baos.toByteArray();
}
// Deserialize
//-----------------------------------------------------------------------
/**
* <p>
* Deserializes an {@code Object} from the specified stream.
* </p>
*
* <p>
* The stream will be closed once the object is written. This avoids the need for a finally clause, and maybe also
* exception handling, in the application code.
* </p>
*
* <p>
* The stream passed in is not buffered internally within this method. This is the responsibility of your
* application if desired.
* </p>
*
* <p>
* If the call site incorrectly types the return value, a {@link ClassCastException} is thrown from the call site.
* Without Generics in this declaration, the call site must type cast and can cause the same ClassCastException.
* Note that in both cases, the ClassCastException is in the call site, not in this method.
* </p>
*
* @param <T> the object type to be deserialized
* @param inputStream
* the serialized object input stream, must not be null
* @return the deserialized object
* @throws IllegalArgumentException
* if {@code inputStream} is {@code null}
* @throws SerializationException
* (runtime) if the serialization fails
*/
public static <T> T deserialize(final InputStream inputStream) {
if (inputStream == null) {
throw new IllegalArgumentException("The InputStream must not be null");
}
ObjectInputStream in = null;
try {
// stream closed in the finally
in = new ObjectInputStream(inputStream);
@SuppressWarnings("unchecked") // may fail with CCE if serialised form is incorrect
final T obj = (T) in.readObject();
return obj;
} catch (final ClassCastException ex) {
throw new RuntimeException(ex);
} catch (final ClassNotFoundException ex) {
throw new RuntimeException(ex);
} catch (final IOException ex) {
throw new RuntimeException(ex);
} finally {
try {
if (in != null) {
in.close();
}
} catch (final IOException ex) { // NOPMD
// ignore close exception
}
}
}
/**
* <p>
* Deserializes a single {@code Object} from an array of bytes.
* </p>
*
* <p>
* If the call site incorrectly types the return value, a {@link ClassCastException} is thrown from the call site.
* Without Generics in this declaration, the call site must type cast and can cause the same ClassCastException.
* Note that in both cases, the ClassCastException is in the call site, not in this method.
* </p>
*
* @param <T> the object type to be deserialized
* @param objectData
* the serialized object, must not be null
* @return the deserialized object
* @throws IllegalArgumentException
* if {@code objectData} is {@code null}
* @throws SerializationException
* (runtime) if the serialization fails
*/
public static <T> T deserialize(final byte[] objectData) {
if (objectData == null) {
throw new IllegalArgumentException("The byte[] must not be null");
}
return SerializationUtils.<T>deserialize(new ByteArrayInputStream(objectData));
}
/**
* <p>Custom specialization of the standard JDK {@link java.io.ObjectInputStream}
* that uses a custom <code>ClassLoader</code> to resolve a class.
* If the specified <code>ClassLoader</code> is not able to resolve the class,
* the context classloader of the current thread will be used.
* This way, the standard deserialization work also in web-application
* containers and application servers, no matter in which of the
* <code>ClassLoader</code> the particular class that encapsulates
* serialization/deserialization lives. </p>
*
* <p>For more in-depth information about the problem for which this
* class here is a workaround, see the JIRA issue LANG-626. </p>
*/
static class ClassLoaderAwareObjectInputStream extends ObjectInputStream {
private static final Map<String, Class<?>> primitiveTypes =
new HashMap<String, Class<?>>();
private final ClassLoader classLoader;
/**
* Constructor.
* @param in The <code>InputStream</code>.
* @param classLoader classloader to use
* @throws IOException if an I/O error occurs while reading stream header.
* @see java.io.ObjectInputStream
*/
public ClassLoaderAwareObjectInputStream(final InputStream in, final ClassLoader classLoader) throws IOException {
super(in);
this.classLoader = classLoader;
primitiveTypes.put("byte", byte.class);
primitiveTypes.put("short", short.class);
primitiveTypes.put("int", int.class);
primitiveTypes.put("long", long.class);
primitiveTypes.put("float", float.class);
primitiveTypes.put("double", double.class);
primitiveTypes.put("boolean", boolean.class);
primitiveTypes.put("char", char.class);
primitiveTypes.put("void", void.class);
}
/**
* Overriden version that uses the parametrized <code>ClassLoader</code> or the <code>ClassLoader</code>
* of the current <code>Thread</code> to resolve the class.
* @param desc An instance of class <code>ObjectStreamClass</code>.
* @return A <code>Class</code> object corresponding to <code>desc</code>.
* @throws IOException Any of the usual Input/Output exceptions.
* @throws ClassNotFoundException If class of a serialized object cannot be found.
*/
@Override
protected Class<?> resolveClass(final ObjectStreamClass desc) throws IOException, ClassNotFoundException {
final String name = desc.getName();
try {
return Class.forName(name, false, classLoader);
} catch (final ClassNotFoundException ex) {
try {
return Class.forName(name, false, Thread.currentThread().getContextClassLoader());
} catch (final ClassNotFoundException cnfe) {
final Class<?> cls = primitiveTypes.get(name);
if (cls != null) {
return cls;
} else {
throw cnfe;
}
}
}
}
}
}
@@ -4,11 +4,12 @@ import java.util.MissingResourceException;
import java.util.ResourceBundle;
import com.alibaba.fastjson.JSON;
import com.foxinmy.weixin4j.model.Consts;
import com.foxinmy.weixin4j.model.WeixinAccount;
/**
* 公众号配置信息 class路径下weixin4j.properties文件
*
*
* @className Weixin4jConfigUtil
* @author jinyu(foxinmy@gmail.com)
* @date 2014年10月31日
@@ -23,7 +24,7 @@ public class Weixin4jConfigUtil {
CLASSPATH_VALUE = Thread.currentThread().getContextClassLoader()
.getResource("").getPath();
try {
weixinBundle = ResourceBundle.getBundle("weixin4j");
weixinBundle = ResourceBundle.getBundle(Consts.WEIXIN4J);
} catch (MissingResourceException e) {
;
}
@@ -40,7 +41,7 @@ public class Weixin4jConfigUtil {
/**
* 获取weixin4j.properties文件中的key值
*
*
* @param key
* @return
*/
@@ -51,7 +52,7 @@ public class Weixin4jConfigUtil {
/**
* key不存在时则返回传入的默认值
*
*
* @param key
* @param defaultValue
* @return
@@ -73,7 +74,7 @@ public class Weixin4jConfigUtil {
/**
* 判断属性是否存在[classpath:]如果存在则拼接项目路径后返回 一般用于文件的绝对路径获取
*
*
* @param key
* @return
*/
@@ -82,7 +83,7 @@ public class Weixin4jConfigUtil {
}
/**
*
*
* @param key
* @param defaultValue
* @return