| @@ -1,219 +1,232 @@ | |||||
| <?xml version="1.0"?> | <?xml version="1.0"?> | ||||
| <project | <project | ||||
| xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd" | |||||
| xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> | |||||
| <modelVersion>4.0.0</modelVersion> | |||||
| <groupId>me.chanjar</groupId> | |||||
| <artifactId>weixin-java-parent</artifactId> | |||||
| <version>1.0.7-SNAPSHOT</version> | |||||
| <packaging>pom</packaging> | |||||
| <name>WeiXin Java Tools - Parent</name> | |||||
| <description>微信公众号、企业号上级POM</description> | |||||
| <url>https://github.com/chanjarster/weixin-java-tools</url> | |||||
| xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd" | |||||
| xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> | |||||
| <modelVersion>4.0.0</modelVersion> | |||||
| <groupId>me.chanjar</groupId> | |||||
| <artifactId>weixin-java-parent</artifactId> | |||||
| <version>1.0.7-SNAPSHOT</version> | |||||
| <packaging>pom</packaging> | |||||
| <name>WeiXin Java Tools - Parent</name> | |||||
| <description>微信公众号、企业号上级POM</description> | |||||
| <url>https://github.com/chanjarster/weixin-java-tools</url> | |||||
| <licenses> | |||||
| <license> | |||||
| <name>The Apache License, Version 2.0</name> | |||||
| <url>http://www.apache.org/licenses/LICENSE-2.0.txt</url> | |||||
| </license> | |||||
| </licenses> | |||||
| <licenses> | |||||
| <license> | |||||
| <name>The Apache License, Version 2.0</name> | |||||
| <url>http://www.apache.org/licenses/LICENSE-2.0.txt</url> | |||||
| </license> | |||||
| </licenses> | |||||
| <developers> | |||||
| <developer> | |||||
| <name>Daniel Qian</name> | |||||
| <email>chanjarster@gmail.com</email> | |||||
| </developer> | |||||
| </developers> | |||||
| <developers> | |||||
| <developer> | |||||
| <name>Daniel Qian</name> | |||||
| <email>chanjarster@gmail.com</email> | |||||
| </developer> | |||||
| </developers> | |||||
| <scm> | |||||
| <connection>scm:git:https://github.com/chanjarster/weixin-java-tools.git</connection> | |||||
| <developerConnection>scm:git:git@github.com:chanjarster/weixin-java-tools.git</developerConnection> | |||||
| <url>https://github.com/chanjarster/weixin-java-tools</url> | |||||
| </scm> | |||||
| <scm> | |||||
| <connection>scm:git:https://github.com/chanjarster/weixin-java-tools.git</connection> | |||||
| <developerConnection>scm:git:git@github.com:chanjarster/weixin-java-tools.git</developerConnection> | |||||
| <url>https://github.com/chanjarster/weixin-java-tools</url> | |||||
| </scm> | |||||
| <modules> | |||||
| <module>weixin-java-common</module> | |||||
| <module>weixin-java-cp</module> | |||||
| <module>weixin-java-mp</module> | |||||
| </modules> | |||||
| <modules> | |||||
| <module>weixin-java-common</module> | |||||
| <module>weixin-java-cp</module> | |||||
| <module>weixin-java-mp</module> | |||||
| </modules> | |||||
| <properties> | |||||
| <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> | |||||
| <downloadJavadocs>true</downloadJavadocs> | |||||
| <downloadSources>true</downloadSources> | |||||
| <httpclient.version>4.3.5</httpclient.version> | |||||
| <slf4j.version>1.7.10</slf4j.version> | |||||
| <logback.version>1.1.2</logback.version> | |||||
| </properties> | |||||
| <properties> | |||||
| <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> | |||||
| <downloadJavadocs>true</downloadJavadocs> | |||||
| <downloadSources>true</downloadSources> | |||||
| <httpclient.version>4.3.5</httpclient.version> | |||||
| </properties> | |||||
| <dependencies> | |||||
| <dependency> | |||||
| <groupId>org.slf4j</groupId> | |||||
| <artifactId>slf4j-api</artifactId> | |||||
| <version>${slf4j.version}</version> | |||||
| </dependency> | |||||
| <dependency> | |||||
| <groupId>ch.qos.logback</groupId> | |||||
| <artifactId>logback-classic</artifactId> | |||||
| <version>${logback.version}</version> | |||||
| <scope>test</scope> | |||||
| </dependency> | |||||
| <dependency> | |||||
| <groupId>org.apache.httpcomponents</groupId> | |||||
| <artifactId>fluent-hc</artifactId> | |||||
| <version>${httpclient.version}</version> | |||||
| </dependency> | |||||
| <dependency> | |||||
| <groupId>org.apache.httpcomponents</groupId> | |||||
| <artifactId>httpmime</artifactId> | |||||
| <version>${httpclient.version}</version> | |||||
| </dependency> | |||||
| <dependency> | |||||
| <groupId>com.google.code.gson</groupId> | |||||
| <artifactId>gson</artifactId> | |||||
| <version>2.2.2</version> | |||||
| </dependency> | |||||
| <dependency> | |||||
| <groupId>commons-codec</groupId> | |||||
| <artifactId>commons-codec</artifactId> | |||||
| <version>1.9</version> | |||||
| </dependency> | |||||
| <dependency> | |||||
| <groupId>commons-io</groupId> | |||||
| <artifactId>commons-io</artifactId> | |||||
| <version>2.4</version> | |||||
| </dependency> | |||||
| </dependencies> | |||||
| <dependencyManagement> | |||||
| <dependencies> | <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> | </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> | <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> | </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> | </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.Map; | ||||
| import java.util.concurrent.ConcurrentHashMap; | import java.util.concurrent.ConcurrentHashMap; | ||||
| import java.util.concurrent.atomic.AtomicBoolean; | |||||
| /** | /** | ||||
| * <pre> | * <pre> | ||||
| @@ -9,7 +10,7 @@ import java.util.concurrent.ConcurrentHashMap; | |||||
| * 将每个消息id保存在内存里,每隔5秒清理已经过期的消息id,每个消息id的过期时间是15秒 | * 将每个消息id保存在内存里,每隔5秒清理已经过期的消息id,每个消息id的过期时间是15秒 | ||||
| * </pre> | * </pre> | ||||
| */ | */ | ||||
| public class WxMsgIdInMemoryDuplicateChecker implements WxMsgIdDuplicateChecker { | |||||
| public class WxMessageInMemoryDuplicateChecker implements WxMessageDuplicateChecker { | |||||
| /** | /** | ||||
| * 一个消息ID在内存的过期时间:15秒 | * 一个消息ID在内存的过期时间:15秒 | ||||
| @@ -21,7 +22,15 @@ public class WxMsgIdInMemoryDuplicateChecker implements WxMsgIdDuplicateChecker | |||||
| */ | */ | ||||
| private final Long clearPeriod; | 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构造函数 | * WxMsgIdInMemoryDuplicateChecker构造函数 | ||||
| @@ -30,10 +39,9 @@ public class WxMsgIdInMemoryDuplicateChecker implements WxMsgIdDuplicateChecker | |||||
| * 每隔多少周期检查消息ID是否过期:5秒 | * 每隔多少周期检查消息ID是否过期:5秒 | ||||
| * </pre> | * </pre> | ||||
| */ | */ | ||||
| public WxMsgIdInMemoryDuplicateChecker() { | |||||
| public WxMessageInMemoryDuplicateChecker() { | |||||
| this.timeToLive = 15 * 1000l; | this.timeToLive = 15 * 1000l; | ||||
| this.clearPeriod = 5 * 1000l; | this.clearPeriod = 5 * 1000l; | ||||
| this.start(); | |||||
| } | } | ||||
| /** | /** | ||||
| @@ -41,13 +49,15 @@ public class WxMsgIdInMemoryDuplicateChecker implements WxMsgIdDuplicateChecker | |||||
| * @param timeToLive 一个消息ID在内存的过期时间:毫秒 | * @param timeToLive 一个消息ID在内存的过期时间:毫秒 | ||||
| * @param clearPeriod 每隔多少周期检查消息ID是否过期:毫秒 | * @param clearPeriod 每隔多少周期检查消息ID是否过期:毫秒 | ||||
| */ | */ | ||||
| public WxMsgIdInMemoryDuplicateChecker(Long timeToLive, Long clearPeriod) { | |||||
| public WxMessageInMemoryDuplicateChecker(Long timeToLive, Long clearPeriod) { | |||||
| this.timeToLive = timeToLive; | this.timeToLive = timeToLive; | ||||
| this.clearPeriod = clearPeriod; | this.clearPeriod = clearPeriod; | ||||
| this.start(); | |||||
| } | } | ||||
| private void start() { | |||||
| protected void checkBackgroundProcessStarted() { | |||||
| if (backgroundProcessStarted.getAndSet(true)) { | |||||
| return; | |||||
| } | |||||
| Thread t = new Thread(new Runnable() { | Thread t = new Thread(new Runnable() { | ||||
| @Override | @Override | ||||
| public void run() { | public void run() { | ||||
| @@ -55,7 +65,7 @@ public class WxMsgIdInMemoryDuplicateChecker implements WxMsgIdDuplicateChecker | |||||
| while (true) { | while (true) { | ||||
| Thread.sleep(clearPeriod); | Thread.sleep(clearPeriod); | ||||
| Long now = System.currentTimeMillis(); | 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) { | if (now - entry.getValue() > timeToLive) { | ||||
| msgId2Timestamp.entrySet().remove(entry); | msgId2Timestamp.entrySet().remove(entry); | ||||
| } | } | ||||
| @@ -71,8 +81,12 @@ public class WxMsgIdInMemoryDuplicateChecker implements WxMsgIdDuplicateChecker | |||||
| } | } | ||||
| @Override | @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) { | if (timestamp == null) { | ||||
| // 第一次接收到这个消息 | // 第一次接收到这个消息 | ||||
| return false; | 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; | import com.thoughtworks.xstream.converters.basic.StringConverter; | ||||
| /** | |||||
| * Created by qianjia on 15/1/19. | |||||
| */ | |||||
| public class XStreamCDataConverter extends StringConverter { | public class XStreamCDataConverter extends StringConverter { | ||||
| @Override | @Override | ||||
| @@ -14,9 +14,6 @@ import com.thoughtworks.xstream.security.PrimitiveTypePermission; | |||||
| import java.io.Writer; | import java.io.Writer; | ||||
| /** | |||||
| * Created by qianjia on 15/1/19. | |||||
| */ | |||||
| public class XStreamInitializer { | public class XStreamInitializer { | ||||
| public static XStream getInstance() { | public static XStream getInstance() { | ||||
| @@ -1,8 +1,5 @@ | |||||
| package me.chanjar.weixin.common.util.xml; | package me.chanjar.weixin.common.util.xml; | ||||
| /** | |||||
| * Created by qianjia on 15/1/19. | |||||
| */ | |||||
| public class XStreamMediaIdConverter extends XStreamCDataConverter { | public class XStreamMediaIdConverter extends XStreamCDataConverter { | ||||
| @Override | @Override | ||||
| public String toString(Object obj) { | 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.Assert; | ||||
| import org.testng.annotations.Test; | import org.testng.annotations.Test; | ||||
| /** | |||||
| * Created by qianjia on 15/1/20. | |||||
| */ | |||||
| @Test | @Test | ||||
| public class WxMsgIdInMemoryDuplicateCheckerTest { | |||||
| public class WxMessageInMemoryDuplicateCheckerTest { | |||||
| public void test() throws InterruptedException { | public void test() throws InterruptedException { | ||||
| Long[] msgIds = new Long[] { 1l, 2l, 3l, 4l, 5l, 6l, 7l, 8l }; | 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) { | for (Long msgId : msgIds) { | ||||
| boolean result = checker.isDuplicate(msgId); | |||||
| boolean result = checker.isDuplicate(String.valueOf(msgId)); | |||||
| Assert.assertFalse(result); | Assert.assertFalse(result); | ||||
| } | } | ||||
| // 过1秒再检查 | // 过1秒再检查 | ||||
| Thread.sleep(1000l); | Thread.sleep(1000l); | ||||
| for (Long msgId : msgIds) { | for (Long msgId : msgIds) { | ||||
| boolean result = checker.isDuplicate(msgId); | |||||
| boolean result = checker.isDuplicate(String.valueOf(msgId)); | |||||
| Assert.assertTrue(result); | Assert.assertTrue(result); | ||||
| } | } | ||||
| // 过1.5秒再检查 | // 过1.5秒再检查 | ||||
| Thread.sleep(1500l); | Thread.sleep(1500l); | ||||
| for (Long msgId : msgIds) { | for (Long msgId : msgIds) { | ||||
| boolean result = checker.isDuplicate(msgId); | |||||
| boolean result = checker.isDuplicate(String.valueOf(msgId)); | |||||
| Assert.assertFalse(result); | Assert.assertFalse(result); | ||||
| } | } | ||||
| @@ -0,0 +1,16 @@ | |||||
| <configuration> | |||||
| <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender"> | |||||
| <!-- encoders are assigned the type | |||||
| ch.qos.logback.classic.encoder.PatternLayoutEncoder by default --> | |||||
| <encoder> | |||||
| <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern> | |||||
| </encoder> | |||||
| </appender> | |||||
| <root level="info"> | |||||
| <appender-ref ref="STDOUT" /> | |||||
| </root> | |||||
| <logger name="me.chanjar.weixin.common" level="debug" /> | |||||
| </configuration> | |||||
| @@ -1,13 +1,14 @@ | |||||
| <!DOCTYPE suite SYSTEM "http://testng.org/testng-1.0.dtd" > | <!DOCTYPE suite SYSTEM "http://testng.org/testng-1.0.dtd" > | ||||
| <suite name="Weixin-java-tool-suite" verbose="1"> | <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> | </suite> | ||||
| @@ -37,11 +37,11 @@ public class WxCpInMemoryConfigStorage implements WxCpConfigStorage { | |||||
| this.expiresTime = 0; | this.expiresTime = 0; | ||||
| } | } | ||||
| public void updateAccessToken(WxAccessToken accessToken) { | |||||
| public synchronized void updateAccessToken(WxAccessToken accessToken) { | |||||
| updateAccessToken(accessToken.getAccessToken(), accessToken.getExpiresIn()); | updateAccessToken(accessToken.getAccessToken(), accessToken.getExpiresIn()); | ||||
| } | } | ||||
| public void updateAccessToken(String accessToken, int expiresInSeconds) { | |||||
| public synchronized void updateAccessToken(String accessToken, int expiresInSeconds) { | |||||
| this.accessToken = accessToken; | this.accessToken = accessToken; | ||||
| this.expiresTime = System.currentTimeMillis() + (expiresInSeconds - 200) * 1000l; | this.expiresTime = System.currentTimeMillis() + (expiresInSeconds - 200) * 1000l; | ||||
| } | } | ||||
| @@ -1,5 +1,7 @@ | |||||
| package me.chanjar.weixin.cp.api; | 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.WxCpXmlMessage; | ||||
| import me.chanjar.weixin.cp.bean.WxCpXmlOutMessage; | import me.chanjar.weixin.cp.bean.WxCpXmlOutMessage; | ||||
| @@ -7,18 +9,21 @@ import java.util.Map; | |||||
| /** | /** | ||||
| * 处理微信推送消息的处理器接口 | * 处理微信推送消息的处理器接口 | ||||
| * @author Daniel Qian | |||||
| * | * | ||||
| * @author Daniel Qian | |||||
| */ | */ | ||||
| public interface WxCpMessageHandler { | public interface WxCpMessageHandler { | ||||
| /** | /** | ||||
| * | |||||
| * @param wxMessage | * @param wxMessage | ||||
| * @param context 上下文,如果handler或interceptor之间有信息要传递,可以用这个 | |||||
| * @param context 上下文,如果handler或interceptor之间有信息要传递,可以用这个 | |||||
| * @param wxCpService | * @param wxCpService | ||||
| * @param sessionManager | |||||
| * @return xml格式的消息,如果在异步规则里处理的话,可以返回null | * @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; | package me.chanjar.weixin.cp.api; | ||||
| import me.chanjar.weixin.common.session.WxSessionManager; | |||||
| import me.chanjar.weixin.cp.bean.WxCpXmlMessage; | import me.chanjar.weixin.cp.bean.WxCpXmlMessage; | ||||
| import java.util.Map; | import java.util.Map; | ||||
| /** | /** | ||||
| * 微信消息拦截器,可以用来做验证 | * 微信消息拦截器,可以用来做验证 | ||||
| * @author Daniel Qian | |||||
| * | * | ||||
| * @author Daniel Qian | |||||
| */ | */ | ||||
| public interface WxCpMessageInterceptor { | public interface WxCpMessageInterceptor { | ||||
| /** | /** | ||||
| * 拦截微信消息 | * 拦截微信消息 | ||||
| * | |||||
| * @param wxMessage | * @param wxMessage | ||||
| * @param context 上下文,如果handler或interceptor之间有信息要传递,可以用这个 | |||||
| * @param context 上下文,如果handler或interceptor之间有信息要传递,可以用这个 | |||||
| * @param wxCpService | * @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; | 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.WxCpXmlMessage; | ||||
| import me.chanjar.weixin.cp.bean.WxCpXmlOutMessage; | import me.chanjar.weixin.cp.bean.WxCpXmlOutMessage; | ||||
| import org.slf4j.Logger; | |||||
| import org.slf4j.LoggerFactory; | |||||
| import java.util.ArrayList; | import java.util.ArrayList; | ||||
| import java.util.HashMap; | import java.util.HashMap; | ||||
| import java.util.List; | import java.util.List; | ||||
| import java.util.Map; | import java.util.Map; | ||||
| import java.util.concurrent.ExecutionException; | |||||
| import java.util.concurrent.ExecutorService; | import java.util.concurrent.ExecutorService; | ||||
| import java.util.concurrent.Executors; | import java.util.concurrent.Executors; | ||||
| import java.util.concurrent.Future; | |||||
| import java.util.regex.Pattern; | import java.util.regex.Pattern; | ||||
| /** | /** | ||||
| @@ -43,7 +48,9 @@ import java.util.regex.Pattern; | |||||
| */ | */ | ||||
| public class WxCpMessageRouter { | 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>(); | private final List<Rule> rules = new ArrayList<Rule>(); | ||||
| @@ -51,22 +58,22 @@ public class WxCpMessageRouter { | |||||
| private ExecutorService executorService; | private ExecutorService executorService; | ||||
| private WxMsgIdDuplicateChecker wxMsgIdDuplicateChecker; | |||||
| private WxMessageDuplicateChecker messageDuplicateChecker; | |||||
| private WxSessionManager sessionManager; | |||||
| public WxCpMessageRouter(WxCpService wxCpService) { | public WxCpMessageRouter(WxCpService wxCpService) { | ||||
| this.wxCpService = wxCpService; | this.wxCpService = wxCpService; | ||||
| this.executorService = Executors.newFixedThreadPool(DEFAULT_THREAD_POOL_SIZE); | 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 | * @param executorService | ||||
| */ | */ | ||||
| public void setExecutorService(ExecutorService executorService) { | public void setExecutorService(ExecutorService executorService) { | ||||
| @@ -74,11 +81,25 @@ public class WxCpMessageRouter { | |||||
| } | } | ||||
| /** | /** | ||||
| * 设置自定义的WxMsgIdDuplicateChecker | |||||
| * @param wxMsgIdDuplicateChecker | |||||
| * <pre> | |||||
| * 设置自定义的 {@link me.chanjar.weixin.common.util.WxMessageDuplicateChecker} | |||||
| * 如果不调用该方法,默认使用 {@link me.chanjar.weixin.common.util.WxMessageInMemoryDuplicateChecker} | |||||
| * </pre> | |||||
| * @param messageDuplicateChecker | |||||
| */ | |||||
| public void setMessageDuplicateChecker(WxMessageDuplicateChecker messageDuplicateChecker) { | |||||
| this.messageDuplicateChecker = messageDuplicateChecker; | |||||
| } | |||||
| /** | |||||
| * <pre> | |||||
| * 设置自定义的{@link me.chanjar.weixin.common.session.WxSessionManager} | |||||
| * 如果不调用该方法,默认使用 {@linke SessionManagerImpl} | |||||
| * </pre> | |||||
| * @param sessionManager | |||||
| */ | */ | ||||
| public void setWxMsgIdDuplicateChecker(WxMsgIdDuplicateChecker wxMsgIdDuplicateChecker) { | |||||
| this.wxMsgIdDuplicateChecker = wxMsgIdDuplicateChecker; | |||||
| public void setSessionManager(WxSessionManager sessionManager) { | |||||
| this.sessionManager = sessionManager; | |||||
| } | } | ||||
| /** | /** | ||||
| @@ -86,7 +107,7 @@ public class WxCpMessageRouter { | |||||
| * @return | * @return | ||||
| */ | */ | ||||
| public Rule rule() { | public Rule rule() { | ||||
| return new Rule(this, wxCpService); | |||||
| return new Rule(this, wxCpService, sessionManager); | |||||
| } | } | ||||
| /** | /** | ||||
| @@ -94,7 +115,7 @@ public class WxCpMessageRouter { | |||||
| * @param wxMessage | * @param wxMessage | ||||
| */ | */ | ||||
| public WxCpXmlOutMessage route(final WxCpXmlMessage wxMessage) { | public WxCpXmlOutMessage route(final WxCpXmlMessage wxMessage) { | ||||
| if (wxMsgIdDuplicateChecker.isDuplicate(wxMessage.getMsgId())) { | |||||
| if (isDuplicateMessage(wxMessage)) { | |||||
| // 如果是重复消息,那么就不做处理 | // 如果是重复消息,那么就不做处理 | ||||
| return null; | return null; | ||||
| } | } | ||||
| @@ -115,27 +136,84 @@ public class WxCpMessageRouter { | |||||
| } | } | ||||
| WxCpXmlOutMessage res = null; | WxCpXmlOutMessage res = null; | ||||
| final List<Future> futures = new ArrayList<Future>(); | |||||
| for (final Rule rule : matchRules) { | for (final Rule rule : matchRules) { | ||||
| // 返回最后一个非异步的rule的执行结果 | // 返回最后一个非异步的rule的执行结果 | ||||
| if(rule.async) { | 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 { | } else { | ||||
| res = rule.service(wxMessage); | 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; | 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 { | public static class Rule { | ||||
| private final WxCpMessageRouter routerBuilder; | private final WxCpMessageRouter routerBuilder; | ||||
| private final WxCpService wxCpService; | private final WxCpService wxCpService; | ||||
| private final WxSessionManager sessionManager; | |||||
| private boolean async = true; | private boolean async = true; | ||||
| private String fromUser; | private String fromUser; | ||||
| @@ -158,9 +236,10 @@ public class WxCpMessageRouter { | |||||
| private List<WxCpMessageInterceptor> interceptors = new ArrayList<WxCpMessageInterceptor>(); | 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.routerBuilder = routerBuilder; | ||||
| this.wxCpService = wxCpService; | this.wxCpService = wxCpService; | ||||
| this.sessionManager = sessionManager; | |||||
| } | } | ||||
| /** | /** | ||||
| @@ -338,7 +417,7 @@ public class WxCpMessageRouter { | |||||
| Map<String, Object> context = new HashMap<String, Object>(); | Map<String, Object> context = new HashMap<String, Object>(); | ||||
| // 如果拦截器不通过 | // 如果拦截器不通过 | ||||
| for (WxCpMessageInterceptor interceptor : this.interceptors) { | for (WxCpMessageInterceptor interceptor : this.interceptors) { | ||||
| if (!interceptor.intercept(wxMessage, context, wxCpService)) { | |||||
| if (!interceptor.intercept(wxMessage, context, wxCpService, sessionManager)) { | |||||
| return null; | return null; | ||||
| } | } | ||||
| } | } | ||||
| @@ -347,7 +426,7 @@ public class WxCpMessageRouter { | |||||
| WxCpXmlOutMessage res = null; | WxCpXmlOutMessage res = null; | ||||
| for (WxCpMessageHandler handler : this.handlers) { | for (WxCpMessageHandler handler : this.handlers) { | ||||
| // 返回最后handler的结果 | // 返回最后handler的结果 | ||||
| res = handler.handle(wxMessage, context, wxCpService); | |||||
| res = handler.handle(wxMessage, context, wxCpService, sessionManager); | |||||
| } | } | ||||
| return res; | 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.WxMenu; | ||||
| import me.chanjar.weixin.common.bean.result.WxMediaUploadResult; | import me.chanjar.weixin.common.bean.result.WxMediaUploadResult; | ||||
| import me.chanjar.weixin.common.exception.WxErrorException; | 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.common.util.http.RequestExecutor; | ||||
| import me.chanjar.weixin.cp.bean.WxCpDepart; | import me.chanjar.weixin.cp.bean.WxCpDepart; | ||||
| import me.chanjar.weixin.cp.bean.WxCpMessage; | import me.chanjar.weixin.cp.bean.WxCpMessage; | ||||
| @@ -353,4 +355,46 @@ public interface WxCpService { | |||||
| * @param wxConfigProvider | * @param wxConfigProvider | ||||
| */ | */ | ||||
| public void setWxCpConfigStorage(WxCpConfigStorage 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.WxError; | ||||
| import me.chanjar.weixin.common.bean.result.WxMediaUploadResult; | import me.chanjar.weixin.common.bean.result.WxMediaUploadResult; | ||||
| import me.chanjar.weixin.common.exception.WxErrorException; | 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.StringUtils; | ||||
| import me.chanjar.weixin.common.util.crypto.SHA1; | import me.chanjar.weixin.common.util.crypto.SHA1; | ||||
| import me.chanjar.weixin.common.util.fs.FileUtils; | 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.BasicResponseHandler; | ||||
| import org.apache.http.impl.client.CloseableHttpClient; | import org.apache.http.impl.client.CloseableHttpClient; | ||||
| import org.apache.http.impl.client.HttpClients; | import org.apache.http.impl.client.HttpClients; | ||||
| import org.slf4j.Logger; | |||||
| import org.slf4j.LoggerFactory; | |||||
| import java.io.File; | import java.io.File; | ||||
| import java.io.IOException; | import java.io.IOException; | ||||
| @@ -41,10 +46,11 @@ import java.io.InputStream; | |||||
| import java.io.StringReader; | import java.io.StringReader; | ||||
| import java.util.List; | import java.util.List; | ||||
| import java.util.UUID; | import java.util.UUID; | ||||
| import java.util.concurrent.atomic.AtomicBoolean; | |||||
| public class WxCpServiceImpl implements WxCpService { | public class WxCpServiceImpl implements WxCpService { | ||||
| protected final Logger log = LoggerFactory.getLogger(WxCpServiceImpl.class); | |||||
| /** | /** | ||||
| * 全局的是否正在刷新access token的锁 | * 全局的是否正在刷新access token的锁 | ||||
| */ | */ | ||||
| @@ -52,12 +58,16 @@ public class WxCpServiceImpl implements WxCpService { | |||||
| protected WxCpConfigStorage wxCpConfigStorage; | protected WxCpConfigStorage wxCpConfigStorage; | ||||
| protected final ThreadLocal<Integer> retryTimes = new ThreadLocal<Integer>(); | |||||
| protected CloseableHttpClient httpClient; | protected CloseableHttpClient httpClient; | ||||
| protected HttpHost httpProxy; | 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) { | public boolean checkSignature(String msgSignature, String timestamp, String nonce, String data) { | ||||
| try { | try { | ||||
| return SHA1.gen(wxCpConfigStorage.getToken(), timestamp, nonce, data).equals(msgSignature); | return SHA1.gen(wxCpConfigStorage.getToken(), timestamp, nonce, data).equals(msgSignature); | ||||
| @@ -366,6 +376,33 @@ public class WxCpServiceImpl implements WxCpService { | |||||
| * @throws WxErrorException | * @throws WxErrorException | ||||
| */ | */ | ||||
| public <T, E> T execute(RequestExecutor<T, E> executor, String uri, E data) 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 accessToken = getAccessToken(false); | ||||
| String uriWithAccessToken = uri; | String uriWithAccessToken = uri; | ||||
| @@ -381,31 +418,10 @@ public class WxCpServiceImpl implements WxCpService { | |||||
| * 42001 access_token超时 | * 42001 access_token超时 | ||||
| */ | */ | ||||
| if (error.getErrorCode() == 42001 || error.getErrorCode() == 40001) { | if (error.getErrorCode() == 42001 || error.getErrorCode() == 40001) { | ||||
| // 强制设置wxCpConfigStorage它的access token过期了,这样在下一次请求里就会刷新access token | |||||
| // 强制设置wxMpConfigStorage它的access token过期了,这样在下一次请求里就会刷新access token | |||||
| wxCpConfigStorage.expireAccessToken(); | wxCpConfigStorage.expireAccessToken(); | ||||
| return execute(executor, uri, data); | 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) { | if (error.getErrorCode() != 0) { | ||||
| throw new WxErrorException(error); | throw new WxErrorException(error); | ||||
| } | } | ||||
| @@ -416,7 +432,6 @@ public class WxCpServiceImpl implements WxCpService { | |||||
| throw new RuntimeException(e); | throw new RuntimeException(e); | ||||
| } | } | ||||
| } | } | ||||
| protected CloseableHttpClient getHttpclient() { | protected CloseableHttpClient getHttpclient() { | ||||
| return httpClient; | 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.HashMap; | ||||
| import java.util.Map; | import java.util.Map; | ||||
| /** | |||||
| * Created by qianjia on 15/1/19. | |||||
| */ | |||||
| public class XStreamTransformer { | public class XStreamTransformer { | ||||
| protected static final Map<Class, XStream> CLASS_2_XSTREAM_INSTANCE = configXStreamInstance(); | 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; | package me.chanjar.weixin.cp.api; | ||||
| import me.chanjar.weixin.common.api.WxConsts; | 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.WxCpXmlMessage; | ||||
| import me.chanjar.weixin.cp.bean.WxCpXmlOutMessage; | import me.chanjar.weixin.cp.bean.WxCpXmlOutMessage; | ||||
| import org.testng.Assert; | import org.testng.Assert; | ||||
| @@ -67,7 +69,8 @@ public class WxCpMessageRouterTest { | |||||
| final WxCpMessageRouter router = new WxCpMessageRouter(null); | final WxCpMessageRouter router = new WxCpMessageRouter(null); | ||||
| router.rule().handler(new WxCpMessageHandler() { | router.rule().handler(new WxCpMessageHandler() { | ||||
| @Override | @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; | return null; | ||||
| } | } | ||||
| }).end(); | }).end(); | ||||
| @@ -149,11 +152,145 @@ public class WxCpMessageRouterTest { | |||||
| } | } | ||||
| @Override | @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(','); | sb.append(this.echoStr).append(','); | ||||
| return null; | 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; | package me.chanjar.weixin.cp.demo; | ||||
| import me.chanjar.weixin.common.session.WxSessionManager; | |||||
| import me.chanjar.weixin.cp.api.*; | import me.chanjar.weixin.cp.api.*; | ||||
| import me.chanjar.weixin.cp.bean.WxCpXmlMessage; | import me.chanjar.weixin.cp.bean.WxCpXmlMessage; | ||||
| import me.chanjar.weixin.cp.bean.WxCpXmlOutMessage; | import me.chanjar.weixin.cp.bean.WxCpXmlOutMessage; | ||||
| @@ -46,7 +47,7 @@ public class WxCpDemoServer { | |||||
| WxCpMessageHandler handler = new WxCpMessageHandler() { | WxCpMessageHandler handler = new WxCpMessageHandler() { | ||||
| @Override | @Override | ||||
| public WxCpXmlOutMessage handle(WxCpXmlMessage wxMessage, Map<String, Object> context, | public WxCpXmlOutMessage handle(WxCpXmlMessage wxMessage, Map<String, Object> context, | ||||
| WxCpService wxCpService) { | |||||
| WxCpService wxCpService, WxSessionManager sessionManager) { | |||||
| WxCpXmlOutTextMessage m = WxCpXmlOutMessage | WxCpXmlOutTextMessage m = WxCpXmlOutMessage | ||||
| .TEXT() | .TEXT() | ||||
| .content("测试加密消息") | .content("测试加密消息") | ||||
| @@ -60,7 +61,7 @@ public class WxCpDemoServer { | |||||
| WxCpMessageHandler oauth2handler = new WxCpMessageHandler() { | WxCpMessageHandler oauth2handler = new WxCpMessageHandler() { | ||||
| @Override | @Override | ||||
| public WxCpXmlOutMessage handle(WxCpXmlMessage wxMessage, Map<String, Object> context, | public WxCpXmlOutMessage handle(WxCpXmlMessage wxMessage, Map<String, Object> context, | ||||
| WxCpService wxCpService) { | |||||
| WxCpService wxCpService, WxSessionManager sessionManager) { | |||||
| String href = "<a href=\"" + wxCpService.oauth2buildAuthorizationUrl(null) | String href = "<a href=\"" + wxCpService.oauth2buildAuthorizationUrl(null) | ||||
| + "\">测试oauth2</a>"; | + "\">测试oauth2</a>"; | ||||
| return WxCpXmlOutMessage | return WxCpXmlOutMessage | ||||
| @@ -10,9 +10,6 @@ import javax.servlet.http.HttpServletResponse; | |||||
| import java.io.IOException; | import java.io.IOException; | ||||
| import java.util.Arrays; | import java.util.Arrays; | ||||
| /** | |||||
| * Created by qianjia on 14/11/28. | |||||
| */ | |||||
| public class WxCpOAuth2Servlet extends HttpServlet { | public class WxCpOAuth2Servlet extends HttpServlet { | ||||
| protected WxCpService wxCpService; | protected WxCpService wxCpService; | ||||
| @@ -0,0 +1,16 @@ | |||||
| <configuration> | |||||
| <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender"> | |||||
| <!-- encoders are assigned the type | |||||
| ch.qos.logback.classic.encoder.PatternLayoutEncoder by default --> | |||||
| <encoder> | |||||
| <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern> | |||||
| </encoder> | |||||
| </appender> | |||||
| <root level="info"> | |||||
| <appender-ref ref="STDOUT" /> | |||||
| </root> | |||||
| <logger name="me.chanjar.weixin.cp" level="debug" /> | |||||
| </configuration> | |||||
| @@ -1,28 +1,29 @@ | |||||
| <!DOCTYPE suite SYSTEM "http://testng.org/testng-1.0.dtd" > | <!DOCTYPE suite SYSTEM "http://testng.org/testng-1.0.dtd" > | ||||
| <suite name="Weixin-java-tool-suite" verbose="1"> | <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> | </suite> | ||||
| @@ -152,7 +152,6 @@ public class WxMpInMemoryConfigStorage implements WxMpConfigStorage { | |||||
| this.http_proxy_password = http_proxy_password; | this.http_proxy_password = http_proxy_password; | ||||
| } | } | ||||
| @Override | @Override | ||||
| public String toString() { | public String toString() { | ||||
| return "WxMpInMemoryConfigStorage{" + | return "WxMpInMemoryConfigStorage{" + | ||||
| @@ -166,6 +165,8 @@ public class WxMpInMemoryConfigStorage implements WxMpConfigStorage { | |||||
| ", http_proxy_port=" + http_proxy_port + | ", http_proxy_port=" + http_proxy_port + | ||||
| ", http_proxy_username='" + http_proxy_username + '\'' + | ", http_proxy_username='" + http_proxy_username + '\'' + | ||||
| ", http_proxy_password='" + http_proxy_password + '\'' + | ", http_proxy_password='" + http_proxy_password + '\'' + | ||||
| ", jsapiTicket='" + jsapiTicket + '\'' + | |||||
| ", jsapiTicketExpiresTime='" + jsapiTicketExpiresTime + '\'' + | |||||
| '}'; | '}'; | ||||
| } | } | ||||
| @@ -1,5 +1,6 @@ | |||||
| package me.chanjar.weixin.mp.api; | 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.WxMpXmlMessage; | ||||
| import me.chanjar.weixin.mp.bean.WxMpXmlOutMessage; | import me.chanjar.weixin.mp.bean.WxMpXmlOutMessage; | ||||
| @@ -7,18 +8,21 @@ import java.util.Map; | |||||
| /** | /** | ||||
| * 处理微信推送消息的处理器接口 | * 处理微信推送消息的处理器接口 | ||||
| * @author chanjarster | |||||
| * | * | ||||
| * @author Daniel Qian | |||||
| */ | */ | ||||
| public interface WxMpMessageHandler { | public interface WxMpMessageHandler { | ||||
| /** | /** | ||||
| * | |||||
| * @param wxMessage | * @param wxMessage | ||||
| * @param context 上下文,如果handler或interceptor之间有信息要传递,可以用这个 | |||||
| * @param context 上下文,如果handler或interceptor之间有信息要传递,可以用这个 | |||||
| * @param wxMpService | * @param wxMpService | ||||
| * @param sessionManager | |||||
| * @return xml格式的消息,如果在异步规则里处理的话,可以返回null | * @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; | 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.WxMpXmlMessage; | ||||
| import java.util.Map; | import java.util.Map; | ||||
| /** | /** | ||||
| * 微信消息拦截器,可以用来做验证 | * 微信消息拦截器,可以用来做验证 | ||||
| * @author qianjia | |||||
| * | * | ||||
| * @author Daniel Qian | |||||
| */ | */ | ||||
| public interface WxMpMessageInterceptor { | public interface WxMpMessageInterceptor { | ||||
| /** | /** | ||||
| * 拦截微信消息 | * 拦截微信消息 | ||||
| * | |||||
| * @param wxMessage | * @param wxMessage | ||||
| * @param context 上下文,如果handler或interceptor之间有信息要传递,可以用这个 | |||||
| * @param context 上下文,如果handler或interceptor之间有信息要传递,可以用这个 | |||||
| * @param wxMpService | * @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; | 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.WxMpXmlMessage; | ||||
| import me.chanjar.weixin.mp.bean.WxMpXmlOutMessage; | import me.chanjar.weixin.mp.bean.WxMpXmlOutMessage; | ||||
| import org.slf4j.Logger; | |||||
| import org.slf4j.LoggerFactory; | |||||
| import java.util.ArrayList; | import java.util.ArrayList; | ||||
| import java.util.HashMap; | import java.util.HashMap; | ||||
| import java.util.List; | import java.util.List; | ||||
| import java.util.Map; | import java.util.Map; | ||||
| import java.util.concurrent.ExecutionException; | |||||
| import java.util.concurrent.ExecutorService; | import java.util.concurrent.ExecutorService; | ||||
| import java.util.concurrent.Executors; | import java.util.concurrent.Executors; | ||||
| import java.util.concurrent.Future; | |||||
| import java.util.regex.Pattern; | import java.util.regex.Pattern; | ||||
| /** | /** | ||||
| @@ -38,12 +44,12 @@ import java.util.regex.Pattern; | |||||
| * router.route(message); | * router.route(message); | ||||
| * | * | ||||
| * </pre> | * </pre> | ||||
| * @author qianjia | |||||
| * | |||||
| */ | */ | ||||
| public class WxMpMessageRouter { | 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>(); | private final List<Rule> rules = new ArrayList<Rule>(); | ||||
| @@ -51,22 +57,22 @@ public class WxMpMessageRouter { | |||||
| private ExecutorService executorService; | private ExecutorService executorService; | ||||
| private WxMsgIdDuplicateChecker wxMsgIdDuplicateChecker; | |||||
| private WxMessageDuplicateChecker messageDuplicateChecker; | |||||
| private WxSessionManager sessionManager; | |||||
| public WxMpMessageRouter(WxMpService wxMpService) { | public WxMpMessageRouter(WxMpService wxMpService) { | ||||
| this.wxMpService = wxMpService; | this.wxMpService = wxMpService; | ||||
| this.executorService = Executors.newFixedThreadPool(DEFAULT_THREAD_POOL_SIZE); | 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 | * @param executorService | ||||
| */ | */ | ||||
| public void setExecutorService(ExecutorService 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 | * @return | ||||
| */ | */ | ||||
| public Rule rule() { | public Rule rule() { | ||||
| return new Rule(this, wxMpService); | |||||
| return new Rule(this, wxMpService, sessionManager); | |||||
| } | } | ||||
| /** | /** | ||||
| @@ -94,7 +114,7 @@ public class WxMpMessageRouter { | |||||
| * @param wxMessage | * @param wxMessage | ||||
| */ | */ | ||||
| public WxMpXmlOutMessage route(final WxMpXmlMessage wxMessage) { | public WxMpXmlOutMessage route(final WxMpXmlMessage wxMessage) { | ||||
| if (wxMsgIdDuplicateChecker.isDuplicate(wxMessage.getMsgId())) { | |||||
| if (isDuplicateMessage(wxMessage)) { | |||||
| // 如果是重复消息,那么就不做处理 | // 如果是重复消息,那么就不做处理 | ||||
| return null; | return null; | ||||
| } | } | ||||
| @@ -109,33 +129,90 @@ public class WxMpMessageRouter { | |||||
| } | } | ||||
| } | } | ||||
| } | } | ||||
| if (matchRules.size() == 0) { | if (matchRules.size() == 0) { | ||||
| return null; | return null; | ||||
| } | } | ||||
| WxMpXmlOutMessage res = null; | WxMpXmlOutMessage res = null; | ||||
| final List<Future> futures = new ArrayList<Future>(); | |||||
| for (final Rule rule : matchRules) { | for (final Rule rule : matchRules) { | ||||
| // 返回最后一个非异步的rule的执行结果 | // 返回最后一个非异步的rule的执行结果 | ||||
| if(rule.async) { | 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 { | } else { | ||||
| res = rule.service(wxMessage); | 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; | 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 { | public static class Rule { | ||||
| private final WxMpMessageRouter routerBuilder; | private final WxMpMessageRouter routerBuilder; | ||||
| private final WxMpService wxMpService; | private final WxMpService wxMpService; | ||||
| private final WxSessionManager sessionManager; | |||||
| private boolean async = true; | private boolean async = true; | ||||
| private String fromUser; | private String fromUser; | ||||
| @@ -155,10 +232,11 @@ public class WxMpMessageRouter { | |||||
| private List<WxMpMessageHandler> handlers = new ArrayList<WxMpMessageHandler>(); | private List<WxMpMessageHandler> handlers = new ArrayList<WxMpMessageHandler>(); | ||||
| private List<WxMpMessageInterceptor> interceptors = new ArrayList<WxMpMessageInterceptor>(); | 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.routerBuilder = routerBuilder; | ||||
| this.wxMpService = wxMpService; | this.wxMpService = wxMpService; | ||||
| this.sessionManager = sessionManager; | |||||
| } | } | ||||
| /** | /** | ||||
| @@ -324,7 +402,7 @@ public class WxMpMessageRouter { | |||||
| Map<String, Object> context = new HashMap<String, Object>(); | Map<String, Object> context = new HashMap<String, Object>(); | ||||
| // 如果拦截器不通过 | // 如果拦截器不通过 | ||||
| for (WxMpMessageInterceptor interceptor : this.interceptors) { | for (WxMpMessageInterceptor interceptor : this.interceptors) { | ||||
| if (!interceptor.intercept(wxMessage, context, wxMpService)) { | |||||
| if (!interceptor.intercept(wxMessage, context, wxMpService, sessionManager)) { | |||||
| return null; | return null; | ||||
| } | } | ||||
| } | } | ||||
| @@ -333,7 +411,7 @@ public class WxMpMessageRouter { | |||||
| WxMpXmlOutMessage res = null; | WxMpXmlOutMessage res = null; | ||||
| for (WxMpMessageHandler handler : this.handlers) { | for (WxMpMessageHandler handler : this.handlers) { | ||||
| // 返回最后handler的结果 | // 返回最后handler的结果 | ||||
| res = handler.handle(wxMessage, context, wxMpService); | |||||
| res = handler.handle(wxMessage, context, wxMpService, sessionManager); | |||||
| } | } | ||||
| return res; | 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.WxMenu; | ||||
| import me.chanjar.weixin.common.bean.result.WxMediaUploadResult; | import me.chanjar.weixin.common.bean.result.WxMediaUploadResult; | ||||
| import me.chanjar.weixin.common.exception.WxErrorException; | 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.common.util.http.RequestExecutor; | ||||
| import me.chanjar.weixin.mp.bean.*; | import me.chanjar.weixin.mp.bean.*; | ||||
| import me.chanjar.weixin.mp.bean.result.*; | 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; | 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); | 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.WxError; | ||||
| import me.chanjar.weixin.common.bean.result.WxMediaUploadResult; | import me.chanjar.weixin.common.bean.result.WxMediaUploadResult; | ||||
| import me.chanjar.weixin.common.exception.WxErrorException; | 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.StringUtils; | ||||
| import me.chanjar.weixin.common.util.crypto.SHA1; | import me.chanjar.weixin.common.util.crypto.SHA1; | ||||
| import me.chanjar.weixin.common.util.fs.FileUtils; | 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.BasicResponseHandler; | ||||
| import org.apache.http.impl.client.CloseableHttpClient; | import org.apache.http.impl.client.CloseableHttpClient; | ||||
| import org.apache.http.impl.client.HttpClients; | import org.apache.http.impl.client.HttpClients; | ||||
| import org.slf4j.Logger; | |||||
| import org.slf4j.LoggerFactory; | |||||
| import java.io.File; | import java.io.File; | ||||
| import java.io.IOException; | import java.io.IOException; | ||||
| @@ -42,6 +46,8 @@ import java.util.UUID; | |||||
| public class WxMpServiceImpl implements WxMpService { | public class WxMpServiceImpl implements WxMpService { | ||||
| protected final Logger log = LoggerFactory.getLogger(WxMpServiceImpl.class); | |||||
| /** | /** | ||||
| * 全局的是否正在刷新access token的锁 | * 全局的是否正在刷新access token的锁 | ||||
| */ | */ | ||||
| @@ -54,12 +60,16 @@ public class WxMpServiceImpl implements WxMpService { | |||||
| protected WxMpConfigStorage wxMpConfigStorage; | protected WxMpConfigStorage wxMpConfigStorage; | ||||
| protected final ThreadLocal<Integer> retryTimes = new ThreadLocal<Integer>(); | |||||
| protected CloseableHttpClient httpClient; | protected CloseableHttpClient httpClient; | ||||
| protected HttpHost httpProxy; | 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) { | public boolean checkSignature(String timestamp, String nonce, String signature) { | ||||
| try { | try { | ||||
| return SHA1.gen(wxMpConfigStorage.getToken(), timestamp, nonce).equals(signature); | return SHA1.gen(wxMpConfigStorage.getToken(), timestamp, nonce).equals(signature); | ||||
| @@ -439,7 +449,7 @@ public class WxMpServiceImpl implements WxMpService { | |||||
| return execute(new SimplePostRequestExecutor(), url, postData); | return execute(new SimplePostRequestExecutor(), url, postData); | ||||
| } | } | ||||
| /** | |||||
| /** | |||||
| * 向微信端发送请求,在这里执行的策略是当发生access_token过期时才去刷新,然后重新执行请求,而不是全局定时请求 | * 向微信端发送请求,在这里执行的策略是当发生access_token过期时才去刷新,然后重新执行请求,而不是全局定时请求 | ||||
| * @param executor | * @param executor | ||||
| * @param uri | * @param uri | ||||
| @@ -448,6 +458,33 @@ public class WxMpServiceImpl implements WxMpService { | |||||
| * @throws WxErrorException | * @throws WxErrorException | ||||
| */ | */ | ||||
| public <T, E> T execute(RequestExecutor<T, E> executor, String uri, E data) 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 accessToken = getAccessToken(false); | ||||
| String uriWithAccessToken = uri; | String uriWithAccessToken = uri; | ||||
| @@ -467,27 +504,6 @@ public class WxMpServiceImpl implements WxMpService { | |||||
| wxMpConfigStorage.expireAccessToken(); | wxMpConfigStorage.expireAccessToken(); | ||||
| return execute(executor, uri, data); | 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) { | if (error.getErrorCode() != 0) { | ||||
| throw new WxErrorException(error); | 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.ArrayList; | ||||
| import java.util.List; | import java.util.List; | ||||
| /** | |||||
| * Created by qianjia on 14/11/1. | |||||
| */ | |||||
| public class WxMpTemplateMessage { | public class WxMpTemplateMessage { | ||||
| private String toUser; | private String toUser; | ||||
| @@ -2,9 +2,6 @@ package me.chanjar.weixin.mp.bean.result; | |||||
| import me.chanjar.weixin.mp.util.json.WxMpGsonBuilder; | import me.chanjar.weixin.mp.util.json.WxMpGsonBuilder; | ||||
| /** | |||||
| * Created by qianjia on 14/11/26. | |||||
| */ | |||||
| public class WxMpOAuth2AccessToken { | public class WxMpOAuth2AccessToken { | ||||
| private String accessToken; | private String accessToken; | ||||
| @@ -14,11 +14,6 @@ import me.chanjar.weixin.mp.bean.WxMpCustomMessage; | |||||
| import java.lang.reflect.Type; | import java.lang.reflect.Type; | ||||
| /** | |||||
| * | |||||
| * @author qianjia | |||||
| * | |||||
| */ | |||||
| public class WxMpCustomMessageGsonAdapter implements JsonSerializer<WxMpCustomMessage> { | public class WxMpCustomMessageGsonAdapter implements JsonSerializer<WxMpCustomMessage> { | ||||
| public JsonElement serialize(WxMpCustomMessage message, Type typeOfSrc, JsonSerializationContext context) { | 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; | import java.lang.reflect.Type; | ||||
| /** | |||||
| * | |||||
| * @author qianjia | |||||
| * | |||||
| */ | |||||
| public class WxMpGroupGsonAdapter implements JsonSerializer<WxMpGroup>, JsonDeserializer<WxMpGroup> { | public class WxMpGroupGsonAdapter implements JsonSerializer<WxMpGroup>, JsonDeserializer<WxMpGroup> { | ||||
| public JsonElement serialize(WxMpGroup group, Type typeOfSrc, JsonSerializationContext context) { | 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; | import java.lang.reflect.Type; | ||||
| /** | |||||
| * | |||||
| * @author qianjia | |||||
| * | |||||
| */ | |||||
| public class WxMpMassGroupMessageGsonAdapter implements JsonSerializer<WxMpMassGroupMessage> { | public class WxMpMassGroupMessageGsonAdapter implements JsonSerializer<WxMpMassGroupMessage> { | ||||
| public JsonElement serialize(WxMpMassGroupMessage message, Type typeOfSrc, JsonSerializationContext context) { | 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; | import java.lang.reflect.Type; | ||||
| /** | |||||
| * | |||||
| * @author qianjia | |||||
| * | |||||
| */ | |||||
| public class WxMpMassNewsGsonAdapter implements JsonSerializer<WxMpMassNews> { | public class WxMpMassNewsGsonAdapter implements JsonSerializer<WxMpMassNews> { | ||||
| public JsonElement serialize(WxMpMassNews message, Type typeOfSrc, JsonSerializationContext context) { | 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; | import java.lang.reflect.Type; | ||||
| /** | |||||
| * | |||||
| * @author qianjia | |||||
| * | |||||
| */ | |||||
| public class WxMpMassOpenIdsMessageGsonAdapter implements JsonSerializer<WxMpMassOpenIdsMessage> { | public class WxMpMassOpenIdsMessageGsonAdapter implements JsonSerializer<WxMpMassOpenIdsMessage> { | ||||
| public JsonElement serialize(WxMpMassOpenIdsMessage message, Type typeOfSrc, JsonSerializationContext context) { | 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; | import java.lang.reflect.Type; | ||||
| /** | |||||
| * Created by qianjia on 14/11/28. | |||||
| */ | |||||
| public class WxMpOAuth2AccessTokenAdapter implements JsonDeserializer<WxMpOAuth2AccessToken> { | public class WxMpOAuth2AccessTokenAdapter implements JsonDeserializer<WxMpOAuth2AccessToken> { | ||||
| public WxMpOAuth2AccessToken deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws | 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; | import java.lang.reflect.Type; | ||||
| /** | |||||
| * @author qianjia | |||||
| */ | |||||
| public class WxMpTemplateMessageGsonAdapter implements JsonSerializer<WxMpTemplateMessage> { | public class WxMpTemplateMessageGsonAdapter implements JsonSerializer<WxMpTemplateMessage> { | ||||
| public JsonElement serialize(WxMpTemplateMessage message, Type typeOfSrc, JsonSerializationContext context) { | 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; | import java.lang.reflect.Type; | ||||
| /** | |||||
| * | |||||
| * @author qianjia | |||||
| * | |||||
| */ | |||||
| public class WxUserGsonAdapter implements JsonDeserializer<WxMpUser> { | public class WxUserGsonAdapter implements JsonDeserializer<WxMpUser> { | ||||
| public WxMpUser deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException { | 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; | import java.lang.reflect.Type; | ||||
| /** | |||||
| * | |||||
| * @author qianjia | |||||
| * | |||||
| */ | |||||
| public class WxUserListGsonAdapter implements JsonDeserializer<WxMpUserList> { | public class WxUserListGsonAdapter implements JsonDeserializer<WxMpUserList> { | ||||
| public WxMpUserList deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException { | 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.HashMap; | ||||
| import java.util.Map; | import java.util.Map; | ||||
| /** | |||||
| * Created by qianjia on 15/1/19. | |||||
| */ | |||||
| public class XStreamTransformer { | public class XStreamTransformer { | ||||
| protected static final Map<Class, XStream> CLASS_2_XSTREAM_INSTANCE = configXStreamInstance(); | 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; | package me.chanjar.weixin.mp.api; | ||||
| import me.chanjar.weixin.common.api.WxConsts; | 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.WxMpXmlMessage; | ||||
| import me.chanjar.weixin.mp.bean.WxMpXmlOutMessage; | import me.chanjar.weixin.mp.bean.WxMpXmlOutMessage; | ||||
| import org.testng.Assert; | import org.testng.Assert; | ||||
| @@ -67,7 +69,8 @@ public class WxMpMessageRouterTest { | |||||
| final WxMpMessageRouter router = new WxMpMessageRouter(null); | final WxMpMessageRouter router = new WxMpMessageRouter(null); | ||||
| router.rule().handler(new WxMpMessageHandler() { | router.rule().handler(new WxMpMessageHandler() { | ||||
| @Override | @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; | return null; | ||||
| } | } | ||||
| }).end(); | }).end(); | ||||
| @@ -149,11 +152,144 @@ public class WxMpMessageRouterTest { | |||||
| } | } | ||||
| @Override | @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(','); | sb.append(this.echoStr).append(','); | ||||
| return null; | 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.api.WxConsts; | ||||
| import me.chanjar.weixin.common.bean.result.WxMediaUploadResult; | import me.chanjar.weixin.common.bean.result.WxMediaUploadResult; | ||||
| import me.chanjar.weixin.common.exception.WxErrorException; | 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.api.*; | ||||
| import me.chanjar.weixin.mp.bean.WxMpXmlMessage; | import me.chanjar.weixin.mp.bean.WxMpXmlMessage; | ||||
| import me.chanjar.weixin.mp.bean.WxMpXmlOutImageMessage; | import me.chanjar.weixin.mp.bean.WxMpXmlOutImageMessage; | ||||
| @@ -51,7 +52,7 @@ public class WxMpDemoServer { | |||||
| WxMpMessageHandler textHandler = new WxMpMessageHandler() { | WxMpMessageHandler textHandler = new WxMpMessageHandler() { | ||||
| @Override | @Override | ||||
| public WxMpXmlOutMessage handle(WxMpXmlMessage wxMessage, Map<String, Object> context, | public WxMpXmlOutMessage handle(WxMpXmlMessage wxMessage, Map<String, Object> context, | ||||
| WxMpService wxMpService) { | |||||
| WxMpService wxMpService, WxSessionManager sessionManager) { | |||||
| WxMpXmlOutTextMessage m | WxMpXmlOutTextMessage m | ||||
| = WxMpXmlOutMessage.TEXT().content("测试加密消息").fromUser(wxMessage.getToUserName()) | = WxMpXmlOutMessage.TEXT().content("测试加密消息").fromUser(wxMessage.getToUserName()) | ||||
| .toUser(wxMessage.getFromUserName()).build(); | .toUser(wxMessage.getFromUserName()).build(); | ||||
| @@ -62,7 +63,7 @@ public class WxMpDemoServer { | |||||
| WxMpMessageHandler imageHandler = new WxMpMessageHandler() { | WxMpMessageHandler imageHandler = new WxMpMessageHandler() { | ||||
| @Override | @Override | ||||
| public WxMpXmlOutMessage handle(WxMpXmlMessage wxMessage, Map<String, Object> context, | public WxMpXmlOutMessage handle(WxMpXmlMessage wxMessage, Map<String, Object> context, | ||||
| WxMpService wxMpService) { | |||||
| WxMpService wxMpService, WxSessionManager sessionManager) { | |||||
| try { | try { | ||||
| WxMediaUploadResult wxMediaUploadResult = wxMpService | WxMediaUploadResult wxMediaUploadResult = wxMpService | ||||
| .mediaUpload(WxConsts.MEDIA_IMAGE, WxConsts.FILE_JPG, ClassLoader.getSystemResourceAsStream("mm.jpeg")); | .mediaUpload(WxConsts.MEDIA_IMAGE, WxConsts.FILE_JPG, ClassLoader.getSystemResourceAsStream("mm.jpeg")); | ||||
| @@ -86,7 +87,7 @@ public class WxMpDemoServer { | |||||
| WxMpMessageHandler oauth2handler = new WxMpMessageHandler() { | WxMpMessageHandler oauth2handler = new WxMpMessageHandler() { | ||||
| @Override | @Override | ||||
| public WxMpXmlOutMessage handle(WxMpXmlMessage wxMessage, Map<String, Object> context, | 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) | String href = "<a href=\"" + wxMpService.oauth2buildAuthorizationUrl(WxConsts.OAUTH2_SCOPE_USER_INFO, null) | ||||
| + "\">测试oauth2</a>"; | + "\">测试oauth2</a>"; | ||||
| return WxMpXmlOutMessage | return WxMpXmlOutMessage | ||||
| @@ -0,0 +1,16 @@ | |||||
| <configuration> | |||||
| <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender"> | |||||
| <!-- encoders are assigned the type | |||||
| ch.qos.logback.classic.encoder.PatternLayoutEncoder by default --> | |||||
| <encoder> | |||||
| <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern> | |||||
| </encoder> | |||||
| </appender> | |||||
| <root level="info"> | |||||
| <appender-ref ref="STDOUT" /> | |||||
| </root> | |||||
| <logger name="me.chanjar.weixin.mp" level="debug" /> | |||||
| </configuration> | |||||
| @@ -3,6 +3,7 @@ | |||||
| <suite name="Weixin-java-tool-suite" verbose="1"> | <suite name="Weixin-java-tool-suite" verbose="1"> | ||||
| <test name="API_Test"> | <test name="API_Test"> | ||||
| <classes> | <classes> | ||||
| <class name="me.chanjar.weixin.mp.api.WxMpBusyRetryTest" /> | |||||
| <class name="me.chanjar.weixin.mp.api.WxMpBaseAPITest" /> | <class name="me.chanjar.weixin.mp.api.WxMpBaseAPITest" /> | ||||
| <class name="me.chanjar.weixin.mp.api.WxMpCustomMessageAPITest" /> | <class name="me.chanjar.weixin.mp.api.WxMpCustomMessageAPITest" /> | ||||
| <class name="me.chanjar.weixin.mp.api.WxMpMenuAPITest" /> | <class name="me.chanjar.weixin.mp.api.WxMpMenuAPITest" /> | ||||