SpringBoot2.0.3+Mybatis+Mysql+druid實現讀寫分離+事務
mysql支援一主多從,即在寫庫的資料庫發生變動時,會同步到所有從庫,只是同步過程中,會有一定的延遲(除非業務中出現,立即寫立即讀,否則稍微的延遲是可以接收的)。
當資料庫有主從之分了,那應用程式碼也應該讀寫分離了。這時候的事務就不像單個數據庫那麼簡單了,為此整理出解決事務問題的方案:使用AbstractRoutingDataSource+aop+annotation在service層決定資料來源,可以支援事務。此方案的缺點:類內部方法通過this.xx()方式相互呼叫時,aop不會進行攔截,需進行特殊處理。
下面對這一方案做詳細介紹。
1、POM.xml
<!-- 定義公共資源版本 --> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.0.3.RELEASE</version> <relativePath /> </parent> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> <java.version>1.8</java.version> </properties> <dependencies> <!-- 上邊引入 parent,因此 下邊無需指定版本 --> <!-- 包含 mvc,aop 等jar資源 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> <exclusions> <exclusion><!-- 去除預設log配置 --> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-logging</artifactId> </exclusion> </exclusions> </dependency> <!-- 配置log4j2 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-log4j2</artifactId> </dependency> <!-- 配置log4j2 --> <!-- 支援識別yml配置 --> <dependency> <groupId>com.fasterxml.jackson.dataformat</groupId> <artifactId>jackson-dataformat-yaml</artifactId> </dependency> <!-- 支援識別yml配置 --> <!-- 熱部署 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-devtools</artifactId> <optional>true</optional> <scope>true</scope> </dependency> <!-- 熱部署 --> <!-- AOP的配置 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId> </dependency> <!-- AOP的配置 --> <!-- springboot,mybatis 整合包 --> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>1.3.2</version> </dependency> <!-- springboot,mybatis 整合包 --> <!-- mysql 驅動包 --> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> </dependency> <!-- mysql 驅動包 --> <!-- springboot測試 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <!-- springboot測試 --> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <scope>test</scope> </dependency> <!-- ali的druid --> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid-spring-boot-starter</artifactId> <version>1.1.10</version> </dependency> <!-- ali的druid --> <!--開始 JSONObject物件依賴的jar包 --> <dependency> <groupId>commons-beanutils</groupId> <artifactId>commons-beanutils</artifactId> <version>1.9.3</version> </dependency> <dependency> <groupId>commons-collections</groupId> <artifactId>commons-collections</artifactId> <version>3.2.2</version> </dependency> <dependency> <groupId>commons-lang</groupId> <artifactId>commons-lang</artifactId> <version>2.6</version> </dependency> <dependency> <groupId>commons-logging</groupId> <artifactId>commons-logging</artifactId> <version>1.2</version> </dependency> <dependency> <groupId>net.sf.ezmorph</groupId> <artifactId>ezmorph</artifactId> <version>1.0.6</version> </dependency> <dependency> <groupId>net.sf.json-lib</groupId> <artifactId>json-lib</artifactId> <version>2.4</version> <classifier>jdk15</classifier> </dependency> <!--結束 JSONObject物件依賴的jar包 --> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <configuration> <!-- 沒有該配置,devtools 不生效 --> <fork>true</fork> </configuration> </plugin> </plugins> </build>
2、 application.yml和application-dev.yml
本例子的資料庫,都是在本地的mysql中建立2個庫,test,test_01,例子是為了測試程式碼的讀寫分離。下面是application.yml:
spring: profiles: active: dev thymeleaf: cache: true prefix: classpath:/templates/ suffix: .html mode: HTML5 encoding: UTF-8 servlet: content-type: text/html mybatis: config-location: classpath:mybatis/mybatis-config.xml mapper-locations: classpath:mybatis/mapper/*.xml
下面是application-dev.yml:
server:
port: 8080
spring:
aop:
proxy-target-class: true
datasource:
#readSize為從庫數量
readSize: 1
###################以下為druid增加的配置###########################
type: com.alibaba.druid.pool.DruidDataSource
druid:
master:
url: jdbc:mysql://127.0.0.1:3306/test?useUnicode=true&characterEncoding=utf8&serverTimezone=UTC
driver-class-name: com.mysql.jdbc.Driver
username: root
password: root
initialSize: 5
minIdle: 5
maxActive: 20
maxWait: 60000
timeBetweenEvictionRunsMillis: 60000
min-evictableIdleTimeMillis: 300000
validationQuery: SELECT 1 FROM DUAL
testWhileIdle: true
testOnBorrow: true
testOnReturn: false
poolPreparedStatements: true
useGlobalDataSourceStat: true
maxPoolPreparedStatementPerConnectionSize: 20
filters: stat,wall
WebStatFilter:
exclusions: '*.js,*.gif,*.jpg,*.png,*.css,*.ico,/druid/*'
enabled: true
urlPattern: '/*'
StatViewServlet:
enabled: true
urlPattern: '/druid/*'
loginUsername: druid
loginPassword: druid
slave:
url: jdbc:mysql://127.0.0.1:3306/test_01?useUnicode=true&characterEncoding=utf8&serverTimezone=UTC
driver-class-name: com.mysql.jdbc.Driver
username: root
password: root
initialSize: 5
minIdle: 5
maxActive: 20
maxWait: 60000
timeBetweenEvictionRunsMillis: 60000
min-evictableIdleTimeMillis: 300000
validationQuery: SELECT 1 FROM DUAL
testWhileIdle: true
testOnBorrow: true
testOnReturn: false
poolPreparedStatements: true
useGlobalDataSourceStat: true
maxPoolPreparedStatementPerConnectionSize: 20
filters: stat,wall
WebStatFilter:
exclusions: '*.js,*.gif,*.jpg,*.png,*.css,*.ico,/druid/*'
enabled: true
urlPattern: '/*'
StatViewServlet:
enabled: true
urlPattern: '/druid/*'
loginUsername: druid
loginPassword: druid
###############以上為配置druid新增的配置########################################
3、mybatis-config.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<settings>
<!-- 獲取資料庫自增主鍵值 -->
<setting name="useGeneratedKeys" value="true"/>
<!-- 使用列別名替換列名,預設為 true -->
<setting name="useColumnLabel" value="true"/>
<!-- 開啟駝峰命名轉換:Table(create_time) => Entity(createTime) -->
<setting name="mapUnderscoreToCamelCase" value="true"/>
</settings>
</configuration>
4、DataSourceConfiguration.java(讀取配置多個數據源)
package com.wocloud.arch.ssm.mybatis;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
import javax.sql.DataSource;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.jdbc.DataSourceBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
@Configuration
public class DataSourceConfiguration {
@Value("${spring.datasource.type}")
private Class<? extends DataSource> dataSourceType;
@Bean(name = "writeDataSource", destroyMethod = "close", initMethod = "init")
@Primary
@ConfigurationProperties(prefix = "spring.datasource.druid.master")
public DataSource masterDataSource() {
return DataSourceBuilder.create().type(dataSourceType).build();
}
@Bean(name = "readDataSource", destroyMethod = "close", initMethod = "init")
@ConfigurationProperties(prefix = "spring.datasource.druid.slave")
public DataSource slaveDataSource() {
return DataSourceBuilder.create().type(dataSourceType).build();
}
@Bean("readDataSources")
public List<DataSource> readDataSources() throws SQLException {
List<DataSource> dataSources = new ArrayList<>();
//dataSources.add(masterDataSource());
dataSources.add(slaveDataSource());
return dataSources;
}
}
5、MybatisConfiguration.java(資料庫的sqlSessionFactorys、roundRobinDataSouceProxy、sqlSessionTemplate、annotationDrivenTransactionManager的設定。重點是roundRobinDataSouceProxy()方法,它把所有的資料庫源交給MyAbstractRoutingDataSource類,這個類見第6項,並由它的determineCurrentLookupKey()進行決定資料來源的選擇,其中讀庫進行了簡單的以輪詢的方式的負載均衡)
package com.wocloud.arch.ssm.mybatis;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.SqlSessionTemplate;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.core.io.support.ResourcePatternResolver;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.annotation.EnableTransactionManagement;
import com.wocloud.arch.ssm.utils.SpringContextUtil;
import javax.sql.DataSource;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@Configuration
@AutoConfigureAfter(DataSourceConfiguration.class)
@ConditionalOnClass({EnableTransactionManagement.class})
@Import({DataSourceConfiguration.class})
@MapperScan(basePackages = {"com.wocloud.arch.ssm.mapper"})
public class MybatisConfiguration {
@Value("${spring.datasource.type}")
private Class<? extends DataSource> dataSourceType;
@Value("${spring.datasource.readSize}")
private String dataSourceSize;
@Bean
@ConditionalOnMissingBean
public SqlSessionFactory sqlSessionFactory(ApplicationContext ac) throws Exception {
SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
sqlSessionFactoryBean.setDataSource(roundRobinDataSouceProxy(ac));
ResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
sqlSessionFactoryBean.setMapperLocations(resolver.getResources("classpath:mybatis/mapper/*.xml"));
sqlSessionFactoryBean.setTypeAliasesPackage("com.wocloud.arch.ssm.model");
sqlSessionFactoryBean.getObject().getConfiguration().setMapUnderscoreToCamelCase(true);
return sqlSessionFactoryBean.getObject();
}
/**
* 有多少個數據源就要配置多少個bean
*/
@Bean
public AbstractRoutingDataSource roundRobinDataSouceProxy(ApplicationContext ac) {
int size = Integer.parseInt(dataSourceSize);
MyAbstractRoutingDataSource proxy = new MyAbstractRoutingDataSource(size);
Map<Object, Object> targetDataSources = new HashMap<Object, Object>();
DataSource writeDataSource = (DataSource) ac.getBean("writeDataSource");
List<DataSource> readDataSources = (List<DataSource>) ac.getBean("readDataSources");
for (int i = 0; i < size; i++) {
targetDataSources.put(i, readDataSources.get(i));
}
proxy.setDefaultTargetDataSource(writeDataSource);
proxy.setTargetDataSources(targetDataSources);
return proxy;
}
@Bean
public SqlSessionTemplate sqlSessionTemplate(SqlSessionFactory sqlSessionFactory) {
return new SqlSessionTemplate(sqlSessionFactory);
}
//事務管理
@Bean
public PlatformTransactionManager annotationDrivenTransactionManager() {
return new DataSourceTransactionManager((DataSource)SpringContextUtil.getBean("roundRobinDataSouceProxy"));
}
}
6、MyAbstractRoutingDataSource.java
package com.wocloud.arch.ssm.mybatis;
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
import java.util.concurrent.atomic.AtomicInteger;
public class MyAbstractRoutingDataSource extends AbstractRoutingDataSource {
private final int dataSourceNumber;
private AtomicInteger count = new AtomicInteger(0);
public MyAbstractRoutingDataSource(int dataSourceNumber) {
this.dataSourceNumber = dataSourceNumber;
}
@Override
protected Object determineCurrentLookupKey() {
String typeKey = DataSourceContextHolder.getJdbcType();
if (DataSourceType.write.getType().equals(typeKey) || typeKey == null) {
return DataSourceType.write.getType();
}
// 讀 簡單負載(因為從庫數量為1,實際上目前沒有負載效果)
int number = count.getAndAdd(1);
int lookupKey = number % dataSourceNumber;
return new Integer(lookupKey);
}
}
7、DataSourceType.java()
package com.wocloud.arch.ssm.mybatis;
public enum DataSourceType {
read("read", "從庫"),
write("write", "主庫");
private String type;
private String name;
DataSourceType(String type, String name) {
this.type = type;
this.name = name;
}
public String getType() {
return type;
}
public void setType(String type) {
this.type = type;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
8、DataSourceContextHolder.java
package com.wocloud.arch.ssm.mybatis;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class DataSourceContextHolder {
private static final ThreadLocal<String> local = new ThreadLocal<String>();
public static ThreadLocal<String> getLocal() {
return local;
}
private final static Logger logger = LoggerFactory.getLogger(DataSourceContextHolder.class);
public static void read() {
local.set(DataSourceType.read.getType());
logger.info("切換到讀庫...");
}
public static void write() {
local.set(DataSourceType.write.getType());
logger.info("切換到寫庫...");
}
public static String getJdbcType() {
return local.get();
}
}
9、寫庫、讀庫的註解
package com.wocloud.arch.ssm.mybatis;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface ReadDataSource {
String description() default "";
}
package com.wocloud.arch.ssm.mybatis;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface WriteDataSource {
String description() default "";
}
10、 DataSourceAop.java(事務的決定者)
package com.wocloud.arch.ssm.mybatis;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
import org.springframework.core.PriorityOrdered;
import org.springframework.stereotype.Component;
/*
* 在service層決定資料來源
*
* 必須在事務AOP之前執行,所以實現Ordered,order的值越小,越先執行
* 如果一旦開始切換到寫庫,則之後的讀都會走寫庫
*/
@Aspect
@EnableAspectJAutoProxy(exposeProxy=true,proxyTargetClass=true)
@Component
public class DataSourceAop implements PriorityOrdered {
private final static Logger logger = LoggerFactory.getLogger(DataSourceAop.class);
@Pointcut("@annotation(com.wocloud.arch.ssm.mybatis.WriteDataSource)")
public void writeMethod(){}
@Pointcut("@annotation(com.wocloud.arch.ssm.mybatis.ReadDataSource)")
public void readMethod(){}
@Before("writeMethod() and execution(* com.wocloud.arch.ssm.service.impl..*.*(..)) ")
public void beforeWrite(JoinPoint point) {
//設定資料庫為寫資料
DataSourceContextHolder.write();
//除錯程式碼,可註釋
String className = point.getTarget().getClass().getName();
String methodName = point.getSignature().getName();
logger.info("dataSource切換到:Write 開始執行:" + className + "." + methodName + "()方法...");
}
@Before("readMethod() and execution(* com.wocloud.arch.ssm.service.impl..*.*(..)) ")
public void beforeRead(JoinPoint point) throws ClassNotFoundException {
//設定資料庫為讀資料
DataSourceContextHolder.read();
//除錯程式碼,可註釋
String className = point.getTarget().getClass().getName();//根據切點獲取當前呼叫的類名
String methodName = point.getSignature().getName();//根據切點獲取當前呼叫的類方法
logger.info("dataSource切換到:Read 開始執行:" + className + "." + methodName + "()方法...");
// Object[] args = point.getArgs();//根據切點獲取當前類方法的引數
// Class reflexClassName = Class.forName(className);//根據反射獲取當前呼叫類的例項
// Method[] methods = reflexClassName.getMethods();//獲取該例項的所有方法
// for (Method method : methods) {
// if (method.getName().equals(methodName)) {
// String desrciption = method.getAnnotation(ReadDataSource.class).description();//獲取該例項方法上註解裡面的描述資訊
// System.out.println("desrciption:" + desrciption);
// }
// }
}
@Override
public int getOrder() {
/**
* 值越小,越優先執行
* 要優於事務的執行
* 在啟動類中加上了@EnableTransactionManagement(order = 10)
*/
return 1;
}
}
11、CdkeyMapper.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.wocloud.arch.ssm.mapper.CdkeyMapper">
<resultMap id="BaseResultMap" type="com.wocloud.arch.ssm.model.Cdkey">
<!--
WARNING - @mbggenerated
This element is automatically generated by MyBatis Generator, do not modify.
-->
<id column="id" jdbcType="BIGINT" property="id" />
<result column="cdkey" jdbcType="VARCHAR" property="cdkey" />
<result column="order_code" jdbcType="VARCHAR" property="orderCode" />
<result column="isusage" jdbcType="VARCHAR" property="isusage" />
<result column="issend" jdbcType="SMALLINT" property="issend" />
<result column="first_time" jdbcType="TIMESTAMP" property="firstTime" />
<result column="last_time" jdbcType="TIMESTAMP" property="lastTime" />
<result column="order_code_wo" jdbcType="VARCHAR" property="orderCodeWo" />
<result column="flag1" jdbcType="VARCHAR" property="flag1" />
<result column="flag2" jdbcType="VARCHAR" property="flag2" />
<result column="flag3" jdbcType="INTEGER" property="flag3" />
<result column="flag4" jdbcType="INTEGER" property="flag4" />
</resultMap>
<delete id="deleteByPrimaryKey" parameterType="java.lang.Long">
<!--
WARNING - @mbggenerated
This element is automatically generated by MyBatis Generator, do not modify.
-->
delete from cdkey
where id = #{id,jdbcType=BIGINT}
</delete>
<insert id="insert" parameterType="com.wocloud.arch.ssm.model.Cdkey">
<!--
WARNING - @mbggenerated
This element is automatically generated by MyBatis Generator, do not modify.
-->
<selectKey keyProperty="id" order="BEFORE" resultType="java.lang.Long">
SELECT LAST_INSERT_ID()
</selectKey>
insert into cdkey (id, cdkey, order_code,
isusage, issend, first_time,
last_time, order_code_wo, flag1,
flag2, flag3, flag4
)
values (#{id,jdbcType=BIGINT}, #{cdkey,jdbcType=VARCHAR}, #{orderCode,jdbcType=VARCHAR},
#{isusage,jdbcType=VARCHAR}, #{issend,jdbcType=SMALLINT}, #{firstTime,jdbcType=TIMESTAMP},
#{lastTime,jdbcType=TIMESTAMP}, #{orderCodeWo,jdbcType=VARCHAR}, #{flag1,jdbcType=VARCHAR},
#{flag2,jdbcType=VARCHAR}, #{flag3,jdbcType=INTEGER}, #{flag4,jdbcType=INTEGER}
)
</insert>
<update id="updateByPrimaryKey" parameterType="com.wocloud.arch.ssm.model.Cdkey">
<!--
WARNING - @mbggenerated
This element is automatically generated by MyBatis Generator, do not modify.
-->
update cdkey
set cdkey = #{cdkey,jdbcType=VARCHAR},
order_code = #{orderCode,jdbcType=VARCHAR},
isusage = #{isusage,jdbcType=VARCHAR},
issend = #{issend,jdbcType=SMALLINT},
first_time = #{firstTime,jdbcType=TIMESTAMP},
last_time = #{lastTime,jdbcType=TIMESTAMP},
order_code_wo = #{orderCodeWo,jdbcType=VARCHAR},
flag1 = #{flag1,jdbcType=VARCHAR},
flag2 = #{flag2,jdbcType=VARCHAR},
flag3 = #{flag3,jdbcType=INTEGER},
flag4 = #{flag4,jdbcType=INTEGER}
where id = #{id,jdbcType=BIGINT}
</update>
<select id="selectByPrimaryKey" parameterType="java.lang.Long" resultMap="BaseResultMap">
<!--
WARNING - @mbggenerated
This element is automatically generated by MyBatis Generator, do not modify.
-->
select id, cdkey, order_code, isusage, issend, first_time, last_time, order_code_wo,
flag1, flag2, flag3, flag4
from cdkey
where id = #{id,jdbcType=BIGINT}
</select>
<select id="selectAll" resultMap="BaseResultMap">
<!--
WARNING - @mbggenerated
This element is automatically generated by MyBatis Generator, do not modify.
-->
select id, cdkey, order_code, isusage, issend, first_time, last_time, order_code_wo,
flag1, flag2, flag3, flag4
from cdkey
</select>
<select id="selectCdkeyByIsUsage" parameterType="java.lang.String" resultMap="BaseResultMap">
select id, cdkey, order_code, isusage, issend, first_time, last_time, order_code_wo,
flag1, flag2, flag3, flag4
from cdkey where 1=1
<if test="_parameter!=null">
and isusage = #{isusage,jdbcType=VARCHAR} limit 1
</if>
FOR UPDATE
</select>
<select id="selectCdkeyByOrderCode" parameterType="java.lang.String" resultMap="BaseResultMap">
select id, cdkey, order_code, isusage, issend, first_time, last_time, order_code_wo,
flag1, flag2, flag3, flag4
from cdkey where 1=1
<if test="_parameter!=null">
and order_code = #{orderCode,jdbcType=VARCHAR}
</if>
</select>
</mapper>
12、CdkeyMapper.java
package com.wocloud.arch.ssm.mapper;
import com.wocloud.arch.ssm.model.Cdkey;
import java.util.List;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface CdkeyMapper {
/**
* This method was generated by MyBatis Generator.
* This method corresponds to the database table cdkey
*
* @mbggenerated
*/
int deleteByPrimaryKey(Long id);
/**
* This method was generated by MyBatis Generator.
* This method corresponds to the database table cdkey
*
* @mbggenerated
*/
int insert(Cdkey record);
/**
* This method was generated by MyBatis Generator.
* This method corresponds to the database table cdkey
*
* @mbggenerated
*/
Cdkey selectByPrimaryKey(Long id);
/**
* This method was generated by MyBatis Generator.
* This method corresponds to the database table cdkey
*
* @mbggenerated
*/
List<Cdkey> selectAll();
/**
* This method was generated by MyBatis Generator.
* This method corresponds to the database table cdkey
*
* @mbggenerated
*/
int updateByPrimaryKey(Cdkey record);
/*
* 查詢可用兌換碼
*/
Cdkey selectCdkeyByIsUsage(String isUsage);
/*
* 由訂單號查詢兌換碼是否已傳送
*/
Cdkey selectCdkeyByOrderCode(String orderCode);
}
13、CdkeyService.java
/**
*
*/
package com.wocloud.arch.ssm.service;
import java.util.List;
import com.wocloud.arch.ssm.model.Cdkey;
/**
* @author mazhen
*
*/
public interface CdkeyService {
/*
* 插入Cdkey資訊
* @param Cdkey
* @return
*/
public int insertCdkey(Cdkey cdkey);
/*
* 查詢Cdkey資訊
* @param cdkeyId
* @return Cdkey
*/
public Cdkey queryByCdkeyId(Long cdkeyId);
/*
* 刪除Cdkey資訊
* @param cdkeyId
* @return
*/
public int deleteByCdkeyId(Long cdkeyId);
/*
* 更新Cdkey資訊
* @param Cdkey
* @return
*/
public int updateCdkey(Cdkey cdkey);
/*
* 查詢所有Cdkey資訊
* @param
* @return
*/
public List<Cdkey> queryAll();
/*
* 根據簡訊狀態查詢可用Cdkey兌換碼
* @param messageStatus
* @return Cdkey
*/
public Cdkey queryCdkeyByIsUsage(String isUsage);
/*
* 根據訂單號查詢Cdkey兌換碼是否已存在
* @param orderCode
* @return Cdkey
*/
public Cdkey queryCdkeyByOrderCode(String orderCode);
/*
* 兌換碼是否使用,cdkey表中更新資料
*/
public String updateCdkeyIsusage(JSONObject jsonObject);
}
14、CdkeyServiceImpl.java(使用getService()的原因:
/**
*
*/
package com.wocloud.arch.ssm.service.impl;
import java.util.List;
import com.wocloud.arch.ssm.mybatis.ReadDataSource;
import com.wocloud.arch.ssm.mybatis.WriteDataSource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import com.wocloud.arch.ssm.mapper.CdkeyMapper;
import com.wocloud.arch.ssm.model.Cdkey;
import com.wocloud.arch.ssm.service.CdkeyService;
import com.wocloud.arch.ssm.utils.SpringContextUtil;
import net.sf.json.JSONObject;
/**
* @author mazhen
* 注:AOP ,內部方法之間互相呼叫時,如果是this.xxx()這形式,不會觸發AOP攔截,可能會
* 導致無法決定資料庫是走寫庫還是讀庫
* 方法:
* 為了觸發AOP的攔截,呼叫內部方法時,需要特殊處理下,看方法getService()
*/
@Service
public class CdkeyServiceImpl implements CdkeyService {
private final static Logger logger = LoggerFactory.getLogger(CdkeyParameterServiceImpl.class);
@Autowired
private CdkeyMapper cdkeyMapper;
@Override
@WriteDataSource(description="WRITE")
public int insertCdkey(Cdkey cdkey) {
return cdkeyMapper.insert(cdkey);
}
@Override
@ReadDataSource(description="READ")
public Cdkey queryByCdkeyId(Long cdkeyId) {
return cdkeyMapper.selectByPrimaryKey(cdkeyId);
}
@Override
@WriteDataSource(description="WRITE")
public int deleteByCdkeyId(Long cdkeyId) {
return cdkeyMapper.deleteByPrimaryKey(cdkeyId);
}
@Override
@WriteDataSource(description="WRITE")
public int updateCdkey(Cdkey cdkey) {
return cdkeyMapper.updateByPrimaryKey(cdkey);
}
@Override
@ReadDataSource(description="READ")
public List<Cdkey> queryAll() {
return cdkeyMapper.selectAll();
}
@Override
@ReadDataSource(description="READ")
public Cdkey queryCdkeyByIsUsage(String isUsage) {
return cdkeyMapper.selectCdkeyByIsUsage(isUsage);
}
@Override
@ReadDataSource(description="READ")
public Cdkey queryCdkeyByOrderCode(String orderCode) {
return cdkeyMapper.selectCdkeyByOrderCode(orderCode);
}
@Override
@Transactional
public String updateCdkeyIsusage(JSONObject jsonObject) {
String cdkeyStr = null;
Cdkey cdkey = getService().queryCdkeyByIsUsage("0");
if (null == cdkey) {
logger.info("兌換碼已用完");
return cdkeyStr;
}
cdkey.setOrderCode(jsonObject.getString("orderCode"));
cdkey.setIsusage(jsonObject.getString("telephone"));
cdkey.setLastTime(RandomTool.getMillisecond());
try {
if (getService().updateCdkey(cdkey) > 0) {
cdkeyStr = cdkey.getCdkey();
}
} catch (Exception e) {
logger.info("異常資訊:"+e);
}
return cdkeyStr;
}
private CdkeyServiceImpl getService(){
return SpringContextUtil.getBean(this.getClass());
}
}
15、SpringContextUtil.java
package com.wocloud.arch.ssm.utils;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;
@Component
public class SpringContextUtil implements ApplicationContextAware{
private static ApplicationContext applicationContext = null;
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
if(SpringContextUtil.applicationContext == null){
SpringContextUtil.applicationContext = applicationContext;
}
}
public static ApplicationContext getApplicationContext() {
return applicationContext;
}
public static Object getBean(String name){
return getApplicationContext().getBean(name);
}
public static <T> T getBean(Class<T> clazz){
return getApplicationContext().getBean(clazz);
}
}
16、寫一個Controller進行驗證:
/**
*
*/
package com.wocloud.arch.ssm.controller;
import java.io.IOException;
import java.security.DigestException;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import javax.servlet.http.HttpServletRequest;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
import com.wocloud.arch.ssm.model.CdkeyParameter;
import com.wocloud.arch.ssm.responseResult.ResponseData;
import com.wocloud.arch.ssm.responseResult.ResponseResult;
import com.wocloud.arch.ssm.service.CdkeyParameterService;
import com.wocloud.arch.ssm.service.CdkeyService;
import net.sf.json.JSONObject;
/**
* @author mazhen
* 兌換碼請求介面
*/
@RestController
public class CdkeyController {
/**
* 引入日誌,注意都是"org.slf4j"包下
*/
private final static Logger logger = LoggerFactory.getLogger(CdkeyController.class);
@Autowired
private CdkeyParameterService cdkeyParameterService;
@Autowired
private CdkeyService cdkeyService;
/*
* 兌換碼post請求
* @param HttpServletRequest request
* @result ResponseResult
*/
@RequestMapping(value = "cdkeySender",method = RequestMethod.POST)
public ResponseData cdkeySender(HttpServletRequest request) {
logger.info("開始接收合作伙伴的引數");
Map<String, Object> map = new HashMap<>();
JSONObject jsonObjectData = null;
JSONObject jsonObject = null;
try {
jsonObjectData = ParameterUtil.getParameters(request);
} catch (IOException e) {
e.printStackTrace();
logger.error("介面獲取引數異常:"+e);
return responseDataFromContent(
"3",
"failure",
null,
"介面異常");
}
/*
* 校驗json包體完整性
*/
logger.info("從合作伙伴獲取到的jsonObjectData:"+jsonObjectData);
if (null == jsonObjectData || ("").equals(jsonObjectData) || !jsonObjectData.containsKey("data")) {
logger.error("介面獲取引數失敗");
return responseDataFromContent(
"4",
"failure",
null,
"介面獲取引數失敗");
}
logger.info(jsonObjectData.getString("data"));
jsonObject = JSONObject.fromObject(jsonObjectData.getString("data"));
String telePhone = jsonObject.getString("telephone");
try {
return updateCdkey(jsonObject);
} catch (DigestException e) {
e.printStackTrace();
logger.error("介面簽名演算法異常:"+e);
return responseDataFromContent(
"5",
"failure",
null,
"介面簽名演算法異常");
}
}
/*
* 處理Response資料
*/
private ResponseData responseDataFromContent(final String code, String message, String telePhone, String detail) {
ResponseData responseData = new ResponseData();
ResponseResult result = new ResponseResult();
result.setCode(code);
result.setMessage(message);
if (telePhone != null) {
result.setTelephone(telePhone);
}
result.setDetail(detail);
responseData.setData(result);
responseData.setResponseTime(RandomTool.getTimeStamp());
return responseData;
}
/*
* 更新cdkey資料庫
*/
public ResponseData updateCdkey(JSONObject jsonObject) {
ResponseData responseData = null;
String cdkeyStr = cdkeyService.updateCdkeyIsusage(jsonObject);
if (cdkeyStr == null) {
logger.info("cdkey表更新失敗,簡訊傳送失敗!");
responseData = responseDataFromContent(
"11",
"failure",
jsonObject.getString("telephone"),
"資料處理失敗");
} else {
logger.info("cdkey表更新成功,簡訊傳送成功!");
responseData = responseDataFromContent(
"0",
"success",
jsonObject.getString("telephone"),
"請求成功")
}
return responseData;
}
}
17、ResponseResult.java
/**
*
*/
package com.wocloud.arch.ssm.responseResult;
/**
* @author mazhen
*
*/
public class ResponseData {
private ResponseResult data;
private String responseTime;
public ResponseResult getData() {
return data;
}
public void setData(ResponseResult data) {
this.data = data;
}
public String getResponseTime() {
return responseTime;
}
public void setResponseTime(String responseTime) {
this.responseTime = responseTime;
}
}
18、ResponseData.java
/**
*
*/
package com.wocloud.arch.ssm.responseResult;
/**
* @author mazhen
*
*/
public class ResponseResult {
private String code;
private String message;
private String telephone;
private String detail;
public String getCode() {
return code;
}
public void setCode(String code) {
this.code = code;
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
public String getTelephone() {
return telephone;
}
public void setTelephone(String telephone) {
this.telephone = telephone;
}
public String getDetail() {
return detail;
}
public void setDetail(String detail) {
this.detail = detail;
}
public ResponseResult(String code,String message,String telephone,String detail){
super();
this.code = code;
this.message = message;
this.telephone = telephone;
this.detail = detail;
}
public ResponseResult(){
}
}
19、ParameterUtil.java
/**
*
*/
package com.wocloud.arch.ssm.utils.common;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import javax.servlet.http.HttpServletRequest;
import net.sf.json.JSONObject;
/**
* @author mazhen
* 獲取post請求中的body內容
*/
public class ParameterUtil {
public static JSONObject getParameters(HttpServletRequest request) throws IOException {
//從request中以"UTF-8"形式獲取輸入流,避免中文亂碼
BufferedReader br = new BufferedReader(new InputStreamReader(request.getInputStream(),"UTF-8"));
StringBuffer sb = new StringBuffer("");
String temp;
//一行一行讀取並放入到StringBuffer中
while((temp = br.readLine()) != null){
sb.append(temp);
}
br.close();
String acceptjson = sb.toString();
JSONObject jo = null;
//把String轉成JSON物件
if (acceptjson != "") {
jo = JSONObject.fromObject(acceptjson);
}
return jo;
}
}
最後,使用apache bench進行測試,事務的生效如圖所示: