@@ -1,219 +1,232 @@ | |||
<?xml version="1.0"?> | |||
<project | |||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd" | |||
xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> | |||
<modelVersion>4.0.0</modelVersion> | |||
<groupId>me.chanjar</groupId> | |||
<artifactId>weixin-java-parent</artifactId> | |||
<version>1.0.7-SNAPSHOT</version> | |||
<packaging>pom</packaging> | |||
<name>WeiXin Java Tools - Parent</name> | |||
<description>微信公众号、企业号上级POM</description> | |||
<url>https://github.com/chanjarster/weixin-java-tools</url> | |||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd" | |||
xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> | |||
<modelVersion>4.0.0</modelVersion> | |||
<groupId>me.chanjar</groupId> | |||
<artifactId>weixin-java-parent</artifactId> | |||
<version>1.0.7-SNAPSHOT</version> | |||
<packaging>pom</packaging> | |||
<name>WeiXin Java Tools - Parent</name> | |||
<description>微信公众号、企业号上级POM</description> | |||
<url>https://github.com/chanjarster/weixin-java-tools</url> | |||
<licenses> | |||
<license> | |||
<name>The Apache License, Version 2.0</name> | |||
<url>http://www.apache.org/licenses/LICENSE-2.0.txt</url> | |||
</license> | |||
</licenses> | |||
<licenses> | |||
<license> | |||
<name>The Apache License, Version 2.0</name> | |||
<url>http://www.apache.org/licenses/LICENSE-2.0.txt</url> | |||
</license> | |||
</licenses> | |||
<developers> | |||
<developer> | |||
<name>Daniel Qian</name> | |||
<email>chanjarster@gmail.com</email> | |||
</developer> | |||
</developers> | |||
<developers> | |||
<developer> | |||
<name>Daniel Qian</name> | |||
<email>chanjarster@gmail.com</email> | |||
</developer> | |||
</developers> | |||
<scm> | |||
<connection>scm:git:https://github.com/chanjarster/weixin-java-tools.git</connection> | |||
<developerConnection>scm:git:git@github.com:chanjarster/weixin-java-tools.git</developerConnection> | |||
<url>https://github.com/chanjarster/weixin-java-tools</url> | |||
</scm> | |||
<scm> | |||
<connection>scm:git:https://github.com/chanjarster/weixin-java-tools.git</connection> | |||
<developerConnection>scm:git:git@github.com:chanjarster/weixin-java-tools.git</developerConnection> | |||
<url>https://github.com/chanjarster/weixin-java-tools</url> | |||
</scm> | |||
<modules> | |||
<module>weixin-java-common</module> | |||
<module>weixin-java-cp</module> | |||
<module>weixin-java-mp</module> | |||
</modules> | |||
<modules> | |||
<module>weixin-java-common</module> | |||
<module>weixin-java-cp</module> | |||
<module>weixin-java-mp</module> | |||
</modules> | |||
<properties> | |||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> | |||
<downloadJavadocs>true</downloadJavadocs> | |||
<downloadSources>true</downloadSources> | |||
<httpclient.version>4.3.5</httpclient.version> | |||
<slf4j.version>1.7.10</slf4j.version> | |||
<logback.version>1.1.2</logback.version> | |||
</properties> | |||
<properties> | |||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> | |||
<downloadJavadocs>true</downloadJavadocs> | |||
<downloadSources>true</downloadSources> | |||
<httpclient.version>4.3.5</httpclient.version> | |||
</properties> | |||
<dependencies> | |||
<dependency> | |||
<groupId>org.slf4j</groupId> | |||
<artifactId>slf4j-api</artifactId> | |||
<version>${slf4j.version}</version> | |||
</dependency> | |||
<dependency> | |||
<groupId>ch.qos.logback</groupId> | |||
<artifactId>logback-classic</artifactId> | |||
<version>${logback.version}</version> | |||
<scope>test</scope> | |||
</dependency> | |||
<dependency> | |||
<groupId>org.apache.httpcomponents</groupId> | |||
<artifactId>fluent-hc</artifactId> | |||
<version>${httpclient.version}</version> | |||
</dependency> | |||
<dependency> | |||
<groupId>org.apache.httpcomponents</groupId> | |||
<artifactId>httpmime</artifactId> | |||
<version>${httpclient.version}</version> | |||
</dependency> | |||
<dependency> | |||
<groupId>com.google.code.gson</groupId> | |||
<artifactId>gson</artifactId> | |||
<version>2.2.2</version> | |||
</dependency> | |||
<dependency> | |||
<groupId>commons-codec</groupId> | |||
<artifactId>commons-codec</artifactId> | |||
<version>1.9</version> | |||
</dependency> | |||
<dependency> | |||
<groupId>commons-io</groupId> | |||
<artifactId>commons-io</artifactId> | |||
<version>2.4</version> | |||
</dependency> | |||
</dependencies> | |||
<dependencyManagement> | |||
<dependencies> | |||
<dependency> | |||
<groupId>org.apache.httpcomponents</groupId> | |||
<artifactId>fluent-hc</artifactId> | |||
<version>${httpclient.version}</version> | |||
</dependency> | |||
<dependency> | |||
<groupId>org.apache.httpcomponents</groupId> | |||
<artifactId>httpmime</artifactId> | |||
<version>${httpclient.version}</version> | |||
</dependency> | |||
<dependency> | |||
<groupId>com.google.code.gson</groupId> | |||
<artifactId>gson</artifactId> | |||
<version>2.2.2</version> | |||
</dependency> | |||
<dependency> | |||
<groupId>commons-codec</groupId> | |||
<artifactId>commons-codec</artifactId> | |||
<version>1.9</version> | |||
</dependency> | |||
<dependency> | |||
<groupId>commons-io</groupId> | |||
<artifactId>commons-io</artifactId> | |||
<version>2.4</version> | |||
</dependency> | |||
<dependency> | |||
<groupId>junit</groupId> | |||
<artifactId>junit</artifactId> | |||
<version>4.11</version> | |||
<scope>test</scope> | |||
</dependency> | |||
<dependency> | |||
<groupId>com.google.inject</groupId> | |||
<artifactId>guice</artifactId> | |||
<version>3.0</version> | |||
<scope>test</scope> | |||
</dependency> | |||
<dependency> | |||
<groupId>org.testng</groupId> | |||
<artifactId>testng</artifactId> | |||
<version>6.8.7</version> | |||
<scope>test</scope> | |||
</dependency> | |||
<dependency> | |||
<groupId>org.mockito</groupId> | |||
<artifactId>mockito-all</artifactId> | |||
<version>1.9.5</version> | |||
<scope>test</scope> | |||
</dependency> | |||
<dependency> | |||
<groupId>org.eclipse.jetty</groupId> | |||
<artifactId>jetty-server</artifactId> | |||
<version>9.3.0.M0</version> | |||
<scope>test</scope> | |||
</dependency> | |||
<dependency> | |||
<groupId>org.eclipse.jetty</groupId> | |||
<artifactId>jetty-servlet</artifactId> | |||
<version>9.3.0.M0</version> | |||
<scope>test</scope> | |||
</dependency> | |||
</dependencies> | |||
</dependencyManagement> | |||
<dependencyManagement> | |||
<dependencies> | |||
<dependency> | |||
<groupId>junit</groupId> | |||
<artifactId>junit</artifactId> | |||
<version>4.11</version> | |||
<scope>test</scope> | |||
</dependency> | |||
<dependency> | |||
<groupId>com.google.inject</groupId> | |||
<artifactId>guice</artifactId> | |||
<version>3.0</version> | |||
<scope>test</scope> | |||
</dependency> | |||
<dependency> | |||
<groupId>org.testng</groupId> | |||
<artifactId>testng</artifactId> | |||
<version>6.8.7</version> | |||
<scope>test</scope> | |||
</dependency> | |||
<dependency> | |||
<groupId>org.mockito</groupId> | |||
<artifactId>mockito-all</artifactId> | |||
<version>1.9.5</version> | |||
<scope>test</scope> | |||
</dependency> | |||
<dependency> | |||
<groupId>org.eclipse.jetty</groupId> | |||
<artifactId>jetty-server</artifactId> | |||
<version>9.3.0.M0</version> | |||
<scope>test</scope> | |||
</dependency> | |||
<dependency> | |||
<groupId>org.eclipse.jetty</groupId> | |||
<artifactId>jetty-servlet</artifactId> | |||
<version>9.3.0.M0</version> | |||
<scope>test</scope> | |||
</dependency> | |||
</dependencies> | |||
</dependencyManagement> | |||
<distributionManagement> | |||
<snapshotRepository> | |||
<id>ossrh</id> | |||
<url>https://oss.sonatype.org/content/repositories/snapshots</url> | |||
</snapshotRepository> | |||
<repository> | |||
<id>ossrh</id> | |||
<url>https://oss.sonatype.org/service/local/staging/deploy/maven2/</url> | |||
</repository> | |||
</distributionManagement> | |||
<profiles> | |||
<profile> | |||
<id>release</id> | |||
<build> | |||
<plugins> | |||
<plugin> | |||
<groupId>org.apache.maven.plugins</groupId> | |||
<artifactId>maven-source-plugin</artifactId> | |||
<version>2.2.1</version> | |||
<executions> | |||
<execution> | |||
<id>attach-sources</id> | |||
<goals> | |||
<goal>jar-no-fork</goal> | |||
</goals> | |||
</execution> | |||
</executions> | |||
</plugin> | |||
<plugin> | |||
<groupId>org.apache.maven.plugins</groupId> | |||
<artifactId>maven-javadoc-plugin</artifactId> | |||
<version>2.9.1</version> | |||
<executions> | |||
<execution> | |||
<id>attach-javadocs</id> | |||
<goals> | |||
<goal>jar</goal> | |||
</goals> | |||
</execution> | |||
</executions> | |||
<configuration> | |||
<charset>UTF-8</charset> | |||
<locale>zh_CN</locale> | |||
</configuration> | |||
</plugin> | |||
<plugin> | |||
<groupId>org.apache.maven.plugins</groupId> | |||
<artifactId>maven-gpg-plugin</artifactId> | |||
<version>1.5</version> | |||
<executions> | |||
<execution> | |||
<id>sign-artifacts</id> | |||
<phase>verify</phase> | |||
<goals> | |||
<goal>sign</goal> | |||
</goals> | |||
</execution> | |||
</executions> | |||
</plugin> | |||
</plugins> | |||
</build> | |||
</profile> | |||
</profiles> | |||
<build> | |||
<pluginManagement> | |||
<plugins> | |||
<plugin> | |||
<groupId>org.apache.maven.plugins</groupId> | |||
<artifactId>maven-surefire-plugin</artifactId> | |||
<version>2.17</version> | |||
</plugin> | |||
</plugins> | |||
</pluginManagement> | |||
<distributionManagement> | |||
<snapshotRepository> | |||
<id>ossrh</id> | |||
<url>https://oss.sonatype.org/content/repositories/snapshots</url> | |||
</snapshotRepository> | |||
<repository> | |||
<id>ossrh</id> | |||
<url>https://oss.sonatype.org/service/local/staging/deploy/maven2/</url> | |||
</repository> | |||
</distributionManagement> | |||
<profiles> | |||
<profile> | |||
<id>release</id> | |||
<build> | |||
<plugins> | |||
<plugin> | |||
<groupId>org.sonatype.plugins</groupId> | |||
<artifactId>nexus-staging-maven-plugin</artifactId> | |||
<version>1.6.3</version> | |||
<extensions>true</extensions> | |||
<configuration> | |||
<serverId>ossrh</serverId> | |||
<nexusUrl>https://oss.sonatype.org/</nexusUrl> | |||
<autoReleaseAfterClose>false</autoReleaseAfterClose> | |||
</configuration> | |||
</plugin> | |||
<plugin> | |||
<groupId>org.apache.maven.plugins</groupId> | |||
<artifactId>maven-release-plugin</artifactId> | |||
<version>2.5</version> | |||
<configuration> | |||
<autoVersionSubmodules>true</autoVersionSubmodules> | |||
<useReleaseProfile>false</useReleaseProfile> | |||
<releaseProfiles>release</releaseProfiles> | |||
<goals>deploy</goals> | |||
</configuration> | |||
</plugin> | |||
<plugin> | |||
<groupId>org.apache.maven.plugins</groupId> | |||
<artifactId>maven-source-plugin</artifactId> | |||
<version>2.2.1</version> | |||
<executions> | |||
<execution> | |||
<id>attach-sources</id> | |||
<goals> | |||
<goal>jar-no-fork</goal> | |||
</goals> | |||
</execution> | |||
</executions> | |||
</plugin> | |||
<plugin> | |||
<groupId>org.apache.maven.plugins</groupId> | |||
<artifactId>maven-javadoc-plugin</artifactId> | |||
<version>2.9.1</version> | |||
<executions> | |||
<execution> | |||
<id>attach-javadocs</id> | |||
<goals> | |||
<goal>jar</goal> | |||
</goals> | |||
</execution> | |||
</executions> | |||
<configuration> | |||
<charset>UTF-8</charset> | |||
<locale>zh_CN</locale> | |||
</configuration> | |||
</plugin> | |||
<plugin> | |||
<groupId>org.apache.maven.plugins</groupId> | |||
<artifactId>maven-gpg-plugin</artifactId> | |||
<version>1.5</version> | |||
<executions> | |||
<execution> | |||
<id>sign-artifacts</id> | |||
<phase>verify</phase> | |||
<goals> | |||
<goal>sign</goal> | |||
</goals> | |||
</execution> | |||
</executions> | |||
</plugin> | |||
</plugins> | |||
</build> | |||
</build> | |||
</profile> | |||
</profiles> | |||
<build> | |||
<pluginManagement> | |||
<plugins> | |||
<plugin> | |||
<groupId>org.apache.maven.plugins</groupId> | |||
<artifactId>maven-surefire-plugin</artifactId> | |||
<version>2.17</version> | |||
</plugin> | |||
</plugins> | |||
</pluginManagement> | |||
<plugins> | |||
<plugin> | |||
<groupId>org.sonatype.plugins</groupId> | |||
<artifactId>nexus-staging-maven-plugin</artifactId> | |||
<version>1.6.3</version> | |||
<extensions>true</extensions> | |||
<configuration> | |||
<serverId>ossrh</serverId> | |||
<nexusUrl>https://oss.sonatype.org/</nexusUrl> | |||
<autoReleaseAfterClose>false</autoReleaseAfterClose> | |||
</configuration> | |||
</plugin> | |||
<plugin> | |||
<groupId>org.apache.maven.plugins</groupId> | |||
<artifactId>maven-release-plugin</artifactId> | |||
<version>2.5</version> | |||
<configuration> | |||
<autoVersionSubmodules>true</autoVersionSubmodules> | |||
<useReleaseProfile>false</useReleaseProfile> | |||
<releaseProfiles>release</releaseProfiles> | |||
<goals>deploy</goals> | |||
</configuration> | |||
</plugin> | |||
</plugins> | |||
</build> | |||
</project> |
@@ -0,0 +1,31 @@ | |||
/* | |||
* 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 me.chanjar.weixin.common.session; | |||
/** | |||
* Manifest constants for the <code>org.apache.catalina.session</code> | |||
* package. | |||
* | |||
* @author Craig R. McClanahan | |||
*/ | |||
public class Constants { | |||
public static final String Package = "me.chanjar.weixin.common.session"; | |||
} |
@@ -0,0 +1,68 @@ | |||
package me.chanjar.weixin.common.session; | |||
public interface InternalSession { | |||
/** | |||
* Return the <code>HttpSession</code> for which this object | |||
* is the facade. | |||
*/ | |||
WxSession getSession(); | |||
/** | |||
* Set the <code>isValid</code> flag for this session. | |||
* | |||
* @param isValid The new value for the <code>isValid</code> flag | |||
*/ | |||
public void setValid(boolean isValid); | |||
/** | |||
* Return the <code>isValid</code> flag for this session. | |||
*/ | |||
boolean isValid(); | |||
/** | |||
* Return the session identifier for this session. | |||
*/ | |||
String getIdInternal(); | |||
/** | |||
* Perform the internal processing required to invalidate this session, | |||
* without triggering an exception if the session has already expired. | |||
*/ | |||
void expire(); | |||
/** | |||
* Update the accessed time information for this session. This method | |||
* should be called by the context when a request comes in for a particular | |||
* session, even if the application does not reference it. | |||
*/ | |||
void access(); | |||
/** | |||
* End the access. | |||
*/ | |||
void endAccess(); | |||
/** | |||
* Set the creation time for this session. This method is called by the | |||
* Manager when an existing Session instance is reused. | |||
* | |||
* @param time The new creation time | |||
*/ | |||
void setCreationTime(long time); | |||
/** | |||
* Set the default maximum inactive interval (in seconds) | |||
* for Sessions created by this Manager. | |||
* | |||
* @param interval The new default value | |||
*/ | |||
void setMaxInactiveInterval(int interval); | |||
/** | |||
* Set the session identifier for this session. | |||
* | |||
* @param id The new session identifier | |||
*/ | |||
void setId(String id); | |||
} |
@@ -0,0 +1,113 @@ | |||
package me.chanjar.weixin.common.session; | |||
public interface InternalSessionManager { | |||
/** | |||
* Return the active Session, associated with this Manager, with the | |||
* specified session id (if any); otherwise return <code>null</code>. | |||
* | |||
* @param id The session id for the session to be returned | |||
* | |||
* @exception IllegalStateException if a new session cannot be | |||
* instantiated for any reason | |||
* @exception java.io.IOException if an input/output error occurs while | |||
* processing this request | |||
*/ | |||
InternalSession findSession(String id); | |||
/** | |||
* Construct and return a new session object, based on the default | |||
* settings specified by this Manager's properties. The session | |||
* id specified will be used as the session id. | |||
* If a new session cannot be created for any reason, return | |||
* <code>null</code>. | |||
* | |||
* @param sessionId The session id which should be used to create the | |||
* new session; if <code>null</code>, a new session id will be | |||
* generated | |||
* @exception IllegalStateException if a new session cannot be | |||
* instantiated for any reason | |||
*/ | |||
public InternalSession createSession(String sessionId); | |||
/** | |||
* Remove this Session from the active Sessions for this Manager. | |||
* | |||
* @param session Session to be removed | |||
*/ | |||
public void remove(InternalSession session); | |||
/** | |||
* Remove this Session from the active Sessions for this Manager. | |||
* | |||
* @param session Session to be removed | |||
* @param update Should the expiration statistics be updated | |||
*/ | |||
public void remove(InternalSession session, boolean update); | |||
/** | |||
* Add this Session to the set of active Sessions for this Manager. | |||
* | |||
* @param session Session to be added | |||
*/ | |||
void add(InternalSession session); | |||
/** | |||
* Returns the number of active sessions | |||
* | |||
* @return number of sessions active | |||
*/ | |||
int getActiveSessions(); | |||
/** | |||
* Get a session from the recycled ones or create a new empty one. | |||
* The PersistentManager manager does not need to create session data | |||
* because it reads it from the Store. | |||
*/ | |||
InternalSession createEmptySession(); | |||
InternalSession[] findSessions(); | |||
/** | |||
* Implements the Manager interface, direct call to processExpires | |||
*/ | |||
public void backgroundProcess(); | |||
/** | |||
* Set the default maximum inactive interval (in seconds) | |||
* for Sessions created by this Manager. | |||
* | |||
* @param interval The new default value | |||
*/ | |||
void setMaxInactiveInterval(int interval); | |||
/** | |||
* <pre> | |||
* Set the manager checks frequency. | |||
* 设置每尝试多少次清理过期session,才真的会执行一次清理动作 | |||
* 要和{@link #setBackgroundProcessorDelay(int)}联合起来看 | |||
* 如果把这个数字设置为6(默认),那么就是说manager要等待 6 * backgroundProcessorDay的时间才会清理过期session | |||
* </pre> | |||
* @param processExpiresFrequency the new manager checks frequency | |||
*/ | |||
void setProcessExpiresFrequency(int processExpiresFrequency); | |||
/** | |||
* <pre> | |||
* Set the manager background processor delay | |||
* 设置manager sleep几秒,尝试执行一次background操作(清理过期session) | |||
* </pre> | |||
* @param backgroundProcessorDelay | |||
*/ | |||
void setBackgroundProcessorDelay(int backgroundProcessorDelay); | |||
/** | |||
* Set the maximum number of active Sessions allowed, or -1 for | |||
* no limit. | |||
* 设置最大活跃session数,默认无限 | |||
* @param max The new maximum number of sessions | |||
*/ | |||
void setMaxActiveSessions(int max); | |||
} |
@@ -0,0 +1,80 @@ | |||
# 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. | |||
applicationSession.session.ise=invalid session state | |||
applicationSession.value.iae=null value | |||
fileStore.saving=Saving Session {0} to file {1} | |||
fileStore.loading=Loading Session {0} from file {1} | |||
fileStore.removing=Removing Session {0} at file {1} | |||
fileStore.deleteFailed=Unable to delete file [{0}] which is preventing the creation of the session storage location | |||
fileStore.createFailed=Unable to create directory [{0}] for the storage of session data | |||
JDBCStore.close=Exception closing database connection {0} | |||
JDBCStore.saving=Saving Session {0} to database {1} | |||
JDBCStore.loading=Loading Session {0} from database {1} | |||
JDBCStore.removing=Removing Session {0} at database {1} | |||
JDBCStore.SQLException=SQL Error {0} | |||
serverSession.value.iae=null value | |||
sessionManagerImpl.createRandom=Created random number generator for session ID generation in {0}ms. | |||
sessionManagerImpl.createSession.tmase=createSession: Too many active sessions | |||
sessionManagerImpl.sessionTimeout=Invalid session timeout setting {0} | |||
sessionManagerImpl.getSession.ise=getSession: Session id cannot be null | |||
sessionManagerImpl.expireException=processsExpire: Exception during session expiration | |||
sessionManagerImpl.loading=Loading persisted sessions from {0} | |||
sessionManagerImpl.loading.cnfe=ClassNotFoundException while loading persisted sessions: {0} | |||
sessionManagerImpl.loading.ioe=IOException while loading persisted sessions: {0} | |||
sessionManagerImpl.unloading=Saving persisted sessions to {0} | |||
sessionManagerImpl.unloading.debug=Unloading persisted sessions | |||
sessionManagerImpl.unloading.ioe=IOException while saving persisted sessions: {0} | |||
sessionManagerImpl.unloading.nosessions=No persisted sessions to unload | |||
sessionManagerImpl.managerLoad=Exception loading sessions from persistent storage | |||
sessionManagerImpl.managerUnload=Exception unloading sessions to persistent storage | |||
sessionManagerImpl.createSession.ise=createSession: Session id cannot be null | |||
sessionImpl.attributeEvent=Session attribute event listener threw exception | |||
sessionImpl.bindingEvent=Session binding event listener threw exception | |||
sessionImpl.invalidate.ise=invalidate: Session already invalidated | |||
sessionImpl.isNew.ise=isNew: Session already invalidated | |||
sessionImpl.getAttribute.ise=getAttribute: Session already invalidated | |||
sessionImpl.getAttributeNames.ise=getAttributeNames: Session already invalidated | |||
sessionImpl.getCreationTime.ise=getCreationTime: Session already invalidated | |||
sessionImpl.getThisAccessedTime.ise=getThisAccessedTime: Session already invalidated | |||
sessionImpl.getLastAccessedTime.ise=getLastAccessedTime: Session already invalidated | |||
sessionImpl.getId.ise=getId: Session already invalidated | |||
sessionImpl.getMaxInactiveInterval.ise=getMaxInactiveInterval: Session already invalidated | |||
sessionImpl.getValueNames.ise=getValueNames: Session already invalidated | |||
sessionImpl.logoutfail=Exception logging out user when expiring session | |||
sessionImpl.notSerializable=Cannot serialize session attribute {0} for session {1} | |||
sessionImpl.removeAttribute.ise=removeAttribute: Session already invalidated | |||
sessionImpl.sessionEvent=Session event listener threw exception | |||
sessionImpl.setAttribute.iae=setAttribute: Non-serializable attribute {0} | |||
sessionImpl.setAttribute.ise=setAttribute: Session [{0}] has already been invalidated | |||
sessionImpl.setAttribute.namenull=setAttribute: name parameter cannot be null | |||
sessionImpl.sessionCreated=Created Session id = {0} | |||
persistentManager.loading=Loading {0} persisted sessions | |||
persistentManager.unloading=Saving {0} persisted sessions | |||
persistentManager.expiring=Expiring {0} sessions before saving them | |||
persistentManager.deserializeError=Error deserializing Session {0}: {1} | |||
persistentManager.serializeError=Error serializing Session {0}: {1} | |||
persistentManager.swapMaxIdle=Swapping session {0} to Store, idle for {1} seconds | |||
persistentManager.backupMaxIdle=Backing up session {0} to Store, idle for {1} seconds | |||
persistentManager.backupException=Exception occurred when backing up Session {0}: {1} | |||
persistentManager.tooManyActive=Too many active sessions, {0}, looking for idle sessions to swap out | |||
persistentManager.swapTooManyActive=Swapping out session {0}, idle for {1} seconds too many sessions active | |||
persistentManager.processSwaps=Checking for sessions to swap out, {0} active sessions in memory | |||
persistentManager.activeSession=Session {0} has been idle for {1} seconds | |||
persistentManager.swapIn=Swapping session {0} in from Store | |||
persistentManager.swapInException=Exception in the Store during swapIn: {0} | |||
persistentManager.swapInInvalid=Swapped session {0} is invalid | |||
persistentManager.storeKeysException=Unable to determine the list of session IDs for sessions in the session store, assuming that the store is empty | |||
persistentManager.storeSizeException=Unable to determine the number of sessions in the session store, assuming that the store is empty |
@@ -0,0 +1,346 @@ | |||
package me.chanjar.weixin.common.session; | |||
import me.chanjar.weixin.common.util.res.StringManager; | |||
import java.util.*; | |||
import java.util.concurrent.ConcurrentHashMap; | |||
import java.util.concurrent.atomic.AtomicInteger; | |||
public class StandardSession implements WxSession, InternalSession { | |||
/** | |||
* The string manager for this package. | |||
*/ | |||
protected static final StringManager sm = | |||
StringManager.getManager(Constants.Package); | |||
// ------------------------------ WxSession | |||
protected Map<String, Object> attributes = new ConcurrentHashMap<String, Object>(); | |||
@Override | |||
public Object getAttribute(String name) { | |||
if (!isValidInternal()) | |||
throw new IllegalStateException | |||
(sm.getString("sessionImpl.getAttribute.ise")); | |||
if (name == null) return null; | |||
return (attributes.get(name)); | |||
} | |||
@Override | |||
public Enumeration<String> getAttributeNames() { | |||
if (!isValidInternal()) | |||
throw new IllegalStateException | |||
(sm.getString("sessionImpl.getAttributeNames.ise")); | |||
Set<String> names = new HashSet<String>(); | |||
names.addAll(attributes.keySet()); | |||
return Collections.enumeration(names); | |||
} | |||
@Override | |||
public void setAttribute(String name, Object value) { | |||
// Name cannot be null | |||
if (name == null) | |||
throw new IllegalArgumentException | |||
(sm.getString("sessionImpl.setAttribute.namenull")); | |||
// Null value is the same as removeAttribute() | |||
if (value == null) { | |||
removeAttribute(name); | |||
return; | |||
} | |||
// Validate our current state | |||
if (!isValidInternal()) | |||
throw new IllegalStateException(sm.getString( | |||
"sessionImpl.setAttribute.ise", getIdInternal())); | |||
attributes.put(name, value); | |||
} | |||
@Override | |||
public void removeAttribute(String name) { | |||
removeAttributeInternal(name); | |||
} | |||
@Override | |||
public void invalidate() { | |||
if (!isValidInternal()) | |||
throw new IllegalStateException | |||
(sm.getString("sessionImpl.invalidate.ise")); | |||
// Cause this session to expire | |||
expire(); | |||
} | |||
// ------------------------------ InternalSession | |||
/** | |||
* The session identifier of this Session. | |||
*/ | |||
protected String id = null; | |||
/** | |||
* Flag indicating whether this session is valid or not. | |||
*/ | |||
protected volatile boolean isValid = false; | |||
/** | |||
* We are currently processing a session expiration, so bypass | |||
* certain IllegalStateException tests. NOTE: This value is not | |||
* included in the serialized version of this object. | |||
*/ | |||
protected transient volatile boolean expiring = false; | |||
/** | |||
* The Manager with which this Session is associated. | |||
*/ | |||
protected transient InternalSessionManager manager = null; | |||
/** | |||
* Type array. | |||
*/ | |||
protected static final String EMPTY_ARRAY[] = new String[0]; | |||
/** | |||
* The time this session was created, in milliseconds since midnight, | |||
* January 1, 1970 GMT. | |||
*/ | |||
protected long creationTime = 0L; | |||
/** | |||
* The current accessed time for this session. | |||
*/ | |||
protected volatile long thisAccessedTime = creationTime; | |||
/** | |||
* The default maximum inactive interval for Sessions created by | |||
* this Manager. | |||
*/ | |||
protected int maxInactiveInterval = 30 * 60; | |||
/** | |||
* The facade associated with this session. NOTE: This value is not | |||
* included in the serialized version of this object. | |||
*/ | |||
protected transient StandardSessionFacade facade = null; | |||
/** | |||
* The access count for this session. | |||
*/ | |||
protected transient AtomicInteger accessCount = null; | |||
public StandardSession(InternalSessionManager manager) { | |||
this.manager = manager; | |||
this.accessCount = new AtomicInteger(); | |||
} | |||
@Override | |||
public WxSession getSession() { | |||
if (facade == null){ | |||
facade = new StandardSessionFacade(this); | |||
} | |||
return (facade); | |||
} | |||
/** | |||
* Return the <code>isValid</code> flag for this session without any expiration | |||
* check. | |||
*/ | |||
protected boolean isValidInternal() { | |||
return this.isValid; | |||
} | |||
/** | |||
* Set the <code>isValid</code> flag for this session. | |||
* | |||
* @param isValid The new value for the <code>isValid</code> flag | |||
*/ | |||
@Override | |||
public void setValid(boolean isValid) { | |||
this.isValid = isValid; | |||
} | |||
@Override | |||
public boolean isValid() { | |||
if (!this.isValid) { | |||
return false; | |||
} | |||
if (this.expiring) { | |||
return true; | |||
} | |||
if (accessCount.get() > 0) { | |||
return true; | |||
} | |||
if (maxInactiveInterval > 0) { | |||
long timeNow = System.currentTimeMillis(); | |||
int timeIdle; | |||
timeIdle = (int) ((timeNow - thisAccessedTime) / 1000L); | |||
if (timeIdle >= maxInactiveInterval) { | |||
expire(); | |||
} | |||
} | |||
return this.isValid; | |||
} | |||
@Override | |||
public String getIdInternal() { | |||
return (this.id); | |||
} | |||
protected void removeAttributeInternal(String name) { | |||
// Avoid NPE | |||
if (name == null) return; | |||
// Remove this attribute from our collection | |||
attributes.remove(name); | |||
} | |||
@Override | |||
public void expire() { | |||
// Check to see if session has already been invalidated. | |||
// Do not check expiring at this point as expire should not return until | |||
// isValid is false | |||
if (!isValid) | |||
return; | |||
synchronized (this) { | |||
// Check again, now we are inside the sync so this code only runs once | |||
// Double check locking - isValid needs to be volatile | |||
// The check of expiring is to ensure that an infinite loop is not | |||
// entered as per bug 56339 | |||
if (expiring || !isValid) | |||
return; | |||
if (manager == null) | |||
return; | |||
// Mark this session as "being expired" | |||
expiring = true; | |||
accessCount.set(0); | |||
// Remove this session from our manager's active sessions | |||
manager.remove(this, true); | |||
// We have completed expire of this session | |||
setValid(false); | |||
expiring = false; | |||
// Unbind any objects associated with this session | |||
String keys[] = keys(); | |||
for (int i = 0; i < keys.length; i++) { | |||
removeAttributeInternal(keys[i]); | |||
} | |||
} | |||
} | |||
@Override | |||
public void access() { | |||
this.thisAccessedTime = System.currentTimeMillis(); | |||
accessCount.incrementAndGet(); | |||
} | |||
@Override | |||
public void endAccess() { | |||
this.thisAccessedTime = System.currentTimeMillis(); | |||
accessCount.decrementAndGet(); | |||
} | |||
@Override | |||
public void setCreationTime(long time) { | |||
this.creationTime = time; | |||
this.thisAccessedTime = time; | |||
} | |||
@Override | |||
public void setMaxInactiveInterval(int interval) { | |||
this.maxInactiveInterval = interval; | |||
} | |||
@Override | |||
public void setId(String id) { | |||
if ((this.id != null) && (manager != null)) | |||
manager.remove(this); | |||
this.id = id; | |||
if (manager != null) | |||
manager.add(this); | |||
} | |||
/** | |||
* Return the names of all currently defined session attributes | |||
* as an array of Strings. If there are no defined attributes, a | |||
* zero-length array is returned. | |||
*/ | |||
protected String[] keys() { | |||
return attributes.keySet().toArray(EMPTY_ARRAY); | |||
} | |||
@Override | |||
public boolean equals(Object o) { | |||
if (this == o) return true; | |||
if (!(o instanceof StandardSession)) return false; | |||
StandardSession session = (StandardSession) o; | |||
if (creationTime != session.creationTime) return false; | |||
if (expiring != session.expiring) return false; | |||
if (isValid != session.isValid) return false; | |||
if (maxInactiveInterval != session.maxInactiveInterval) return false; | |||
if (thisAccessedTime != session.thisAccessedTime) return false; | |||
if (!accessCount.equals(session.accessCount)) return false; | |||
if (!attributes.equals(session.attributes)) return false; | |||
if (!facade.equals(session.facade)) return false; | |||
if (!id.equals(session.id)) return false; | |||
if (!manager.equals(session.manager)) return false; | |||
return true; | |||
} | |||
@Override | |||
public int hashCode() { | |||
int result = attributes.hashCode(); | |||
result = 31 * result + id.hashCode(); | |||
result = 31 * result + (isValid ? 1 : 0); | |||
result = 31 * result + (expiring ? 1 : 0); | |||
result = 31 * result + manager.hashCode(); | |||
result = 31 * result + (int) (creationTime ^ (creationTime >>> 32)); | |||
result = 31 * result + (int) (thisAccessedTime ^ (thisAccessedTime >>> 32)); | |||
result = 31 * result + maxInactiveInterval; | |||
result = 31 * result + facade.hashCode(); | |||
result = 31 * result + accessCount.hashCode(); | |||
return result; | |||
} | |||
} |
@@ -0,0 +1,45 @@ | |||
package me.chanjar.weixin.common.session; | |||
import java.util.Enumeration; | |||
public class StandardSessionFacade implements WxSession { | |||
/** | |||
* Wrapped session object. | |||
*/ | |||
private WxSession session = null; | |||
public StandardSessionFacade(StandardSession session) { | |||
this.session = session; | |||
} | |||
public InternalSession getInternalSession() { | |||
return (InternalSession) session; | |||
} | |||
@Override | |||
public Object getAttribute(String name) { | |||
return session.getAttribute(name); | |||
} | |||
@Override | |||
public Enumeration<String> getAttributeNames() { | |||
return session.getAttributeNames(); | |||
} | |||
@Override | |||
public void setAttribute(String name, Object value) { | |||
session.setAttribute(name, value); | |||
} | |||
@Override | |||
public void removeAttribute(String name) { | |||
session.removeAttribute(name); | |||
} | |||
@Override | |||
public void invalidate() { | |||
session.invalidate(); | |||
} | |||
} |
@@ -0,0 +1,321 @@ | |||
package me.chanjar.weixin.common.session; | |||
import me.chanjar.weixin.common.util.res.StringManager; | |||
import org.slf4j.Logger; | |||
import org.slf4j.LoggerFactory; | |||
import java.util.Map; | |||
import java.util.concurrent.ConcurrentHashMap; | |||
import java.util.concurrent.atomic.AtomicBoolean; | |||
/** | |||
* 基于内存的session manager | |||
*/ | |||
public class StandardSessionManager implements WxSessionManager, InternalSessionManager { | |||
protected final Logger log = LoggerFactory.getLogger(StandardSessionManager.class); | |||
protected static final StringManager sm = | |||
StringManager.getManager(Constants.Package); | |||
/** | |||
* The set of currently active Sessions for this Manager, keyed by | |||
* session identifier. | |||
*/ | |||
protected Map<String, InternalSession> sessions = new ConcurrentHashMap<String, InternalSession>(); | |||
@Override | |||
public WxSession getSession(String sessionId) { | |||
return getSession(sessionId, true); | |||
} | |||
@Override | |||
public WxSession getSession(String sessionId, boolean create) { | |||
if (sessionId == null) { | |||
throw new IllegalStateException | |||
(sm.getString("sessionManagerImpl.getSession.ise")); | |||
} | |||
InternalSession session = findSession(sessionId); | |||
if ((session != null) && !session.isValid()) { | |||
session = null; | |||
} | |||
if (session != null) { | |||
session.access(); | |||
return session.getSession(); | |||
} | |||
// Create a new session if requested and the response is not committed | |||
if (!create) { | |||
return (null); | |||
} | |||
session = createSession(sessionId); | |||
if (session == null) { | |||
return null; | |||
} | |||
session.access(); | |||
return session.getSession(); | |||
} | |||
// -------------------------------------- InternalSessionManager | |||
/** | |||
* The descriptive name of this Manager implementation (for logging). | |||
*/ | |||
private static final String name = "SessionManagerImpl"; | |||
/** | |||
* The maximum number of active Sessions allowed, or -1 for no limit. | |||
*/ | |||
protected int maxActiveSessions = -1; | |||
/** | |||
* Number of session creations that failed due to maxActiveSessions. | |||
*/ | |||
protected int rejectedSessions = 0; | |||
/** | |||
* The default maximum inactive interval for Sessions created by | |||
* this Manager. | |||
*/ | |||
protected int maxInactiveInterval = 30 * 60; | |||
// Number of sessions created by this manager | |||
protected long sessionCounter=0; | |||
protected volatile int maxActive=0; | |||
private final Object maxActiveUpdateLock = new Object(); | |||
/** | |||
* Processing time during session expiration. | |||
*/ | |||
protected long processingTime = 0; | |||
/** | |||
* Iteration count for background processing. | |||
*/ | |||
private int count = 0; | |||
/** | |||
* Frequency of the session expiration, and related manager operations. | |||
* Manager operations will be done once for the specified amount of | |||
* backgrondProcess calls (ie, the lower the amount, the most often the | |||
* checks will occur). | |||
*/ | |||
protected int processExpiresFrequency = 6; | |||
/** | |||
* background processor delay in seconds | |||
*/ | |||
protected int backgroundProcessorDelay = 10; | |||
/** | |||
* 后台清理线程是否已经开启 | |||
*/ | |||
private final AtomicBoolean backgroundProcessStarted = new AtomicBoolean(false); | |||
@Override | |||
public void remove(InternalSession session) { | |||
remove(session, false); | |||
} | |||
@Override | |||
public void remove(InternalSession session, boolean update) { | |||
if (session.getIdInternal() != null) { | |||
sessions.remove(session.getIdInternal()); | |||
} | |||
} | |||
@Override | |||
public InternalSession findSession(String id) { | |||
if (id == null) | |||
return (null); | |||
return sessions.get(id); | |||
} | |||
@Override | |||
public InternalSession createSession(String sessionId) { | |||
if (sessionId == null) { | |||
throw new IllegalStateException | |||
(sm.getString("sessionManagerImpl.createSession.ise")); | |||
} | |||
if ((maxActiveSessions >= 0) && | |||
(getActiveSessions() >= maxActiveSessions)) { | |||
rejectedSessions++; | |||
throw new TooManyActiveSessionsException( | |||
sm.getString("sessionManagerImpl.createSession.tmase"), | |||
maxActiveSessions); | |||
} | |||
// Recycle or create a Session instance | |||
InternalSession session = createEmptySession(); | |||
// Initialize the properties of the new session and return it | |||
session.setValid(true); | |||
session.setCreationTime(System.currentTimeMillis()); | |||
session.setMaxInactiveInterval(this.maxInactiveInterval); | |||
String id = sessionId; | |||
session.setId(id); | |||
sessionCounter++; | |||
return (session); | |||
} | |||
@Override | |||
public int getActiveSessions() { | |||
return sessions.size(); | |||
} | |||
@Override | |||
public InternalSession createEmptySession() { | |||
return (getNewSession()); | |||
} | |||
/** | |||
* Get new session class to be used in the doLoad() method. | |||
*/ | |||
protected InternalSession getNewSession() { | |||
return new StandardSession(this); | |||
} | |||
@Override | |||
public void add(InternalSession session) { | |||
// 当第一次有session创建的时候,开启session清理线程 | |||
if (!backgroundProcessStarted.getAndSet(true)) { | |||
Thread t = new Thread(new Runnable() { | |||
@Override | |||
public void run() { | |||
while (true) { | |||
try { | |||
// 每秒清理一次 | |||
Thread.sleep(backgroundProcessorDelay * 1000l); | |||
backgroundProcess(); | |||
} catch (InterruptedException e) { | |||
log.error("SessionManagerImpl.backgroundProcess error", e); | |||
} | |||
} | |||
} | |||
}); | |||
t.setDaemon(true); | |||
t.start(); | |||
} | |||
sessions.put(session.getIdInternal(), session); | |||
int size = getActiveSessions(); | |||
if( size > maxActive ) { | |||
synchronized(maxActiveUpdateLock) { | |||
if( size > maxActive ) { | |||
maxActive = size; | |||
} | |||
} | |||
} | |||
} | |||
/** | |||
* Return the set of active Sessions associated with this Manager. | |||
* If this Manager has no active Sessions, a zero-length array is returned. | |||
*/ | |||
@Override | |||
public InternalSession[] findSessions() { | |||
return sessions.values().toArray(new InternalSession[0]); | |||
} | |||
@Override | |||
public void backgroundProcess() { | |||
count = (count + 1) % processExpiresFrequency; | |||
if (count == 0) | |||
processExpires(); | |||
} | |||
/** | |||
* Invalidate all sessions that have expired. | |||
*/ | |||
public void processExpires() { | |||
long timeNow = System.currentTimeMillis(); | |||
InternalSession sessions[] = findSessions(); | |||
int expireHere = 0 ; | |||
if(log.isDebugEnabled()) | |||
log.debug("Start expire sessions {} at {} sessioncount {}", getName(), timeNow, sessions.length); | |||
for (int i = 0; i < sessions.length; i++) { | |||
if (sessions[i]!=null && !sessions[i].isValid()) { | |||
expireHere++; | |||
} | |||
} | |||
long timeEnd = System.currentTimeMillis(); | |||
if(log.isDebugEnabled()) | |||
log.debug("End expire sessions {} processingTime {} expired sessions: {}", getName(), timeEnd - timeNow, expireHere); | |||
processingTime += ( timeEnd - timeNow ); | |||
} | |||
@Override | |||
public void setMaxInactiveInterval(int interval) { | |||
this.maxInactiveInterval = interval; | |||
} | |||
/** | |||
* Set the manager checks frequency. | |||
* | |||
* @param processExpiresFrequency the new manager checks frequency | |||
*/ | |||
@Override | |||
public void setProcessExpiresFrequency(int processExpiresFrequency) { | |||
if (processExpiresFrequency <= 0) { | |||
return; | |||
} | |||
this.processExpiresFrequency = processExpiresFrequency; | |||
} | |||
@Override | |||
public void setBackgroundProcessorDelay(int backgroundProcessorDelay) { | |||
this.backgroundProcessorDelay = backgroundProcessorDelay; | |||
} | |||
/** | |||
* Return the descriptive short name of this Manager implementation. | |||
*/ | |||
public String getName() { | |||
return (name); | |||
} | |||
/** | |||
* Set the maximum number of active Sessions allowed, or -1 for | |||
* no limit. | |||
* | |||
* @param max The new maximum number of sessions | |||
*/ | |||
@Override | |||
public void setMaxActiveSessions(int max) { | |||
this.maxActiveSessions = max; | |||
} | |||
} |
@@ -0,0 +1,57 @@ | |||
/* | |||
* 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 me.chanjar.weixin.common.session; | |||
/** | |||
* An exception that indicates the maximum number of active sessions has been | |||
* reached and the server is refusing to create any new sessions. | |||
*/ | |||
public class TooManyActiveSessionsException | |||
extends IllegalStateException | |||
{ | |||
private static final long serialVersionUID = 1L; | |||
/** | |||
* The maximum number of active sessions the server will tolerate. | |||
*/ | |||
private final int maxActiveSessions; | |||
/** | |||
* Creates a new TooManyActiveSessionsException. | |||
* | |||
* @param message A description for the exception. | |||
* @param maxActive The maximum number of active sessions allowed by the | |||
* session manager. | |||
*/ | |||
public TooManyActiveSessionsException(String message, | |||
int maxActive) | |||
{ | |||
super(message); | |||
maxActiveSessions = maxActive; | |||
} | |||
/** | |||
* Gets the maximum number of sessions allowed by the session manager. | |||
* | |||
* @return The maximum number of sessions allowed by the session manager. | |||
*/ | |||
public int getMaxActiveSessions() | |||
{ | |||
return maxActiveSessions; | |||
} | |||
} |
@@ -0,0 +1,17 @@ | |||
package me.chanjar.weixin.common.session; | |||
import java.util.Enumeration; | |||
public interface WxSession { | |||
public Object getAttribute(String name); | |||
public Enumeration<String> getAttributeNames(); | |||
public void setAttribute(String name, Object value); | |||
public void removeAttribute(String name); | |||
public void invalidate(); | |||
} |
@@ -0,0 +1,21 @@ | |||
package me.chanjar.weixin.common.session; | |||
public interface WxSessionManager { | |||
/** | |||
* 获取某个sessionId对应的session,如果sessionId没有对应的session,则新建一个并返回。 | |||
* @param sessionId | |||
* @return | |||
*/ | |||
public WxSession getSession(String sessionId); | |||
/** | |||
* 获取某个sessionId对应的session,如果sessionId没有对应的session,若create为true则新建一个,否则返回null。 | |||
* @param sessionId | |||
* @param create | |||
* @return | |||
*/ | |||
public WxSession getSession(String sessionId, boolean create); | |||
} |
@@ -0,0 +1,27 @@ | |||
package me.chanjar.weixin.common.util; | |||
/** | |||
* <pre> | |||
* 消息重复检查器 | |||
* 微信服务器在五秒内收不到响应会断掉连接,并且重新发起请求,总共重试三次 | |||
* </pre> | |||
*/ | |||
public interface WxMessageDuplicateChecker { | |||
/** | |||
* <h2>公众号的排重方式</h2> | |||
* | |||
* <p>普通消息:关于重试的消息排重,推荐使用msgid排重。<a href="http://mp.weixin.qq.com/wiki/10/79502792eef98d6e0c6e1739da387346.html">文档参考</a>。</p> | |||
* <p>事件消息:关于重试的消息排重,推荐使用FromUserName + CreateTime 排重。<a href="http://mp.weixin.qq.com/wiki/2/5baf56ce4947d35003b86a9805634b1e.html">文档参考</a></p> | |||
* | |||
* <h2>企业号的排重方式</h2> | |||
* | |||
* 官方文档完全没有写,参照公众号的方式排重。 | |||
* | |||
* <p>或者可以采取更简单的方式,如果有MsgId就用MsgId排重,如果没有就用FromUserName+CreateTime排重</p> | |||
* @param messageId messageId需要根据上面讲的方式构造 | |||
* @return 如果是重复消息,返回true,否则返回false | |||
*/ | |||
public boolean isDuplicate(String messageId); | |||
} |
@@ -2,6 +2,7 @@ package me.chanjar.weixin.common.util; | |||
import java.util.Map; | |||
import java.util.concurrent.ConcurrentHashMap; | |||
import java.util.concurrent.atomic.AtomicBoolean; | |||
/** | |||
* <pre> | |||
@@ -9,7 +10,7 @@ import java.util.concurrent.ConcurrentHashMap; | |||
* 将每个消息id保存在内存里,每隔5秒清理已经过期的消息id,每个消息id的过期时间是15秒 | |||
* </pre> | |||
*/ | |||
public class WxMsgIdInMemoryDuplicateChecker implements WxMsgIdDuplicateChecker { | |||
public class WxMessageInMemoryDuplicateChecker implements WxMessageDuplicateChecker { | |||
/** | |||
* 一个消息ID在内存的过期时间:15秒 | |||
@@ -21,7 +22,15 @@ public class WxMsgIdInMemoryDuplicateChecker implements WxMsgIdDuplicateChecker | |||
*/ | |||
private final Long clearPeriod; | |||
private final ConcurrentHashMap<Long, Long> msgId2Timestamp = new ConcurrentHashMap<Long, Long>(); | |||
/** | |||
* 消息id->消息时间戳的map | |||
*/ | |||
private final ConcurrentHashMap<String, Long> msgId2Timestamp = new ConcurrentHashMap<String, Long>(); | |||
/** | |||
* 后台清理线程是否已经开启 | |||
*/ | |||
private final AtomicBoolean backgroundProcessStarted = new AtomicBoolean(false); | |||
/** | |||
* WxMsgIdInMemoryDuplicateChecker构造函数 | |||
@@ -30,10 +39,9 @@ public class WxMsgIdInMemoryDuplicateChecker implements WxMsgIdDuplicateChecker | |||
* 每隔多少周期检查消息ID是否过期:5秒 | |||
* </pre> | |||
*/ | |||
public WxMsgIdInMemoryDuplicateChecker() { | |||
public WxMessageInMemoryDuplicateChecker() { | |||
this.timeToLive = 15 * 1000l; | |||
this.clearPeriod = 5 * 1000l; | |||
this.start(); | |||
} | |||
/** | |||
@@ -41,13 +49,15 @@ public class WxMsgIdInMemoryDuplicateChecker implements WxMsgIdDuplicateChecker | |||
* @param timeToLive 一个消息ID在内存的过期时间:毫秒 | |||
* @param clearPeriod 每隔多少周期检查消息ID是否过期:毫秒 | |||
*/ | |||
public WxMsgIdInMemoryDuplicateChecker(Long timeToLive, Long clearPeriod) { | |||
public WxMessageInMemoryDuplicateChecker(Long timeToLive, Long clearPeriod) { | |||
this.timeToLive = timeToLive; | |||
this.clearPeriod = clearPeriod; | |||
this.start(); | |||
} | |||
private void start() { | |||
protected void checkBackgroundProcessStarted() { | |||
if (backgroundProcessStarted.getAndSet(true)) { | |||
return; | |||
} | |||
Thread t = new Thread(new Runnable() { | |||
@Override | |||
public void run() { | |||
@@ -55,7 +65,7 @@ public class WxMsgIdInMemoryDuplicateChecker implements WxMsgIdDuplicateChecker | |||
while (true) { | |||
Thread.sleep(clearPeriod); | |||
Long now = System.currentTimeMillis(); | |||
for (Map.Entry<Long, Long> entry : msgId2Timestamp.entrySet()) { | |||
for (Map.Entry<String, Long> entry : msgId2Timestamp.entrySet()) { | |||
if (now - entry.getValue() > timeToLive) { | |||
msgId2Timestamp.entrySet().remove(entry); | |||
} | |||
@@ -71,8 +81,12 @@ public class WxMsgIdInMemoryDuplicateChecker implements WxMsgIdDuplicateChecker | |||
} | |||
@Override | |||
public boolean isDuplicate(Long wxMsgId) { | |||
Long timestamp = msgId2Timestamp.putIfAbsent(wxMsgId, System.currentTimeMillis()); | |||
public boolean isDuplicate(String messageId) { | |||
if (messageId == null) { | |||
return false; | |||
} | |||
checkBackgroundProcessStarted(); | |||
Long timestamp = msgId2Timestamp.putIfAbsent(messageId, System.currentTimeMillis()); | |||
if (timestamp == null) { | |||
// 第一次接收到这个消息 | |||
return false; |
@@ -1,18 +0,0 @@ | |||
package me.chanjar.weixin.common.util; | |||
/** | |||
* <pre> | |||
* 消息重复检查器 | |||
* 微信服务器在五秒内收不到响应会断掉连接,并且重新发起请求,总共重试三次 | |||
* </pre> | |||
*/ | |||
public interface WxMsgIdDuplicateChecker { | |||
/** | |||
* 检查消息ID是否重复 | |||
* @param wxMsgId | |||
* @return 如果是重复消息,返回true,否则返回false | |||
*/ | |||
public boolean isDuplicate(Long wxMsgId); | |||
} |
@@ -0,0 +1,251 @@ | |||
/* | |||
* 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 me.chanjar.weixin.common.util.res; | |||
import java.text.MessageFormat; | |||
import java.util.Enumeration; | |||
import java.util.Hashtable; | |||
import java.util.LinkedHashMap; | |||
import java.util.Locale; | |||
import java.util.Map; | |||
import java.util.MissingResourceException; | |||
import java.util.ResourceBundle; | |||
/** | |||
* An internationalization / localization helper class which reduces | |||
* the bother of handling ResourceBundles and takes care of the | |||
* common cases of message formating which otherwise require the | |||
* creation of Object arrays and such. | |||
* | |||
* <p>The StringManager operates on a package basis. One StringManager | |||
* per package can be created and accessed via the getManager method | |||
* call. | |||
* | |||
* <p>The StringManager will look for a ResourceBundle named by | |||
* the package name given plus the suffix of "LocalStrings". In | |||
* practice, this means that the localized information will be contained | |||
* in a LocalStrings.properties file located in the package | |||
* directory of the classpath. | |||
* | |||
* <p>Please see the documentation for java.util.ResourceBundle for | |||
* more information. | |||
* | |||
* @author James Duncan Davidson [duncan@eng.sun.com] | |||
* @author James Todd [gonzo@eng.sun.com] | |||
* @author Mel Martinez [mmartinez@g1440.com] | |||
* @see java.util.ResourceBundle | |||
*/ | |||
public class StringManager { | |||
private static int LOCALE_CACHE_SIZE = 10; | |||
/** | |||
* The ResourceBundle for this StringManager. | |||
*/ | |||
private final ResourceBundle bundle; | |||
private final Locale locale; | |||
/** | |||
* Creates a new StringManager for a given package. This is a | |||
* private method and all access to it is arbitrated by the | |||
* static getManager method call so that only one StringManager | |||
* per package will be created. | |||
* | |||
* @param packageName Name of package to create StringManager for. | |||
*/ | |||
private StringManager(String packageName, Locale locale) { | |||
String bundleName = packageName + ".LocalStrings"; | |||
ResourceBundle bnd = null; | |||
try { | |||
bnd = ResourceBundle.getBundle(bundleName, locale); | |||
} catch( MissingResourceException ex ) { | |||
// Try from the current loader (that's the case for trusted apps) | |||
// Should only be required if using a TC5 style classloader structure | |||
// where common != shared != server | |||
ClassLoader cl = Thread.currentThread().getContextClassLoader(); | |||
if( cl != null ) { | |||
try { | |||
bnd = ResourceBundle.getBundle(bundleName, locale, cl); | |||
} catch(MissingResourceException ex2) { | |||
// Ignore | |||
} | |||
} | |||
} | |||
bundle = bnd; | |||
// Get the actual locale, which may be different from the requested one | |||
if (bundle != null) { | |||
Locale bundleLocale = bundle.getLocale(); | |||
if (bundleLocale.equals(Locale.ROOT)) { | |||
this.locale = Locale.ENGLISH; | |||
} else { | |||
this.locale = bundleLocale; | |||
} | |||
} else { | |||
this.locale = null; | |||
} | |||
} | |||
/** | |||
Get a string from the underlying resource bundle or return | |||
null if the String is not found. | |||
@param key to desired resource String | |||
@return resource String matching <i>key</i> from underlying | |||
bundle or null if not found. | |||
@throws IllegalArgumentException if <i>key</i> is null. | |||
*/ | |||
public String getString(String key) { | |||
if(key == null){ | |||
String msg = "key may not have a null value"; | |||
throw new IllegalArgumentException(msg); | |||
} | |||
String str = null; | |||
try { | |||
// Avoid NPE if bundle is null and treat it like an MRE | |||
if (bundle != null) { | |||
str = bundle.getString(key); | |||
} | |||
} catch(MissingResourceException mre) { | |||
//bad: shouldn't mask an exception the following way: | |||
// str = "[cannot find message associated with key '" + key + | |||
// "' due to " + mre + "]"; | |||
// because it hides the fact that the String was missing | |||
// from the calling code. | |||
//good: could just throw the exception (or wrap it in another) | |||
// but that would probably cause much havoc on existing | |||
// code. | |||
//better: consistent with container pattern to | |||
// simply return null. Calling code can then do | |||
// a null check. | |||
str = null; | |||
} | |||
return str; | |||
} | |||
/** | |||
* Get a string from the underlying resource bundle and format | |||
* it with the given set of arguments. | |||
* | |||
* @param key | |||
* @param args | |||
*/ | |||
public String getString(final String key, final Object... args) { | |||
String value = getString(key); | |||
if (value == null) { | |||
value = key; | |||
} | |||
MessageFormat mf = new MessageFormat(value); | |||
mf.setLocale(locale); | |||
return mf.format(args, new StringBuffer(), null).toString(); | |||
} | |||
/** | |||
* Identify the Locale this StringManager is associated with | |||
*/ | |||
public Locale getLocale() { | |||
return locale; | |||
} | |||
// -------------------------------------------------------------- | |||
// STATIC SUPPORT METHODS | |||
// -------------------------------------------------------------- | |||
private static final Map<String, Map<Locale,StringManager>> managers = | |||
new Hashtable<String, Map<Locale,StringManager>>(); | |||
/** | |||
* Get the StringManager for a particular package. If a manager for | |||
* a package already exists, it will be reused, else a new | |||
* StringManager will be created and returned. | |||
* | |||
* @param packageName The package name | |||
*/ | |||
public static final synchronized StringManager getManager( | |||
String packageName) { | |||
return getManager(packageName, Locale.getDefault()); | |||
} | |||
/** | |||
* Get the StringManager for a particular package and Locale. If a manager | |||
* for a package/Locale combination already exists, it will be reused, else | |||
* a new StringManager will be created and returned. | |||
* | |||
* @param packageName The package name | |||
* @param locale The Locale | |||
*/ | |||
public static final synchronized StringManager getManager( | |||
String packageName, Locale locale) { | |||
Map<Locale,StringManager> map = managers.get(packageName); | |||
if (map == null) { | |||
/* | |||
* Don't want the HashMap to be expanded beyond LOCALE_CACHE_SIZE. | |||
* Expansion occurs when size() exceeds capacity. Therefore keep | |||
* size at or below capacity. | |||
* removeEldestEntry() executes after insertion therefore the test | |||
* for removal needs to use one less than the maximum desired size | |||
* | |||
*/ | |||
map = new LinkedHashMap<Locale,StringManager>(LOCALE_CACHE_SIZE, 1, true) { | |||
private static final long serialVersionUID = 1L; | |||
@Override | |||
protected boolean removeEldestEntry( | |||
Map.Entry<Locale,StringManager> eldest) { | |||
if (size() > (LOCALE_CACHE_SIZE - 1)) { | |||
return true; | |||
} | |||
return false; | |||
} | |||
}; | |||
managers.put(packageName, map); | |||
} | |||
StringManager mgr = map.get(locale); | |||
if (mgr == null) { | |||
mgr = new StringManager(packageName, locale); | |||
map.put(locale, mgr); | |||
} | |||
return mgr; | |||
} | |||
/** | |||
* Retrieve the StringManager for a list of Locales. The first StringManager | |||
* found will be returned. | |||
* | |||
* @param requestedLocales the list of Locales | |||
* | |||
* @return the found StringManager or the default StringManager | |||
*/ | |||
public static StringManager getManager(String packageName, | |||
Enumeration<Locale> requestedLocales) { | |||
while (requestedLocales.hasMoreElements()) { | |||
Locale locale = requestedLocales.nextElement(); | |||
StringManager result = getManager(packageName, locale); | |||
if (result.getLocale().equals(locale)) { | |||
return result; | |||
} | |||
} | |||
// Return the default | |||
return getManager(packageName); | |||
} | |||
} |
@@ -2,9 +2,6 @@ package me.chanjar.weixin.common.util.xml; | |||
import com.thoughtworks.xstream.converters.basic.StringConverter; | |||
/** | |||
* Created by qianjia on 15/1/19. | |||
*/ | |||
public class XStreamCDataConverter extends StringConverter { | |||
@Override | |||
@@ -14,9 +14,6 @@ import com.thoughtworks.xstream.security.PrimitiveTypePermission; | |||
import java.io.Writer; | |||
/** | |||
* Created by qianjia on 15/1/19. | |||
*/ | |||
public class XStreamInitializer { | |||
public static XStream getInstance() { | |||
@@ -1,8 +1,5 @@ | |||
package me.chanjar.weixin.common.util.xml; | |||
/** | |||
* Created by qianjia on 15/1/19. | |||
*/ | |||
public class XStreamMediaIdConverter extends XStreamCDataConverter { | |||
@Override | |||
public String toString(Object obj) { | |||
@@ -0,0 +1,132 @@ | |||
package me.chanjar.weixin.common.session; | |||
import org.testng.Assert; | |||
import org.testng.annotations.DataProvider; | |||
import org.testng.annotations.Test; | |||
@Test | |||
public class SessionTest { | |||
@DataProvider | |||
public Object[][] getSessionManager() { | |||
return new Object[][] { | |||
new Object[] { new StandardSessionManager() } | |||
}; | |||
} | |||
@Test(dataProvider = "getSessionManager", expectedExceptions = IllegalStateException.class) | |||
public void testInvalidate(WxSessionManager sessionManager) { | |||
WxSession session = sessionManager.getSession("abc"); | |||
session.invalidate(); | |||
session.getAttributeNames(); | |||
} | |||
@Test(dataProvider = "getSessionManager") | |||
public void testInvalidate2(InternalSessionManager sessionManager) { | |||
Assert.assertEquals(sessionManager.getActiveSessions(), 0); | |||
WxSession session = ((WxSessionManager) sessionManager).getSession("abc"); | |||
Assert.assertEquals(sessionManager.getActiveSessions(), 1); | |||
session.invalidate(); | |||
Assert.assertEquals(sessionManager.getActiveSessions(), 0); | |||
} | |||
@Test(dataProvider = "getSessionManager") | |||
public void testGetSession(WxSessionManager sessionManager) { | |||
WxSession session1 = sessionManager.getSession("abc"); | |||
WxSession session2 = sessionManager.getSession("abc"); | |||
Assert.assertEquals(session1, session2); | |||
Assert.assertTrue(session1 == session2); | |||
WxSession abc1 = sessionManager.getSession("abc1"); | |||
Assert.assertNotEquals(session1, abc1); | |||
WxSession abc1b = sessionManager.getSession("abc1", false); | |||
Assert.assertEquals(abc1, abc1b); | |||
WxSession def = sessionManager.getSession("def", false); | |||
Assert.assertNull(def); | |||
} | |||
@Test(dataProvider = "getSessionManager") | |||
public void testInvalidateAngGet(WxSessionManager sessionManager) { | |||
WxSession session1 = sessionManager.getSession("abc"); | |||
session1.invalidate(); | |||
WxSession session2 = sessionManager.getSession("abc"); | |||
Assert.assertNotEquals(session1, session2); | |||
InternalSessionManager ism = (InternalSessionManager) sessionManager; | |||
Assert.assertEquals(ism.getActiveSessions(), 1); | |||
} | |||
@Test(dataProvider = "getSessionManager") | |||
public void testBackgroundProcess(WxSessionManager sessionManager) throws InterruptedException { | |||
InternalSessionManager ism = (InternalSessionManager) sessionManager; | |||
ism.setMaxInactiveInterval(1); | |||
ism.setProcessExpiresFrequency(1); | |||
ism.setBackgroundProcessorDelay(1); | |||
Assert.assertEquals(ism.getActiveSessions(), 0); | |||
InternalSession abc = ism.createSession("abc"); | |||
abc.endAccess(); | |||
Thread.sleep(2000l); | |||
Assert.assertEquals(ism.getActiveSessions(), 0); | |||
} | |||
@Test(dataProvider = "getSessionManager") | |||
public void testBackgroundProcess2(WxSessionManager sessionManager) throws InterruptedException { | |||
InternalSessionManager ism = (InternalSessionManager) sessionManager; | |||
ism.setMaxInactiveInterval(100); | |||
ism.setProcessExpiresFrequency(1); | |||
ism.setBackgroundProcessorDelay(1); | |||
Assert.assertEquals(ism.getActiveSessions(), 0); | |||
InternalSession abc = ism.createSession("abc"); | |||
abc.setMaxInactiveInterval(1); | |||
abc.endAccess(); | |||
Thread.sleep(2000l); | |||
Assert.assertEquals(ism.getActiveSessions(), 0); | |||
} | |||
@Test(dataProvider = "getSessionManager") | |||
public void testMaxActive(WxSessionManager sessionManager) throws InterruptedException { | |||
InternalSessionManager ism = (InternalSessionManager) sessionManager; | |||
ism.setMaxActiveSessions(2); | |||
ism.createSession("abc"); | |||
ism.createSession("abc"); | |||
ism.createSession("def"); | |||
} | |||
@Test(dataProvider = "getSessionManager", expectedExceptions = TooManyActiveSessionsException.class) | |||
public void testMaxActive2(WxSessionManager sessionManager) throws InterruptedException { | |||
InternalSessionManager ism = (InternalSessionManager) sessionManager; | |||
ism.setMaxActiveSessions(2); | |||
ism.createSession("abc"); | |||
ism.createSession("abc"); | |||
ism.createSession("def"); | |||
ism.createSession("xyz"); | |||
} | |||
} |
@@ -3,33 +3,30 @@ package me.chanjar.weixin.common.util; | |||
import org.testng.Assert; | |||
import org.testng.annotations.Test; | |||
/** | |||
* Created by qianjia on 15/1/20. | |||
*/ | |||
@Test | |||
public class WxMsgIdInMemoryDuplicateCheckerTest { | |||
public class WxMessageInMemoryDuplicateCheckerTest { | |||
public void test() throws InterruptedException { | |||
Long[] msgIds = new Long[] { 1l, 2l, 3l, 4l, 5l, 6l, 7l, 8l }; | |||
WxMsgIdInMemoryDuplicateChecker checker = new WxMsgIdInMemoryDuplicateChecker(2000l, 1000l); | |||
WxMessageInMemoryDuplicateChecker checker = new WxMessageInMemoryDuplicateChecker(2000l, 1000l); | |||
// 第一次检查 | |||
for (Long msgId : msgIds) { | |||
boolean result = checker.isDuplicate(msgId); | |||
boolean result = checker.isDuplicate(String.valueOf(msgId)); | |||
Assert.assertFalse(result); | |||
} | |||
// 过1秒再检查 | |||
Thread.sleep(1000l); | |||
for (Long msgId : msgIds) { | |||
boolean result = checker.isDuplicate(msgId); | |||
boolean result = checker.isDuplicate(String.valueOf(msgId)); | |||
Assert.assertTrue(result); | |||
} | |||
// 过1.5秒再检查 | |||
Thread.sleep(1500l); | |||
for (Long msgId : msgIds) { | |||
boolean result = checker.isDuplicate(msgId); | |||
boolean result = checker.isDuplicate(String.valueOf(msgId)); | |||
Assert.assertFalse(result); | |||
} | |||
@@ -0,0 +1,16 @@ | |||
<configuration> | |||
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender"> | |||
<!-- encoders are assigned the type | |||
ch.qos.logback.classic.encoder.PatternLayoutEncoder by default --> | |||
<encoder> | |||
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern> | |||
</encoder> | |||
</appender> | |||
<root level="info"> | |||
<appender-ref ref="STDOUT" /> | |||
</root> | |||
<logger name="me.chanjar.weixin.common" level="debug" /> | |||
</configuration> |
@@ -1,13 +1,14 @@ | |||
<!DOCTYPE suite SYSTEM "http://testng.org/testng-1.0.dtd" > | |||
<suite name="Weixin-java-tool-suite" verbose="1"> | |||
<test name="Bean_Test"> | |||
<classes> | |||
<class name="me.chanjar.weixin.common.bean.WxAccessTokenTest" /> | |||
<class name="me.chanjar.weixin.common.bean.WxErrorTest" /> | |||
<class name="me.chanjar.weixin.common.bean.WxMenuTest" /> | |||
<class name="me.chanjar.weixin.common.util.crypto.WxCryptUtilTest" /> | |||
<class name="me.chanjar.weixin.common.util.WxMsgIdInMemoryDuplicateCheckerTest" /> | |||
</classes> | |||
</test> | |||
<test name="Bean_Test"> | |||
<classes> | |||
<class name="me.chanjar.weixin.common.bean.WxAccessTokenTest"/> | |||
<class name="me.chanjar.weixin.common.bean.WxErrorTest"/> | |||
<class name="me.chanjar.weixin.common.bean.WxMenuTest"/> | |||
<class name="me.chanjar.weixin.common.util.crypto.WxCryptUtilTest"/> | |||
<class name="me.chanjar.weixin.common.util.WxMessageInMemoryDuplicateCheckerTest"/> | |||
<class name="me.chanjar.weixin.common.session.SessionTest" /> | |||
</classes> | |||
</test> | |||
</suite> |
@@ -37,11 +37,11 @@ public class WxCpInMemoryConfigStorage implements WxCpConfigStorage { | |||
this.expiresTime = 0; | |||
} | |||
public void updateAccessToken(WxAccessToken accessToken) { | |||
public synchronized void updateAccessToken(WxAccessToken accessToken) { | |||
updateAccessToken(accessToken.getAccessToken(), accessToken.getExpiresIn()); | |||
} | |||
public void updateAccessToken(String accessToken, int expiresInSeconds) { | |||
public synchronized void updateAccessToken(String accessToken, int expiresInSeconds) { | |||
this.accessToken = accessToken; | |||
this.expiresTime = System.currentTimeMillis() + (expiresInSeconds - 200) * 1000l; | |||
} | |||
@@ -1,5 +1,7 @@ | |||
package me.chanjar.weixin.cp.api; | |||
import me.chanjar.weixin.common.session.WxSession; | |||
import me.chanjar.weixin.common.session.WxSessionManager; | |||
import me.chanjar.weixin.cp.bean.WxCpXmlMessage; | |||
import me.chanjar.weixin.cp.bean.WxCpXmlOutMessage; | |||
@@ -7,18 +9,21 @@ import java.util.Map; | |||
/** | |||
* 处理微信推送消息的处理器接口 | |||
* @author Daniel Qian | |||
* | |||
* @author Daniel Qian | |||
*/ | |||
public interface WxCpMessageHandler { | |||
/** | |||
* | |||
* @param wxMessage | |||
* @param context 上下文,如果handler或interceptor之间有信息要传递,可以用这个 | |||
* @param context 上下文,如果handler或interceptor之间有信息要传递,可以用这个 | |||
* @param wxCpService | |||
* @param sessionManager | |||
* @return xml格式的消息,如果在异步规则里处理的话,可以返回null | |||
*/ | |||
public WxCpXmlOutMessage handle(WxCpXmlMessage wxMessage, Map<String, Object> context, WxCpService wxCpService); | |||
public WxCpXmlOutMessage handle(WxCpXmlMessage wxMessage, | |||
Map<String, Object> context, | |||
WxCpService wxCpService, | |||
WxSessionManager sessionManager); | |||
} |
@@ -1,23 +1,29 @@ | |||
package me.chanjar.weixin.cp.api; | |||
import me.chanjar.weixin.common.session.WxSessionManager; | |||
import me.chanjar.weixin.cp.bean.WxCpXmlMessage; | |||
import java.util.Map; | |||
/** | |||
* 微信消息拦截器,可以用来做验证 | |||
* @author Daniel Qian | |||
* | |||
* @author Daniel Qian | |||
*/ | |||
public interface WxCpMessageInterceptor { | |||
/** | |||
* 拦截微信消息 | |||
* | |||
* @param wxMessage | |||
* @param context 上下文,如果handler或interceptor之间有信息要传递,可以用这个 | |||
* @param context 上下文,如果handler或interceptor之间有信息要传递,可以用这个 | |||
* @param wxCpService | |||
* @return true代表OK,false代表不OK | |||
* @param sessionManager | |||
* @return true代表OK,false代表不OK | |||
*/ | |||
public boolean intercept(WxCpXmlMessage wxMessage, Map<String, Object> context, WxCpService wxCpService); | |||
public boolean intercept(WxCpXmlMessage wxMessage, | |||
Map<String, Object> context, | |||
WxCpService wxCpService, | |||
WxSessionManager sessionManager); | |||
} |
@@ -1,16 +1,21 @@ | |||
package me.chanjar.weixin.cp.api; | |||
import me.chanjar.weixin.common.util.WxMsgIdDuplicateChecker; | |||
import me.chanjar.weixin.common.util.WxMsgIdInMemoryDuplicateChecker; | |||
import me.chanjar.weixin.common.session.*; | |||
import me.chanjar.weixin.common.util.WxMessageDuplicateChecker; | |||
import me.chanjar.weixin.common.util.WxMessageInMemoryDuplicateChecker; | |||
import me.chanjar.weixin.cp.bean.WxCpXmlMessage; | |||
import me.chanjar.weixin.cp.bean.WxCpXmlOutMessage; | |||
import org.slf4j.Logger; | |||
import org.slf4j.LoggerFactory; | |||
import java.util.ArrayList; | |||
import java.util.HashMap; | |||
import java.util.List; | |||
import java.util.Map; | |||
import java.util.concurrent.ExecutionException; | |||
import java.util.concurrent.ExecutorService; | |||
import java.util.concurrent.Executors; | |||
import java.util.concurrent.Future; | |||
import java.util.regex.Pattern; | |||
/** | |||
@@ -43,7 +48,9 @@ import java.util.regex.Pattern; | |||
*/ | |||
public class WxCpMessageRouter { | |||
private static final int DEFAULT_THREAD_POOL_SIZE = 20; | |||
protected final Logger log = LoggerFactory.getLogger(WxCpMessageRouter.class); | |||
private static final int DEFAULT_THREAD_POOL_SIZE = 100; | |||
private final List<Rule> rules = new ArrayList<Rule>(); | |||
@@ -51,22 +58,22 @@ public class WxCpMessageRouter { | |||
private ExecutorService executorService; | |||
private WxMsgIdDuplicateChecker wxMsgIdDuplicateChecker; | |||
private WxMessageDuplicateChecker messageDuplicateChecker; | |||
private WxSessionManager sessionManager; | |||
public WxCpMessageRouter(WxCpService wxCpService) { | |||
this.wxCpService = wxCpService; | |||
this.executorService = Executors.newFixedThreadPool(DEFAULT_THREAD_POOL_SIZE); | |||
this.wxMsgIdDuplicateChecker = new WxMsgIdInMemoryDuplicateChecker(); | |||
} | |||
public WxCpMessageRouter(WxCpService wxMpService, int threadPoolSize) { | |||
this.wxCpService = wxMpService; | |||
this.executorService = Executors.newFixedThreadPool(threadPoolSize); | |||
this.wxMsgIdDuplicateChecker = new WxMsgIdInMemoryDuplicateChecker(); | |||
this.messageDuplicateChecker = new WxMessageInMemoryDuplicateChecker(); | |||
this.sessionManager = new StandardSessionManager(); | |||
} | |||
/** | |||
* 设置自定义的ExecutorService | |||
* <pre> | |||
* 设置自定义的 {@link ExecutorService} | |||
* 如果不调用该方法,默认使用 Executors.newFixedThreadPool(100) | |||
* </pre> | |||
* @param executorService | |||
*/ | |||
public void setExecutorService(ExecutorService executorService) { | |||
@@ -74,11 +81,25 @@ public class WxCpMessageRouter { | |||
} | |||
/** | |||
* 设置自定义的WxMsgIdDuplicateChecker | |||
* @param wxMsgIdDuplicateChecker | |||
* <pre> | |||
* 设置自定义的 {@link me.chanjar.weixin.common.util.WxMessageDuplicateChecker} | |||
* 如果不调用该方法,默认使用 {@link me.chanjar.weixin.common.util.WxMessageInMemoryDuplicateChecker} | |||
* </pre> | |||
* @param messageDuplicateChecker | |||
*/ | |||
public void setMessageDuplicateChecker(WxMessageDuplicateChecker messageDuplicateChecker) { | |||
this.messageDuplicateChecker = messageDuplicateChecker; | |||
} | |||
/** | |||
* <pre> | |||
* 设置自定义的{@link me.chanjar.weixin.common.session.WxSessionManager} | |||
* 如果不调用该方法,默认使用 {@linke SessionManagerImpl} | |||
* </pre> | |||
* @param sessionManager | |||
*/ | |||
public void setWxMsgIdDuplicateChecker(WxMsgIdDuplicateChecker wxMsgIdDuplicateChecker) { | |||
this.wxMsgIdDuplicateChecker = wxMsgIdDuplicateChecker; | |||
public void setSessionManager(WxSessionManager sessionManager) { | |||
this.sessionManager = sessionManager; | |||
} | |||
/** | |||
@@ -86,7 +107,7 @@ public class WxCpMessageRouter { | |||
* @return | |||
*/ | |||
public Rule rule() { | |||
return new Rule(this, wxCpService); | |||
return new Rule(this, wxCpService, sessionManager); | |||
} | |||
/** | |||
@@ -94,7 +115,7 @@ public class WxCpMessageRouter { | |||
* @param wxMessage | |||
*/ | |||
public WxCpXmlOutMessage route(final WxCpXmlMessage wxMessage) { | |||
if (wxMsgIdDuplicateChecker.isDuplicate(wxMessage.getMsgId())) { | |||
if (isDuplicateMessage(wxMessage)) { | |||
// 如果是重复消息,那么就不做处理 | |||
return null; | |||
} | |||
@@ -115,27 +136,84 @@ public class WxCpMessageRouter { | |||
} | |||
WxCpXmlOutMessage res = null; | |||
final List<Future> futures = new ArrayList<Future>(); | |||
for (final Rule rule : matchRules) { | |||
// 返回最后一个非异步的rule的执行结果 | |||
if(rule.async) { | |||
executorService.submit(new Runnable() { | |||
public void run() { | |||
rule.service(wxMessage); | |||
} | |||
}); | |||
futures.add( | |||
executorService.submit(new Runnable() { | |||
public void run() { | |||
rule.service(wxMessage); | |||
} | |||
}) | |||
); | |||
} else { | |||
res = rule.service(wxMessage); | |||
// 在同步操作结束,session访问结束 | |||
log.debug("End session access: async=false, sessionId={}", wxMessage.getFromUserName()); | |||
sessionEndAccess(wxMessage); | |||
} | |||
} | |||
if (futures.size() > 0) { | |||
executorService.submit(new Runnable() { | |||
@Override | |||
public void run() { | |||
for (Future future : futures) { | |||
try { | |||
future.get(); | |||
log.debug("End session access: async=true, sessionId={}", wxMessage.getFromUserName()); | |||
// 异步操作结束,session访问结束 | |||
sessionEndAccess(wxMessage); | |||
} catch (InterruptedException e) { | |||
log.error("Error happened when wait task finish", e); | |||
} catch (ExecutionException e) { | |||
log.error("Error happened when wait task finish", e); | |||
} | |||
} | |||
} | |||
}); | |||
} | |||
return res; | |||
} | |||
protected boolean isDuplicateMessage(WxCpXmlMessage wxMessage) { | |||
String messageId = ""; | |||
if (wxMessage.getMsgId() == null) { | |||
messageId = wxMessage.getFromUserName() + "-" + String.valueOf(wxMessage.getCreateTime()); | |||
} else { | |||
messageId = String.valueOf(wxMessage.getMsgId()); | |||
} | |||
if (messageDuplicateChecker.isDuplicate(messageId)) { | |||
return true; | |||
} | |||
return false; | |||
} | |||
/** | |||
* 对session的访问结束 | |||
* @param wxMessage | |||
*/ | |||
protected void sessionEndAccess(WxCpXmlMessage wxMessage) { | |||
InternalSession session = ((InternalSessionManager)sessionManager).findSession(wxMessage.getFromUserName()); | |||
if (session != null) { | |||
session.endAccess(); | |||
} | |||
} | |||
public static class Rule { | |||
private final WxCpMessageRouter routerBuilder; | |||
private final WxCpService wxCpService; | |||
private final WxSessionManager sessionManager; | |||
private boolean async = true; | |||
private String fromUser; | |||
@@ -158,9 +236,10 @@ public class WxCpMessageRouter { | |||
private List<WxCpMessageInterceptor> interceptors = new ArrayList<WxCpMessageInterceptor>(); | |||
protected Rule(WxCpMessageRouter routerBuilder, WxCpService wxCpService) { | |||
protected Rule(WxCpMessageRouter routerBuilder, WxCpService wxCpService, WxSessionManager sessionManager) { | |||
this.routerBuilder = routerBuilder; | |||
this.wxCpService = wxCpService; | |||
this.sessionManager = sessionManager; | |||
} | |||
/** | |||
@@ -338,7 +417,7 @@ public class WxCpMessageRouter { | |||
Map<String, Object> context = new HashMap<String, Object>(); | |||
// 如果拦截器不通过 | |||
for (WxCpMessageInterceptor interceptor : this.interceptors) { | |||
if (!interceptor.intercept(wxMessage, context, wxCpService)) { | |||
if (!interceptor.intercept(wxMessage, context, wxCpService, sessionManager)) { | |||
return null; | |||
} | |||
} | |||
@@ -347,7 +426,7 @@ public class WxCpMessageRouter { | |||
WxCpXmlOutMessage res = null; | |||
for (WxCpMessageHandler handler : this.handlers) { | |||
// 返回最后handler的结果 | |||
res = handler.handle(wxMessage, context, wxCpService); | |||
res = handler.handle(wxMessage, context, wxCpService, sessionManager); | |||
} | |||
return res; | |||
} | |||
@@ -3,6 +3,8 @@ package me.chanjar.weixin.cp.api; | |||
import me.chanjar.weixin.common.bean.WxMenu; | |||
import me.chanjar.weixin.common.bean.result.WxMediaUploadResult; | |||
import me.chanjar.weixin.common.exception.WxErrorException; | |||
import me.chanjar.weixin.common.session.WxSession; | |||
import me.chanjar.weixin.common.session.WxSessionManager; | |||
import me.chanjar.weixin.common.util.http.RequestExecutor; | |||
import me.chanjar.weixin.cp.bean.WxCpDepart; | |||
import me.chanjar.weixin.cp.bean.WxCpMessage; | |||
@@ -353,4 +355,46 @@ public interface WxCpService { | |||
* @param wxConfigProvider | |||
*/ | |||
public void setWxCpConfigStorage(WxCpConfigStorage wxConfigProvider); | |||
/** | |||
* <pre> | |||
* 设置当微信系统响应系统繁忙时,要等待多少 retrySleepMillis(ms) * 2^(重试次数 - 1) 再发起重试 | |||
* 默认:1000ms | |||
* </pre> | |||
* @param retrySleepMillis | |||
*/ | |||
void setRetrySleepMillis(int retrySleepMillis); | |||
/** | |||
* <pre> | |||
* 设置当微信系统响应系统繁忙时,最大重试次数 | |||
* 默认:5次 | |||
* </pre> | |||
* @param maxRetryTimes | |||
*/ | |||
void setMaxRetryTimes(int maxRetryTimes); | |||
/** | |||
* 获取某个sessionId对应的session,如果sessionId没有对应的session,则新建一个并返回。 | |||
* @param id id可以为任意字符串,建议使用FromUserName作为id | |||
* @return | |||
*/ | |||
WxSession getSession(String id); | |||
/** | |||
* 获取某个sessionId对应的session,如果sessionId没有对应的session,若create为true则新建一个,否则返回null。 | |||
* @param id id可以为任意字符串,建议使用FromUserName作为id | |||
* @param create | |||
* @return | |||
*/ | |||
WxSession getSession(String id, boolean create); | |||
/** | |||
* <pre> | |||
* 设置WxSessionManager,只有当需要使用个性化的WxSessionManager的时候才需要调用此方法, | |||
* WxCpService默认使用的是{@link me.chanjar.weixin.common.session.StandardSessionManager} | |||
* </pre> | |||
* @param sessionManager | |||
*/ | |||
void setSessionManager(WxSessionManager sessionManager); | |||
} |
@@ -12,6 +12,9 @@ import me.chanjar.weixin.common.bean.WxMenu; | |||
import me.chanjar.weixin.common.bean.result.WxError; | |||
import me.chanjar.weixin.common.bean.result.WxMediaUploadResult; | |||
import me.chanjar.weixin.common.exception.WxErrorException; | |||
import me.chanjar.weixin.common.session.StandardSessionManager; | |||
import me.chanjar.weixin.common.session.WxSession; | |||
import me.chanjar.weixin.common.session.WxSessionManager; | |||
import me.chanjar.weixin.common.util.StringUtils; | |||
import me.chanjar.weixin.common.util.crypto.SHA1; | |||
import me.chanjar.weixin.common.util.fs.FileUtils; | |||
@@ -34,6 +37,8 @@ import org.apache.http.impl.client.BasicCredentialsProvider; | |||
import org.apache.http.impl.client.BasicResponseHandler; | |||
import org.apache.http.impl.client.CloseableHttpClient; | |||
import org.apache.http.impl.client.HttpClients; | |||
import org.slf4j.Logger; | |||
import org.slf4j.LoggerFactory; | |||
import java.io.File; | |||
import java.io.IOException; | |||
@@ -41,10 +46,11 @@ import java.io.InputStream; | |||
import java.io.StringReader; | |||
import java.util.List; | |||
import java.util.UUID; | |||
import java.util.concurrent.atomic.AtomicBoolean; | |||
public class WxCpServiceImpl implements WxCpService { | |||
protected final Logger log = LoggerFactory.getLogger(WxCpServiceImpl.class); | |||
/** | |||
* 全局的是否正在刷新access token的锁 | |||
*/ | |||
@@ -52,12 +58,16 @@ public class WxCpServiceImpl implements WxCpService { | |||
protected WxCpConfigStorage wxCpConfigStorage; | |||
protected final ThreadLocal<Integer> retryTimes = new ThreadLocal<Integer>(); | |||
protected CloseableHttpClient httpClient; | |||
protected HttpHost httpProxy; | |||
private int retrySleepMillis = 1000; | |||
private int maxRetryTimes = 5; | |||
protected WxSessionManager sessionManager = new StandardSessionManager(); | |||
public boolean checkSignature(String msgSignature, String timestamp, String nonce, String data) { | |||
try { | |||
return SHA1.gen(wxCpConfigStorage.getToken(), timestamp, nonce, data).equals(msgSignature); | |||
@@ -366,6 +376,33 @@ public class WxCpServiceImpl implements WxCpService { | |||
* @throws WxErrorException | |||
*/ | |||
public <T, E> T execute(RequestExecutor<T, E> executor, String uri, E data) throws WxErrorException { | |||
int retryTimes = 0; | |||
do { | |||
try { | |||
return executeInternal(executor, uri, data); | |||
} catch (WxErrorException e) { | |||
WxError error = e.getError(); | |||
/** | |||
* -1 系统繁忙, 1000ms后重试 | |||
*/ | |||
if (error.getErrorCode() == -1) { | |||
int sleepMillis = retrySleepMillis * (1 << retryTimes); | |||
try { | |||
log.debug("微信系统繁忙,{}ms 后重试(第{}次)", sleepMillis, retryTimes + 1); | |||
Thread.sleep(sleepMillis); | |||
} catch (InterruptedException e1) { | |||
throw new RuntimeException(e1); | |||
} | |||
} else { | |||
throw e; | |||
} | |||
} | |||
} while(++retryTimes < maxRetryTimes); | |||
throw new RuntimeException("微信服务端异常,超出重试次数"); | |||
} | |||
protected <T, E> T executeInternal(RequestExecutor<T, E> executor, String uri, E data) throws WxErrorException { | |||
String accessToken = getAccessToken(false); | |||
String uriWithAccessToken = uri; | |||
@@ -381,31 +418,10 @@ public class WxCpServiceImpl implements WxCpService { | |||
* 42001 access_token超时 | |||
*/ | |||
if (error.getErrorCode() == 42001 || error.getErrorCode() == 40001) { | |||
// 强制设置wxCpConfigStorage它的access token过期了,这样在下一次请求里就会刷新access token | |||
// 强制设置wxMpConfigStorage它的access token过期了,这样在下一次请求里就会刷新access token | |||
wxCpConfigStorage.expireAccessToken(); | |||
return execute(executor, uri, data); | |||
} | |||
/** | |||
* -1 系统繁忙, 1000ms后重试 | |||
*/ | |||
if (error.getErrorCode() == -1) { | |||
if (retryTimes.get() == null) { | |||
retryTimes.set(0); | |||
} | |||
if (retryTimes.get() > 4) { | |||
retryTimes.set(0); | |||
throw new RuntimeException("微信服务端异常,超出重试次数"); | |||
} | |||
int sleepMillis = 1000 * (1 << retryTimes.get()); | |||
try { | |||
System.out.println("微信系统繁忙," + sleepMillis + "ms后重试"); | |||
Thread.sleep(sleepMillis); | |||
retryTimes.set(retryTimes.get() + 1); | |||
return execute(executor, uri, data); | |||
} catch (InterruptedException e1) { | |||
throw new RuntimeException(e1); | |||
} | |||
} | |||
if (error.getErrorCode() != 0) { | |||
throw new WxErrorException(error); | |||
} | |||
@@ -416,7 +432,6 @@ public class WxCpServiceImpl implements WxCpService { | |||
throw new RuntimeException(e); | |||
} | |||
} | |||
protected CloseableHttpClient getHttpclient() { | |||
return httpClient; | |||
} | |||
@@ -451,4 +466,37 @@ public class WxCpServiceImpl implements WxCpService { | |||
} | |||
} | |||
@Override | |||
public void setRetrySleepMillis(int retrySleepMillis) { | |||
this.retrySleepMillis = retrySleepMillis; | |||
} | |||
@Override | |||
public void setMaxRetryTimes(int maxRetryTimes) { | |||
this.maxRetryTimes = maxRetryTimes; | |||
} | |||
@Override | |||
public WxSession getSession(String id) { | |||
if (sessionManager == null) { | |||
return null; | |||
} | |||
return sessionManager.getSession(id); | |||
} | |||
@Override | |||
public WxSession getSession(String id, boolean create) { | |||
if (sessionManager == null) { | |||
return null; | |||
} | |||
return sessionManager.getSession(id, create); | |||
} | |||
@Override | |||
public void setSessionManager(WxSessionManager sessionManager) { | |||
this.sessionManager = sessionManager; | |||
} | |||
} |
@@ -8,9 +8,6 @@ import java.io.InputStream; | |||
import java.util.HashMap; | |||
import java.util.Map; | |||
/** | |||
* Created by qianjia on 15/1/19. | |||
*/ | |||
public class XStreamTransformer { | |||
protected static final Map<Class, XStream> CLASS_2_XSTREAM_INSTANCE = configXStreamInstance(); | |||
@@ -0,0 +1,66 @@ | |||
package me.chanjar.weixin.cp.api; | |||
import me.chanjar.weixin.common.bean.result.WxError; | |||
import me.chanjar.weixin.common.exception.WxErrorException; | |||
import me.chanjar.weixin.common.util.http.RequestExecutor; | |||
import org.testng.annotations.DataProvider; | |||
import org.testng.annotations.Test; | |||
import java.util.concurrent.ExecutionException; | |||
import java.util.concurrent.ExecutorService; | |||
import java.util.concurrent.Executors; | |||
import java.util.concurrent.Future; | |||
@Test | |||
public class WxCpBusyRetryTest { | |||
@DataProvider(name="getService") | |||
public Object[][] getService() { | |||
WxCpService service = new WxCpServiceImpl() { | |||
@Override | |||
protected <T, E> T executeInternal(RequestExecutor<T, E> executor, String uri, E data) throws WxErrorException { | |||
WxError error = new WxError(); | |||
error.setErrorCode(-1); | |||
throw new WxErrorException(error); | |||
} | |||
}; | |||
service.setMaxRetryTimes(3); | |||
service.setRetrySleepMillis(500); | |||
return new Object[][] { | |||
new Object[] { service } | |||
}; | |||
} | |||
@Test(dataProvider = "getService", expectedExceptions = RuntimeException.class) | |||
public void testRetry(WxCpService service) throws WxErrorException { | |||
service.execute(null, null, null); | |||
} | |||
@Test(dataProvider = "getService") | |||
public void testRetryInThreadPool(final WxCpService service) throws InterruptedException, ExecutionException { | |||
// 当线程池中的线程复用的时候,还是能保证相同的重试次数 | |||
ExecutorService executorService = Executors.newFixedThreadPool(1); | |||
Runnable runnable = new Runnable() { | |||
@Override | |||
public void run() { | |||
try { | |||
System.out.println("====================="); | |||
System.out.println(Thread.currentThread().getName() + ": testRetry"); | |||
service.execute(null, null, null); | |||
} catch (WxErrorException e) { | |||
throw new RuntimeException(e); | |||
} catch (RuntimeException e) { | |||
// OK | |||
} | |||
} | |||
}; | |||
Future<?> submit1 = executorService.submit(runnable); | |||
Future<?> submit2 = executorService.submit(runnable); | |||
submit1.get(); | |||
submit2.get(); | |||
} | |||
} |
@@ -1,6 +1,8 @@ | |||
package me.chanjar.weixin.cp.api; | |||
import me.chanjar.weixin.common.api.WxConsts; | |||
import me.chanjar.weixin.common.session.StandardSessionManager; | |||
import me.chanjar.weixin.common.session.WxSessionManager; | |||
import me.chanjar.weixin.cp.bean.WxCpXmlMessage; | |||
import me.chanjar.weixin.cp.bean.WxCpXmlOutMessage; | |||
import org.testng.Assert; | |||
@@ -67,7 +69,8 @@ public class WxCpMessageRouterTest { | |||
final WxCpMessageRouter router = new WxCpMessageRouter(null); | |||
router.rule().handler(new WxCpMessageHandler() { | |||
@Override | |||
public WxCpXmlOutMessage handle(WxCpXmlMessage wxMessage, Map<String, Object> context, WxCpService wxCpService) { | |||
public WxCpXmlOutMessage handle(WxCpXmlMessage wxMessage, Map<String, Object> context, WxCpService wxCpService, | |||
WxSessionManager sessionManager) { | |||
return null; | |||
} | |||
}).end(); | |||
@@ -149,11 +152,145 @@ public class WxCpMessageRouterTest { | |||
} | |||
@Override | |||
public WxCpXmlOutMessage handle(WxCpXmlMessage wxMessage, Map<String, Object> context, WxCpService wxCpService) { | |||
public WxCpXmlOutMessage handle(WxCpXmlMessage wxMessage, Map<String, Object> context, WxCpService wxCpService, | |||
WxSessionManager sessionManager) { | |||
sb.append(this.echoStr).append(','); | |||
return null; | |||
} | |||
} | |||
@DataProvider | |||
public Object[][] standardSessionManager() { | |||
// 故意把session存活时间变短,清理更频繁 | |||
StandardSessionManager ism = new StandardSessionManager(); | |||
ism.setMaxInactiveInterval(1); | |||
ism.setProcessExpiresFrequency(1); | |||
ism.setBackgroundProcessorDelay(1); | |||
return new Object[][] { | |||
new Object[] { ism } | |||
}; | |||
} | |||
@Test(dataProvider = "standardSessionManager") | |||
public void testSessionClean1(StandardSessionManager ism) throws InterruptedException { | |||
// 两个同步请求,看是否处理完毕后会被清理掉 | |||
final WxCpMessageRouter router = new WxCpMessageRouter(null); | |||
router.setSessionManager(ism); | |||
router | |||
.rule().async(false).handler(new WxSessionMessageHandler()).next() | |||
.rule().async(false).handler(new WxSessionMessageHandler()).end(); | |||
WxCpXmlMessage msg = new WxCpXmlMessage(); | |||
msg.setFromUserName("abc"); | |||
router.route(msg); | |||
Thread.sleep(2000l); | |||
Assert.assertEquals(ism.getActiveSessions(), 0); | |||
} | |||
@Test(dataProvider = "standardSessionManager") | |||
public void testSessionClean2(StandardSessionManager ism) throws InterruptedException { | |||
// 1个同步,1个异步请求,看是否处理完毕后会被清理掉 | |||
{ | |||
final WxCpMessageRouter router = new WxCpMessageRouter(null); | |||
router.setSessionManager(ism); | |||
router | |||
.rule().async(false).handler(new WxSessionMessageHandler()).next() | |||
.rule().async(true).handler(new WxSessionMessageHandler()).end(); | |||
WxCpXmlMessage msg = new WxCpXmlMessage(); | |||
msg.setFromUserName("abc"); | |||
router.route(msg); | |||
Thread.sleep(2000l); | |||
Assert.assertEquals(ism.getActiveSessions(), 0); | |||
} | |||
{ | |||
final WxCpMessageRouter router = new WxCpMessageRouter(null); | |||
router.setSessionManager(ism); | |||
router | |||
.rule().async(true).handler(new WxSessionMessageHandler()).next() | |||
.rule().async(false).handler(new WxSessionMessageHandler()).end(); | |||
WxCpXmlMessage msg = new WxCpXmlMessage(); | |||
msg.setFromUserName("abc"); | |||
router.route(msg); | |||
Thread.sleep(2000l); | |||
Assert.assertEquals(ism.getActiveSessions(), 0); | |||
} | |||
} | |||
@Test(dataProvider = "standardSessionManager") | |||
public void testSessionClean3(StandardSessionManager ism) throws InterruptedException { | |||
// 2个异步请求,看是否处理完毕后会被清理掉 | |||
final WxCpMessageRouter router = new WxCpMessageRouter(null); | |||
router.setSessionManager(ism); | |||
router | |||
.rule().async(true).handler(new WxSessionMessageHandler()).next() | |||
.rule().async(true).handler(new WxSessionMessageHandler()).end(); | |||
WxCpXmlMessage msg = new WxCpXmlMessage(); | |||
msg.setFromUserName("abc"); | |||
router.route(msg); | |||
Thread.sleep(2000l); | |||
Assert.assertEquals(ism.getActiveSessions(), 0); | |||
} | |||
@Test(dataProvider = "standardSessionManager") | |||
public void testSessionClean4(StandardSessionManager ism) throws InterruptedException { | |||
// 一个同步请求,看是否处理完毕后会被清理掉 | |||
{ | |||
final WxCpMessageRouter router = new WxCpMessageRouter(null); | |||
router.setSessionManager(ism); | |||
router | |||
.rule().async(false).handler(new WxSessionMessageHandler()).end(); | |||
WxCpXmlMessage msg = new WxCpXmlMessage(); | |||
msg.setFromUserName("abc"); | |||
router.route(msg); | |||
Thread.sleep(2000l); | |||
Assert.assertEquals(ism.getActiveSessions(), 0); | |||
} | |||
{ | |||
final WxCpMessageRouter router = new WxCpMessageRouter(null); | |||
router.setSessionManager(ism); | |||
router | |||
.rule().async(true).handler(new WxSessionMessageHandler()).end(); | |||
WxCpXmlMessage msg = new WxCpXmlMessage(); | |||
msg.setFromUserName("abc"); | |||
router.route(msg); | |||
Thread.sleep(2000l); | |||
Assert.assertEquals(ism.getActiveSessions(), 0); | |||
} | |||
} | |||
public static class WxSessionMessageHandler implements WxCpMessageHandler { | |||
@Override | |||
public WxCpXmlOutMessage handle(WxCpXmlMessage wxMessage, Map<String, Object> context, WxCpService wxCpService, | |||
WxSessionManager sessionManager) { | |||
sessionManager.getSession(wxMessage.getFromUserName()); | |||
return null; | |||
} | |||
} | |||
} |
@@ -1,5 +1,6 @@ | |||
package me.chanjar.weixin.cp.demo; | |||
import me.chanjar.weixin.common.session.WxSessionManager; | |||
import me.chanjar.weixin.cp.api.*; | |||
import me.chanjar.weixin.cp.bean.WxCpXmlMessage; | |||
import me.chanjar.weixin.cp.bean.WxCpXmlOutMessage; | |||
@@ -46,7 +47,7 @@ public class WxCpDemoServer { | |||
WxCpMessageHandler handler = new WxCpMessageHandler() { | |||
@Override | |||
public WxCpXmlOutMessage handle(WxCpXmlMessage wxMessage, Map<String, Object> context, | |||
WxCpService wxCpService) { | |||
WxCpService wxCpService, WxSessionManager sessionManager) { | |||
WxCpXmlOutTextMessage m = WxCpXmlOutMessage | |||
.TEXT() | |||
.content("测试加密消息") | |||
@@ -60,7 +61,7 @@ public class WxCpDemoServer { | |||
WxCpMessageHandler oauth2handler = new WxCpMessageHandler() { | |||
@Override | |||
public WxCpXmlOutMessage handle(WxCpXmlMessage wxMessage, Map<String, Object> context, | |||
WxCpService wxCpService) { | |||
WxCpService wxCpService, WxSessionManager sessionManager) { | |||
String href = "<a href=\"" + wxCpService.oauth2buildAuthorizationUrl(null) | |||
+ "\">测试oauth2</a>"; | |||
return WxCpXmlOutMessage | |||
@@ -10,9 +10,6 @@ import javax.servlet.http.HttpServletResponse; | |||
import java.io.IOException; | |||
import java.util.Arrays; | |||
/** | |||
* Created by qianjia on 14/11/28. | |||
*/ | |||
public class WxCpOAuth2Servlet extends HttpServlet { | |||
protected WxCpService wxCpService; | |||
@@ -0,0 +1,16 @@ | |||
<configuration> | |||
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender"> | |||
<!-- encoders are assigned the type | |||
ch.qos.logback.classic.encoder.PatternLayoutEncoder by default --> | |||
<encoder> | |||
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern> | |||
</encoder> | |||
</appender> | |||
<root level="info"> | |||
<appender-ref ref="STDOUT" /> | |||
</root> | |||
<logger name="me.chanjar.weixin.cp" level="debug" /> | |||
</configuration> |
@@ -1,28 +1,29 @@ | |||
<!DOCTYPE suite SYSTEM "http://testng.org/testng-1.0.dtd" > | |||
<suite name="Weixin-java-tool-suite" verbose="1"> | |||
<test name="API_Test"> | |||
<classes> | |||
<class name="me.chanjar.weixin.cp.api.WxCpBaseAPITest" /> | |||
<class name="me.chanjar.weixin.cp.api.WxCpMessageAPITest" /> | |||
<class name="me.chanjar.weixin.cp.api.WxMenuAPITest" /> | |||
<class name="me.chanjar.weixin.cp.api.WxCpDepartAPITest" /> | |||
<class name="me.chanjar.weixin.cp.api.WxCpMediaAPITest" /> | |||
<class name="me.chanjar.weixin.cp.api.WxCpMessageRouterTest" /> | |||
<class name="me.chanjar.weixin.cp.api.WxCpTagAPITest" /> | |||
<class name="me.chanjar.weixin.cp.api.WxCpUserAPITest" /> | |||
</classes> | |||
</test> | |||
<test name="API_Test"> | |||
<classes> | |||
<class name="me.chanjar.weixin.cp.api.WxCpBusyRetryTest"/> | |||
<class name="me.chanjar.weixin.cp.api.WxCpBaseAPITest"/> | |||
<class name="me.chanjar.weixin.cp.api.WxCpMessageAPITest"/> | |||
<class name="me.chanjar.weixin.cp.api.WxMenuAPITest"/> | |||
<class name="me.chanjar.weixin.cp.api.WxCpDepartAPITest"/> | |||
<class name="me.chanjar.weixin.cp.api.WxCpMediaAPITest"/> | |||
<class name="me.chanjar.weixin.cp.api.WxCpMessageRouterTest"/> | |||
<class name="me.chanjar.weixin.cp.api.WxCpTagAPITest"/> | |||
<class name="me.chanjar.weixin.cp.api.WxCpUserAPITest"/> | |||
</classes> | |||
</test> | |||
<test name="Bean_Test"> | |||
<classes> | |||
<class name="me.chanjar.weixin.cp.bean.WxCpMessageTest" /> | |||
<class name="me.chanjar.weixin.cp.bean.WxCpXmlMessageTest" /> | |||
<class name="me.chanjar.weixin.cp.bean.WxCpXmlOutImageMessageTest" /> | |||
<class name="me.chanjar.weixin.cp.bean.WxCpXmlOutNewsMessageTest" /> | |||
<class name="me.chanjar.weixin.cp.bean.WxCpXmlOutVideoMessageTest" /> | |||
<class name="me.chanjar.weixin.cp.bean.WxCpXmlOutVoiceMessageTest" /> | |||
<class name="me.chanjar.weixin.cp.bean.WxCpXmlOutTextMessageTest" /> | |||
</classes> | |||
</test> | |||
<test name="Bean_Test"> | |||
<classes> | |||
<class name="me.chanjar.weixin.cp.bean.WxCpMessageTest"/> | |||
<class name="me.chanjar.weixin.cp.bean.WxCpXmlMessageTest"/> | |||
<class name="me.chanjar.weixin.cp.bean.WxCpXmlOutImageMessageTest"/> | |||
<class name="me.chanjar.weixin.cp.bean.WxCpXmlOutNewsMessageTest"/> | |||
<class name="me.chanjar.weixin.cp.bean.WxCpXmlOutVideoMessageTest"/> | |||
<class name="me.chanjar.weixin.cp.bean.WxCpXmlOutVoiceMessageTest"/> | |||
<class name="me.chanjar.weixin.cp.bean.WxCpXmlOutTextMessageTest"/> | |||
</classes> | |||
</test> | |||
</suite> |
@@ -152,7 +152,6 @@ public class WxMpInMemoryConfigStorage implements WxMpConfigStorage { | |||
this.http_proxy_password = http_proxy_password; | |||
} | |||
@Override | |||
public String toString() { | |||
return "WxMpInMemoryConfigStorage{" + | |||
@@ -166,6 +165,8 @@ public class WxMpInMemoryConfigStorage implements WxMpConfigStorage { | |||
", http_proxy_port=" + http_proxy_port + | |||
", http_proxy_username='" + http_proxy_username + '\'' + | |||
", http_proxy_password='" + http_proxy_password + '\'' + | |||
", jsapiTicket='" + jsapiTicket + '\'' + | |||
", jsapiTicketExpiresTime='" + jsapiTicketExpiresTime + '\'' + | |||
'}'; | |||
} | |||
@@ -1,5 +1,6 @@ | |||
package me.chanjar.weixin.mp.api; | |||
import me.chanjar.weixin.common.session.WxSessionManager; | |||
import me.chanjar.weixin.mp.bean.WxMpXmlMessage; | |||
import me.chanjar.weixin.mp.bean.WxMpXmlOutMessage; | |||
@@ -7,18 +8,21 @@ import java.util.Map; | |||
/** | |||
* 处理微信推送消息的处理器接口 | |||
* @author chanjarster | |||
* | |||
* @author Daniel Qian | |||
*/ | |||
public interface WxMpMessageHandler { | |||
/** | |||
* | |||
* @param wxMessage | |||
* @param context 上下文,如果handler或interceptor之间有信息要传递,可以用这个 | |||
* @param context 上下文,如果handler或interceptor之间有信息要传递,可以用这个 | |||
* @param wxMpService | |||
* @param sessionManager | |||
* @return xml格式的消息,如果在异步规则里处理的话,可以返回null | |||
*/ | |||
public WxMpXmlOutMessage handle(WxMpXmlMessage wxMessage, Map<String, Object> context, WxMpService wxMpService); | |||
public WxMpXmlOutMessage handle(WxMpXmlMessage wxMessage, | |||
Map<String, Object> context, | |||
WxMpService wxMpService, | |||
WxSessionManager sessionManager); | |||
} |
@@ -1,23 +1,29 @@ | |||
package me.chanjar.weixin.mp.api; | |||
import me.chanjar.weixin.common.session.WxSessionManager; | |||
import me.chanjar.weixin.mp.bean.WxMpXmlMessage; | |||
import java.util.Map; | |||
/** | |||
* 微信消息拦截器,可以用来做验证 | |||
* @author qianjia | |||
* | |||
* @author Daniel Qian | |||
*/ | |||
public interface WxMpMessageInterceptor { | |||
/** | |||
* 拦截微信消息 | |||
* | |||
* @param wxMessage | |||
* @param context 上下文,如果handler或interceptor之间有信息要传递,可以用这个 | |||
* @param context 上下文,如果handler或interceptor之间有信息要传递,可以用这个 | |||
* @param wxMpService | |||
* @return true代表OK,false代表不OK | |||
* @param sessionManager | |||
* @return true代表OK,false代表不OK | |||
*/ | |||
public boolean intercept(WxMpXmlMessage wxMessage, Map<String, Object> context, WxMpService wxMpService); | |||
public boolean intercept(WxMpXmlMessage wxMessage, | |||
Map<String, Object> context, | |||
WxMpService wxMpService, | |||
WxSessionManager sessionManager); | |||
} |
@@ -1,16 +1,22 @@ | |||
package me.chanjar.weixin.mp.api; | |||
import me.chanjar.weixin.common.util.WxMsgIdDuplicateChecker; | |||
import me.chanjar.weixin.common.util.WxMsgIdInMemoryDuplicateChecker; | |||
import me.chanjar.weixin.common.api.WxConsts; | |||
import me.chanjar.weixin.common.session.*; | |||
import me.chanjar.weixin.common.util.WxMessageDuplicateChecker; | |||
import me.chanjar.weixin.common.util.WxMessageInMemoryDuplicateChecker; | |||
import me.chanjar.weixin.mp.bean.WxMpXmlMessage; | |||
import me.chanjar.weixin.mp.bean.WxMpXmlOutMessage; | |||
import org.slf4j.Logger; | |||
import org.slf4j.LoggerFactory; | |||
import java.util.ArrayList; | |||
import java.util.HashMap; | |||
import java.util.List; | |||
import java.util.Map; | |||
import java.util.concurrent.ExecutionException; | |||
import java.util.concurrent.ExecutorService; | |||
import java.util.concurrent.Executors; | |||
import java.util.concurrent.Future; | |||
import java.util.regex.Pattern; | |||
/** | |||
@@ -38,12 +44,12 @@ import java.util.regex.Pattern; | |||
* router.route(message); | |||
* | |||
* </pre> | |||
* @author qianjia | |||
* | |||
*/ | |||
public class WxMpMessageRouter { | |||
private static final int DEFAULT_THREAD_POOL_SIZE = 20; | |||
protected final Logger log = LoggerFactory.getLogger(WxMpMessageRouter.class); | |||
private static final int DEFAULT_THREAD_POOL_SIZE = 100; | |||
private final List<Rule> rules = new ArrayList<Rule>(); | |||
@@ -51,22 +57,22 @@ public class WxMpMessageRouter { | |||
private ExecutorService executorService; | |||
private WxMsgIdDuplicateChecker wxMsgIdDuplicateChecker; | |||
private WxMessageDuplicateChecker messageDuplicateChecker; | |||
private WxSessionManager sessionManager; | |||
public WxMpMessageRouter(WxMpService wxMpService) { | |||
this.wxMpService = wxMpService; | |||
this.executorService = Executors.newFixedThreadPool(DEFAULT_THREAD_POOL_SIZE); | |||
this.wxMsgIdDuplicateChecker = new WxMsgIdInMemoryDuplicateChecker(); | |||
} | |||
public WxMpMessageRouter(WxMpService wxMpService, int threadPoolSize) { | |||
this.wxMpService = wxMpService; | |||
this.executorService = Executors.newFixedThreadPool(threadPoolSize); | |||
this.wxMsgIdDuplicateChecker = new WxMsgIdInMemoryDuplicateChecker(); | |||
this.messageDuplicateChecker = new WxMessageInMemoryDuplicateChecker(); | |||
this.sessionManager = new StandardSessionManager(); | |||
} | |||
/** | |||
* 设置自定义的ExecutorService | |||
* <pre> | |||
* 设置自定义的 {@link ExecutorService} | |||
* 如果不调用该方法,默认使用 Executors.newFixedThreadPool(100) | |||
* </pre> | |||
* @param executorService | |||
*/ | |||
public void setExecutorService(ExecutorService executorService) { | |||
@@ -74,11 +80,25 @@ public class WxMpMessageRouter { | |||
} | |||
/** | |||
* 设置自定义的WxMsgIdDuplicateChecker | |||
* @param wxMsgIdDuplicateChecker | |||
* <pre> | |||
* 设置自定义的 {@link me.chanjar.weixin.common.util.WxMessageDuplicateChecker} | |||
* 如果不调用该方法,默认使用 {@link me.chanjar.weixin.common.util.WxMessageInMemoryDuplicateChecker} | |||
* </pre> | |||
* @param messageDuplicateChecker | |||
*/ | |||
public void setWxMsgIdDuplicateChecker(WxMsgIdDuplicateChecker wxMsgIdDuplicateChecker) { | |||
this.wxMsgIdDuplicateChecker = wxMsgIdDuplicateChecker; | |||
public void setMessageDuplicateChecker(WxMessageDuplicateChecker messageDuplicateChecker) { | |||
this.messageDuplicateChecker = messageDuplicateChecker; | |||
} | |||
/** | |||
* <pre> | |||
* 设置自定义的{@link me.chanjar.weixin.common.session.WxSessionManager} | |||
* 如果不调用该方法,默认使用 {@linke SessionManagerImpl} | |||
* </pre> | |||
* @param sessionManager | |||
*/ | |||
public void setSessionManager(WxSessionManager sessionManager) { | |||
this.sessionManager = sessionManager; | |||
} | |||
/** | |||
@@ -86,7 +106,7 @@ public class WxMpMessageRouter { | |||
* @return | |||
*/ | |||
public Rule rule() { | |||
return new Rule(this, wxMpService); | |||
return new Rule(this, wxMpService, sessionManager); | |||
} | |||
/** | |||
@@ -94,7 +114,7 @@ public class WxMpMessageRouter { | |||
* @param wxMessage | |||
*/ | |||
public WxMpXmlOutMessage route(final WxMpXmlMessage wxMessage) { | |||
if (wxMsgIdDuplicateChecker.isDuplicate(wxMessage.getMsgId())) { | |||
if (isDuplicateMessage(wxMessage)) { | |||
// 如果是重复消息,那么就不做处理 | |||
return null; | |||
} | |||
@@ -109,33 +129,90 @@ public class WxMpMessageRouter { | |||
} | |||
} | |||
} | |||
if (matchRules.size() == 0) { | |||
return null; | |||
} | |||
WxMpXmlOutMessage res = null; | |||
final List<Future> futures = new ArrayList<Future>(); | |||
for (final Rule rule : matchRules) { | |||
// 返回最后一个非异步的rule的执行结果 | |||
if(rule.async) { | |||
executorService.submit(new Runnable() { | |||
public void run() { | |||
rule.service(wxMessage); | |||
} | |||
}); | |||
futures.add( | |||
executorService.submit(new Runnable() { | |||
public void run() { | |||
rule.service(wxMessage); | |||
} | |||
}) | |||
); | |||
} else { | |||
res = rule.service(wxMessage); | |||
// 在同步操作结束,session访问结束 | |||
log.debug("End session access: async=false, sessionId={}", wxMessage.getFromUserName()); | |||
sessionEndAccess(wxMessage); | |||
} | |||
} | |||
if (futures.size() > 0) { | |||
executorService.submit(new Runnable() { | |||
@Override | |||
public void run() { | |||
for (Future future : futures) { | |||
try { | |||
future.get(); | |||
log.debug("End session access: async=true, sessionId={}", wxMessage.getFromUserName()); | |||
// 异步操作结束,session访问结束 | |||
sessionEndAccess(wxMessage); | |||
} catch (InterruptedException e) { | |||
log.error("Error happened when wait task finish", e); | |||
} catch (ExecutionException e) { | |||
log.error("Error happened when wait task finish", e); | |||
} | |||
} | |||
} | |||
}); | |||
} | |||
return res; | |||
} | |||
protected boolean isDuplicateMessage(WxMpXmlMessage wxMessage) { | |||
String messageId = ""; | |||
if (wxMessage.getMsgId() == null) { | |||
messageId = wxMessage.getFromUserName() + "-" + String.valueOf(wxMessage.getCreateTime()); | |||
} else { | |||
messageId = String.valueOf(wxMessage.getMsgId()); | |||
} | |||
if (messageDuplicateChecker.isDuplicate(messageId)) { | |||
return true; | |||
} | |||
return false; | |||
} | |||
/** | |||
* 对session的访问结束 | |||
* @param wxMessage | |||
*/ | |||
protected void sessionEndAccess(WxMpXmlMessage wxMessage) { | |||
InternalSession session = ((InternalSessionManager)sessionManager).findSession(wxMessage.getFromUserName()); | |||
if (session != null) { | |||
session.endAccess(); | |||
} | |||
} | |||
public static class Rule { | |||
private final WxMpMessageRouter routerBuilder; | |||
private final WxMpService wxMpService; | |||
private final WxSessionManager sessionManager; | |||
private boolean async = true; | |||
private String fromUser; | |||
@@ -155,10 +232,11 @@ public class WxMpMessageRouter { | |||
private List<WxMpMessageHandler> handlers = new ArrayList<WxMpMessageHandler>(); | |||
private List<WxMpMessageInterceptor> interceptors = new ArrayList<WxMpMessageInterceptor>(); | |||
protected Rule(WxMpMessageRouter routerBuilder, WxMpService wxMpService) { | |||
protected Rule(WxMpMessageRouter routerBuilder, WxMpService wxMpService, WxSessionManager sessionManager) { | |||
this.routerBuilder = routerBuilder; | |||
this.wxMpService = wxMpService; | |||
this.sessionManager = sessionManager; | |||
} | |||
/** | |||
@@ -324,7 +402,7 @@ public class WxMpMessageRouter { | |||
Map<String, Object> context = new HashMap<String, Object>(); | |||
// 如果拦截器不通过 | |||
for (WxMpMessageInterceptor interceptor : this.interceptors) { | |||
if (!interceptor.intercept(wxMessage, context, wxMpService)) { | |||
if (!interceptor.intercept(wxMessage, context, wxMpService, sessionManager)) { | |||
return null; | |||
} | |||
} | |||
@@ -333,7 +411,7 @@ public class WxMpMessageRouter { | |||
WxMpXmlOutMessage res = null; | |||
for (WxMpMessageHandler handler : this.handlers) { | |||
// 返回最后handler的结果 | |||
res = handler.handle(wxMessage, context, wxMpService); | |||
res = handler.handle(wxMessage, context, wxMpService, sessionManager); | |||
} | |||
return res; | |||
} | |||
@@ -3,6 +3,8 @@ package me.chanjar.weixin.mp.api; | |||
import me.chanjar.weixin.common.bean.WxMenu; | |||
import me.chanjar.weixin.common.bean.result.WxMediaUploadResult; | |||
import me.chanjar.weixin.common.exception.WxErrorException; | |||
import me.chanjar.weixin.common.session.WxSession; | |||
import me.chanjar.weixin.common.session.WxSessionManager; | |||
import me.chanjar.weixin.common.util.http.RequestExecutor; | |||
import me.chanjar.weixin.mp.bean.*; | |||
import me.chanjar.weixin.mp.bean.result.*; | |||
@@ -456,9 +458,28 @@ public interface WxMpService { | |||
*/ | |||
public <T, E> T execute(RequestExecutor<T, E> executor, String uri, E data) throws WxErrorException; | |||
/** | |||
* 注入 {@link WxMpConfigStorage} 的实现 | |||
* @param wxConfigProvider | |||
*/ | |||
/** | |||
* 注入 {@link WxMpConfigStorage} 的实现 | |||
* @param wxConfigProvider | |||
*/ | |||
public void setWxMpConfigStorage(WxMpConfigStorage wxConfigProvider); | |||
/** | |||
* <pre> | |||
* 设置当微信系统响应系统繁忙时,要等待多少 retrySleepMillis(ms) * 2^(重试次数 - 1) 再发起重试 | |||
* 默认:1000ms | |||
* </pre> | |||
* @param retrySleepMillis | |||
*/ | |||
void setRetrySleepMillis(int retrySleepMillis); | |||
/** | |||
* <pre> | |||
* 设置当微信系统响应系统繁忙时,最大重试次数 | |||
* 默认:5次 | |||
* </pre> | |||
* @param maxRetryTimes | |||
*/ | |||
void setMaxRetryTimes(int maxRetryTimes); | |||
} |
@@ -10,6 +10,8 @@ import me.chanjar.weixin.common.bean.WxMenu; | |||
import me.chanjar.weixin.common.bean.result.WxError; | |||
import me.chanjar.weixin.common.bean.result.WxMediaUploadResult; | |||
import me.chanjar.weixin.common.exception.WxErrorException; | |||
import me.chanjar.weixin.common.session.StandardSessionManager; | |||
import me.chanjar.weixin.common.session.WxSessionManager; | |||
import me.chanjar.weixin.common.util.StringUtils; | |||
import me.chanjar.weixin.common.util.crypto.SHA1; | |||
import me.chanjar.weixin.common.util.fs.FileUtils; | |||
@@ -31,6 +33,8 @@ import org.apache.http.impl.client.BasicCredentialsProvider; | |||
import org.apache.http.impl.client.BasicResponseHandler; | |||
import org.apache.http.impl.client.CloseableHttpClient; | |||
import org.apache.http.impl.client.HttpClients; | |||
import org.slf4j.Logger; | |||
import org.slf4j.LoggerFactory; | |||
import java.io.File; | |||
import java.io.IOException; | |||
@@ -42,6 +46,8 @@ import java.util.UUID; | |||
public class WxMpServiceImpl implements WxMpService { | |||
protected final Logger log = LoggerFactory.getLogger(WxMpServiceImpl.class); | |||
/** | |||
* 全局的是否正在刷新access token的锁 | |||
*/ | |||
@@ -54,12 +60,16 @@ public class WxMpServiceImpl implements WxMpService { | |||
protected WxMpConfigStorage wxMpConfigStorage; | |||
protected final ThreadLocal<Integer> retryTimes = new ThreadLocal<Integer>(); | |||
protected CloseableHttpClient httpClient; | |||
protected HttpHost httpProxy; | |||
private int retrySleepMillis = 1000; | |||
private int maxRetryTimes = 5; | |||
protected WxSessionManager sessionManager = new StandardSessionManager(); | |||
public boolean checkSignature(String timestamp, String nonce, String signature) { | |||
try { | |||
return SHA1.gen(wxMpConfigStorage.getToken(), timestamp, nonce).equals(signature); | |||
@@ -439,7 +449,7 @@ public class WxMpServiceImpl implements WxMpService { | |||
return execute(new SimplePostRequestExecutor(), url, postData); | |||
} | |||
/** | |||
/** | |||
* 向微信端发送请求,在这里执行的策略是当发生access_token过期时才去刷新,然后重新执行请求,而不是全局定时请求 | |||
* @param executor | |||
* @param uri | |||
@@ -448,6 +458,33 @@ public class WxMpServiceImpl implements WxMpService { | |||
* @throws WxErrorException | |||
*/ | |||
public <T, E> T execute(RequestExecutor<T, E> executor, String uri, E data) throws WxErrorException { | |||
int retryTimes = 0; | |||
do { | |||
try { | |||
return executeInternal(executor, uri, data); | |||
} catch (WxErrorException e) { | |||
WxError error = e.getError(); | |||
/** | |||
* -1 系统繁忙, 1000ms后重试 | |||
*/ | |||
if (error.getErrorCode() == -1) { | |||
int sleepMillis = retrySleepMillis * (1 << retryTimes); | |||
try { | |||
log.debug("微信系统繁忙,{}ms 后重试(第{}次)", sleepMillis, retryTimes + 1); | |||
Thread.sleep(sleepMillis); | |||
} catch (InterruptedException e1) { | |||
throw new RuntimeException(e1); | |||
} | |||
} else { | |||
throw e; | |||
} | |||
} | |||
} while(++retryTimes < maxRetryTimes); | |||
throw new RuntimeException("微信服务端异常,超出重试次数"); | |||
} | |||
protected <T, E> T executeInternal(RequestExecutor<T, E> executor, String uri, E data) throws WxErrorException { | |||
String accessToken = getAccessToken(false); | |||
String uriWithAccessToken = uri; | |||
@@ -467,27 +504,6 @@ public class WxMpServiceImpl implements WxMpService { | |||
wxMpConfigStorage.expireAccessToken(); | |||
return execute(executor, uri, data); | |||
} | |||
/** | |||
* -1 系统繁忙, 1000ms后重试 | |||
*/ | |||
if (error.getErrorCode() == -1) { | |||
if(retryTimes.get() == null) { | |||
retryTimes.set(0); | |||
} | |||
if (retryTimes.get() > 4) { | |||
retryTimes.set(0); | |||
throw new RuntimeException("微信服务端异常,超出重试次数"); | |||
} | |||
int sleepMillis = 1000 * (1 << retryTimes.get()); | |||
try { | |||
System.out.println("微信系统繁忙," + sleepMillis + "ms后重试"); | |||
Thread.sleep(sleepMillis); | |||
retryTimes.set(retryTimes.get() + 1); | |||
return execute(executor, uri, data); | |||
} catch (InterruptedException e1) { | |||
throw new RuntimeException(e1); | |||
} | |||
} | |||
if (error.getErrorCode() != 0) { | |||
throw new WxErrorException(error); | |||
} | |||
@@ -533,4 +549,15 @@ public class WxMpServiceImpl implements WxMpService { | |||
} | |||
} | |||
@Override | |||
public void setRetrySleepMillis(int retrySleepMillis) { | |||
this.retrySleepMillis = retrySleepMillis; | |||
} | |||
@Override | |||
public void setMaxRetryTimes(int maxRetryTimes) { | |||
this.maxRetryTimes = maxRetryTimes; | |||
} | |||
} |
@@ -5,9 +5,6 @@ import me.chanjar.weixin.mp.util.json.WxMpGsonBuilder; | |||
import java.util.ArrayList; | |||
import java.util.List; | |||
/** | |||
* Created by qianjia on 14/11/1. | |||
*/ | |||
public class WxMpTemplateMessage { | |||
private String toUser; | |||
@@ -2,9 +2,6 @@ package me.chanjar.weixin.mp.bean.result; | |||
import me.chanjar.weixin.mp.util.json.WxMpGsonBuilder; | |||
/** | |||
* Created by qianjia on 14/11/26. | |||
*/ | |||
public class WxMpOAuth2AccessToken { | |||
private String accessToken; | |||
@@ -14,11 +14,6 @@ import me.chanjar.weixin.mp.bean.WxMpCustomMessage; | |||
import java.lang.reflect.Type; | |||
/** | |||
* | |||
* @author qianjia | |||
* | |||
*/ | |||
public class WxMpCustomMessageGsonAdapter implements JsonSerializer<WxMpCustomMessage> { | |||
public JsonElement serialize(WxMpCustomMessage message, Type typeOfSrc, JsonSerializationContext context) { | |||
@@ -14,11 +14,6 @@ import me.chanjar.weixin.mp.bean.WxMpGroup; | |||
import java.lang.reflect.Type; | |||
/** | |||
* | |||
* @author qianjia | |||
* | |||
*/ | |||
public class WxMpGroupGsonAdapter implements JsonSerializer<WxMpGroup>, JsonDeserializer<WxMpGroup> { | |||
public JsonElement serialize(WxMpGroup group, Type typeOfSrc, JsonSerializationContext context) { | |||
@@ -17,11 +17,6 @@ import me.chanjar.weixin.mp.bean.WxMpMassGroupMessage; | |||
import java.lang.reflect.Type; | |||
/** | |||
* | |||
* @author qianjia | |||
* | |||
*/ | |||
public class WxMpMassGroupMessageGsonAdapter implements JsonSerializer<WxMpMassGroupMessage> { | |||
public JsonElement serialize(WxMpMassGroupMessage message, Type typeOfSrc, JsonSerializationContext context) { | |||
@@ -13,11 +13,6 @@ import me.chanjar.weixin.mp.bean.WxMpMassNews; | |||
import java.lang.reflect.Type; | |||
/** | |||
* | |||
* @author qianjia | |||
* | |||
*/ | |||
public class WxMpMassNewsGsonAdapter implements JsonSerializer<WxMpMassNews> { | |||
public JsonElement serialize(WxMpMassNews message, Type typeOfSrc, JsonSerializationContext context) { | |||
@@ -14,11 +14,6 @@ import me.chanjar.weixin.mp.bean.WxMpMassOpenIdsMessage; | |||
import java.lang.reflect.Type; | |||
/** | |||
* | |||
* @author qianjia | |||
* | |||
*/ | |||
public class WxMpMassOpenIdsMessageGsonAdapter implements JsonSerializer<WxMpMassOpenIdsMessage> { | |||
public JsonElement serialize(WxMpMassOpenIdsMessage message, Type typeOfSrc, JsonSerializationContext context) { | |||
@@ -6,9 +6,6 @@ import me.chanjar.weixin.mp.bean.result.WxMpOAuth2AccessToken; | |||
import java.lang.reflect.Type; | |||
/** | |||
* Created by qianjia on 14/11/28. | |||
*/ | |||
public class WxMpOAuth2AccessTokenAdapter implements JsonDeserializer<WxMpOAuth2AccessToken> { | |||
public WxMpOAuth2AccessToken deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws | |||
@@ -17,9 +17,6 @@ import me.chanjar.weixin.mp.bean.WxMpTemplateMessage; | |||
import java.lang.reflect.Type; | |||
/** | |||
* @author qianjia | |||
*/ | |||
public class WxMpTemplateMessageGsonAdapter implements JsonSerializer<WxMpTemplateMessage> { | |||
public JsonElement serialize(WxMpTemplateMessage message, Type typeOfSrc, JsonSerializationContext context) { | |||
@@ -14,11 +14,6 @@ import me.chanjar.weixin.mp.bean.result.WxMpUser; | |||
import java.lang.reflect.Type; | |||
/** | |||
* | |||
* @author qianjia | |||
* | |||
*/ | |||
public class WxUserGsonAdapter implements JsonDeserializer<WxMpUser> { | |||
public WxMpUser deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException { | |||
@@ -14,11 +14,6 @@ import me.chanjar.weixin.mp.bean.result.WxMpUserList; | |||
import java.lang.reflect.Type; | |||
/** | |||
* | |||
* @author qianjia | |||
* | |||
*/ | |||
public class WxUserListGsonAdapter implements JsonDeserializer<WxMpUserList> { | |||
public WxMpUserList deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException { | |||
@@ -8,9 +8,6 @@ import java.io.InputStream; | |||
import java.util.HashMap; | |||
import java.util.Map; | |||
/** | |||
* Created by qianjia on 15/1/19. | |||
*/ | |||
public class XStreamTransformer { | |||
protected static final Map<Class, XStream> CLASS_2_XSTREAM_INSTANCE = configXStreamInstance(); | |||
@@ -0,0 +1,66 @@ | |||
package me.chanjar.weixin.mp.api; | |||
import me.chanjar.weixin.common.bean.result.WxError; | |||
import me.chanjar.weixin.common.exception.WxErrorException; | |||
import me.chanjar.weixin.common.util.http.RequestExecutor; | |||
import org.testng.annotations.DataProvider; | |||
import org.testng.annotations.Test; | |||
import java.util.concurrent.ExecutionException; | |||
import java.util.concurrent.ExecutorService; | |||
import java.util.concurrent.Executors; | |||
import java.util.concurrent.Future; | |||
@Test | |||
public class WxMpBusyRetryTest { | |||
@DataProvider(name="getService") | |||
public Object[][] getService() { | |||
WxMpService service = new WxMpServiceImpl() { | |||
@Override | |||
protected <T, E> T executeInternal(RequestExecutor<T, E> executor, String uri, E data) throws WxErrorException { | |||
WxError error = new WxError(); | |||
error.setErrorCode(-1); | |||
throw new WxErrorException(error); | |||
} | |||
}; | |||
service.setMaxRetryTimes(3); | |||
service.setRetrySleepMillis(500); | |||
return new Object[][] { | |||
new Object[] { service } | |||
}; | |||
} | |||
@Test(dataProvider = "getService", expectedExceptions = RuntimeException.class) | |||
public void testRetry(WxMpService service) throws WxErrorException { | |||
service.execute(null, null, null); | |||
} | |||
@Test(dataProvider = "getService") | |||
public void testRetryInThreadPool(final WxMpService service) throws InterruptedException, ExecutionException { | |||
// 当线程池中的线程复用的时候,还是能保证相同的重试次数 | |||
ExecutorService executorService = Executors.newFixedThreadPool(1); | |||
Runnable runnable = new Runnable() { | |||
@Override | |||
public void run() { | |||
try { | |||
System.out.println("====================="); | |||
System.out.println(Thread.currentThread().getName() + ": testRetry"); | |||
service.execute(null, null, null); | |||
} catch (WxErrorException e) { | |||
throw new RuntimeException(e); | |||
} catch (RuntimeException e) { | |||
// OK | |||
} | |||
} | |||
}; | |||
Future<?> submit1 = executorService.submit(runnable); | |||
Future<?> submit2 = executorService.submit(runnable); | |||
submit1.get(); | |||
submit2.get(); | |||
} | |||
} |
@@ -1,6 +1,8 @@ | |||
package me.chanjar.weixin.mp.api; | |||
import me.chanjar.weixin.common.api.WxConsts; | |||
import me.chanjar.weixin.common.session.StandardSessionManager; | |||
import me.chanjar.weixin.common.session.WxSessionManager; | |||
import me.chanjar.weixin.mp.bean.WxMpXmlMessage; | |||
import me.chanjar.weixin.mp.bean.WxMpXmlOutMessage; | |||
import org.testng.Assert; | |||
@@ -67,7 +69,8 @@ public class WxMpMessageRouterTest { | |||
final WxMpMessageRouter router = new WxMpMessageRouter(null); | |||
router.rule().handler(new WxMpMessageHandler() { | |||
@Override | |||
public WxMpXmlOutMessage handle(WxMpXmlMessage wxMessage, Map<String, Object> context, WxMpService wxMpService) { | |||
public WxMpXmlOutMessage handle(WxMpXmlMessage wxMessage, Map<String, Object> context, WxMpService wxMpService, | |||
WxSessionManager sessionManager) { | |||
return null; | |||
} | |||
}).end(); | |||
@@ -149,11 +152,144 @@ public class WxMpMessageRouterTest { | |||
} | |||
@Override | |||
public WxMpXmlOutMessage handle(WxMpXmlMessage wxMessage, Map<String, Object> context, WxMpService wxMpService) { | |||
public WxMpXmlOutMessage handle(WxMpXmlMessage wxMessage, Map<String, Object> context, WxMpService wxMpService, | |||
WxSessionManager sessionManager) { | |||
sb.append(this.echoStr).append(','); | |||
return null; | |||
} | |||
} | |||
@DataProvider | |||
public Object[][] standardSessionManager() { | |||
// 故意把session存活时间变短,清理更频繁 | |||
StandardSessionManager ism = new StandardSessionManager(); | |||
ism.setMaxInactiveInterval(1); | |||
ism.setProcessExpiresFrequency(1); | |||
ism.setBackgroundProcessorDelay(1); | |||
return new Object[][] { | |||
new Object[] { ism } | |||
}; | |||
} | |||
@Test(dataProvider = "standardSessionManager") | |||
public void testSessionClean1(StandardSessionManager ism) throws InterruptedException { | |||
// 两个同步请求,看是否处理完毕后会被清理掉 | |||
final WxMpMessageRouter router = new WxMpMessageRouter(null); | |||
router.setSessionManager(ism); | |||
router | |||
.rule().async(false).handler(new WxSessionMessageHandler()).next() | |||
.rule().async(false).handler(new WxSessionMessageHandler()).end(); | |||
WxMpXmlMessage msg = new WxMpXmlMessage(); | |||
msg.setFromUserName("abc"); | |||
router.route(msg); | |||
Thread.sleep(2000l); | |||
Assert.assertEquals(ism.getActiveSessions(), 0); | |||
} | |||
@Test(dataProvider = "standardSessionManager") | |||
public void testSessionClean2(StandardSessionManager ism) throws InterruptedException { | |||
// 1个同步,1个异步请求,看是否处理完毕后会被清理掉 | |||
{ | |||
final WxMpMessageRouter router = new WxMpMessageRouter(null); | |||
router.setSessionManager(ism); | |||
router | |||
.rule().async(false).handler(new WxSessionMessageHandler()).next() | |||
.rule().async(true).handler(new WxSessionMessageHandler()).end(); | |||
WxMpXmlMessage msg = new WxMpXmlMessage(); | |||
msg.setFromUserName("abc"); | |||
router.route(msg); | |||
Thread.sleep(2000l); | |||
Assert.assertEquals(ism.getActiveSessions(), 0); | |||
} | |||
{ | |||
final WxMpMessageRouter router = new WxMpMessageRouter(null); | |||
router.setSessionManager(ism); | |||
router | |||
.rule().async(true).handler(new WxSessionMessageHandler()).next() | |||
.rule().async(false).handler(new WxSessionMessageHandler()).end(); | |||
WxMpXmlMessage msg = new WxMpXmlMessage(); | |||
msg.setFromUserName("abc"); | |||
router.route(msg); | |||
Thread.sleep(2000l); | |||
Assert.assertEquals(ism.getActiveSessions(), 0); | |||
} | |||
} | |||
@Test(dataProvider = "standardSessionManager") | |||
public void testSessionClean3(StandardSessionManager ism) throws InterruptedException { | |||
// 2个异步请求,看是否处理完毕后会被清理掉 | |||
final WxMpMessageRouter router = new WxMpMessageRouter(null); | |||
router.setSessionManager(ism); | |||
router | |||
.rule().async(true).handler(new WxSessionMessageHandler()).next() | |||
.rule().async(true).handler(new WxSessionMessageHandler()).end(); | |||
WxMpXmlMessage msg = new WxMpXmlMessage(); | |||
msg.setFromUserName("abc"); | |||
router.route(msg); | |||
Thread.sleep(2000l); | |||
Assert.assertEquals(ism.getActiveSessions(), 0); | |||
} | |||
@Test(dataProvider = "standardSessionManager") | |||
public void testSessionClean4(StandardSessionManager ism) throws InterruptedException { | |||
// 一个同步请求,看是否处理完毕后会被清理掉 | |||
{ | |||
final WxMpMessageRouter router = new WxMpMessageRouter(null); | |||
router.setSessionManager(ism); | |||
router | |||
.rule().async(false).handler(new WxSessionMessageHandler()).end(); | |||
WxMpXmlMessage msg = new WxMpXmlMessage(); | |||
msg.setFromUserName("abc"); | |||
router.route(msg); | |||
Thread.sleep(2000l); | |||
Assert.assertEquals(ism.getActiveSessions(), 0); | |||
} | |||
{ | |||
final WxMpMessageRouter router = new WxMpMessageRouter(null); | |||
router.setSessionManager(ism); | |||
router | |||
.rule().async(true).handler(new WxSessionMessageHandler()).end(); | |||
WxMpXmlMessage msg = new WxMpXmlMessage(); | |||
msg.setFromUserName("abc"); | |||
router.route(msg); | |||
Thread.sleep(2000l); | |||
Assert.assertEquals(ism.getActiveSessions(), 0); | |||
} | |||
} | |||
public static class WxSessionMessageHandler implements WxMpMessageHandler { | |||
@Override | |||
public WxMpXmlOutMessage handle(WxMpXmlMessage wxMessage, Map<String, Object> context, WxMpService wxMpService, | |||
WxSessionManager sessionManager) { | |||
sessionManager.getSession(wxMessage.getFromUserName()); | |||
return null; | |||
} | |||
} | |||
} |
@@ -3,6 +3,7 @@ package me.chanjar.weixin.mp.demo; | |||
import me.chanjar.weixin.common.api.WxConsts; | |||
import me.chanjar.weixin.common.bean.result.WxMediaUploadResult; | |||
import me.chanjar.weixin.common.exception.WxErrorException; | |||
import me.chanjar.weixin.common.session.WxSessionManager; | |||
import me.chanjar.weixin.mp.api.*; | |||
import me.chanjar.weixin.mp.bean.WxMpXmlMessage; | |||
import me.chanjar.weixin.mp.bean.WxMpXmlOutImageMessage; | |||
@@ -51,7 +52,7 @@ public class WxMpDemoServer { | |||
WxMpMessageHandler textHandler = new WxMpMessageHandler() { | |||
@Override | |||
public WxMpXmlOutMessage handle(WxMpXmlMessage wxMessage, Map<String, Object> context, | |||
WxMpService wxMpService) { | |||
WxMpService wxMpService, WxSessionManager sessionManager) { | |||
WxMpXmlOutTextMessage m | |||
= WxMpXmlOutMessage.TEXT().content("测试加密消息").fromUser(wxMessage.getToUserName()) | |||
.toUser(wxMessage.getFromUserName()).build(); | |||
@@ -62,7 +63,7 @@ public class WxMpDemoServer { | |||
WxMpMessageHandler imageHandler = new WxMpMessageHandler() { | |||
@Override | |||
public WxMpXmlOutMessage handle(WxMpXmlMessage wxMessage, Map<String, Object> context, | |||
WxMpService wxMpService) { | |||
WxMpService wxMpService, WxSessionManager sessionManager) { | |||
try { | |||
WxMediaUploadResult wxMediaUploadResult = wxMpService | |||
.mediaUpload(WxConsts.MEDIA_IMAGE, WxConsts.FILE_JPG, ClassLoader.getSystemResourceAsStream("mm.jpeg")); | |||
@@ -86,7 +87,7 @@ public class WxMpDemoServer { | |||
WxMpMessageHandler oauth2handler = new WxMpMessageHandler() { | |||
@Override | |||
public WxMpXmlOutMessage handle(WxMpXmlMessage wxMessage, Map<String, Object> context, | |||
WxMpService wxMpService) { | |||
WxMpService wxMpService, WxSessionManager sessionManager) { | |||
String href = "<a href=\"" + wxMpService.oauth2buildAuthorizationUrl(WxConsts.OAUTH2_SCOPE_USER_INFO, null) | |||
+ "\">测试oauth2</a>"; | |||
return WxMpXmlOutMessage | |||
@@ -0,0 +1,16 @@ | |||
<configuration> | |||
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender"> | |||
<!-- encoders are assigned the type | |||
ch.qos.logback.classic.encoder.PatternLayoutEncoder by default --> | |||
<encoder> | |||
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern> | |||
</encoder> | |||
</appender> | |||
<root level="info"> | |||
<appender-ref ref="STDOUT" /> | |||
</root> | |||
<logger name="me.chanjar.weixin.mp" level="debug" /> | |||
</configuration> |
@@ -3,6 +3,7 @@ | |||
<suite name="Weixin-java-tool-suite" verbose="1"> | |||
<test name="API_Test"> | |||
<classes> | |||
<class name="me.chanjar.weixin.mp.api.WxMpBusyRetryTest" /> | |||
<class name="me.chanjar.weixin.mp.api.WxMpBaseAPITest" /> | |||
<class name="me.chanjar.weixin.mp.api.WxMpCustomMessageAPITest" /> | |||
<class name="me.chanjar.weixin.mp.api.WxMpMenuAPITest" /> | |||