mirror of
https://gitee.com/binary/weixin-java-tools.git
synced 2025-05-03 20:27:46 +08:00
Merge branch 'feature/session2' into develop
This commit is contained in:
commit
8cb709fa70
419
pom.xml
419
pom.xml
@ -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>
|
||||
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>
|
||||
|
||||
<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>
|
||||
|
||||
<licenses>
|
||||
<license>
|
||||
<name>The Apache License, Version 2.0</name>
|
||||
<url>http://www.apache.org/licenses/LICENSE-2.0.txt</url>
|
||||
</license>
|
||||
</licenses>
|
||||
<modules>
|
||||
<module>weixin-java-common</module>
|
||||
<module>weixin-java-cp</module>
|
||||
<module>weixin-java-mp</module>
|
||||
</modules>
|
||||
|
||||
<developers>
|
||||
<developer>
|
||||
<name>Daniel Qian</name>
|
||||
<email>chanjarster@gmail.com</email>
|
||||
</developer>
|
||||
</developers>
|
||||
<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>
|
||||
|
||||
<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>
|
||||
|
||||
<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);
|
||||
}
|
||||
|
16
weixin-java-common/src/test/resources/logback-test.xml
Normal file
16
weixin-java-common/src/test/resources/logback-test.xml
Normal file
@ -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 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 +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;
|
||||
|
16
weixin-java-cp/src/test/resources/logback-test.xml
Normal file
16
weixin-java-cp/src/test/resources/logback-test.xml
Normal file
@ -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
|
||||
|
16
weixin-java-mp/src/test/resources/logback-test.xml
Normal file
16
weixin-java-mp/src/test/resources/logback-test.xml
Normal file
@ -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" />
|
||||
|
Loading…
Reference in New Issue
Block a user