文章目录
AbstractRoutingDataSource动态数据源切换
快速构建SpringMVC项目
项目结构:
web.xml:
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://java.sun.com/xml/ns/javaee"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"
id="WebApp_ID" version="2.5">
<display-name>spingmvc</display-name>
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath*:spring-basic.xml</param-value>
</context-param>
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<!-- 配置SpringMVC框架入口 -->
<servlet>
<servlet-name>spring-mvc</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:spring-mvc.xml</param-value>
</init-param>
<!--
tomcat启动时完成初始化
不配置,在第一次请求后完成初始化
-->
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>spring-mvc</servlet-name>
<url-pattern>*.do</url-pattern>
</servlet-mapping>
</web-app>
在resource目录下创建配置文件:log4j.properties、spring-mvc.xml、spring-basic.xml、mybatis-configuration.xml。
log4j.properties:
log4j.rootLogger=DEBUG,A1
log4j.logger.org.mybatis = DEBUG
log4j.appender.A1=org.apache.log4j.ConsoleAppender
log4j.appender.A1.layout=org.apache.log4j.PatternLayout
log4j.appender.A1.layout.ConversionPattern=%-d{yyyy-MM-dd HH:mm:ss,SSS} [%t] [%c]-[%p] %m%n
spring-mvc.xml:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:mvc="http://www.springframework.org/schema/mvc"
xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc.xsd
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd">
<mvc:annotation-driven />
<context:component-scan base-package="com.mvc.controller">
<context:include-filter type="annotation" expression="org.springframework.stereotype.Controller" />
</context:component-scan>
</beans>
spring-basic.xml:
<?xml version="1.0" encoding="UTF-8" ?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:mvc="http://www.springframework.org/schema/mvc"
xmlns:p="http://www.springframework.org/schema/p"
xmlns:tx="http://www.springframework.org/schema/tx"
xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc.xsd
http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd">
<context:annotation-config/>
<context:component-scan base-package="com.mvc">
<context:exclude-filter type="annotation" expression="org.springframework.stereotype.Controller" />
</context:component-scan>
</beans>
控制器:
import com.mvc.entity.User;
import com.mvc.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
@Controller
public class HelloController {
@Autowired
UserService userService;
@RequestMapping("/demo")
@ResponseBody
public String demo() {
User user = userService.demo();
return user.toString();
}
}
sql脚本:
CREATE TABLE `user` (
`id` int(255) NOT NULL AUTO_INCREMENT,
`name` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 2 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
编写AbstractRoutingDataSource动态数据源切换
编写设置动态选择的Datasource,这里的Set方法可以留给AOP调用,或者留给我们的具体的Dao层或者Service层中手动调用,在执行SQL语句之前:
// 根据当前线程来选择具体的数据源
public class HandlerDataSource {
public static final ThreadLocal<String> handlerThreadLocal = new ThreadLocal<String>();
// 提供给AOP去设置当前的线程的数据源的信息
public static void putDataSource(String datasource) {
handlerThreadLocal.set(datasource);
}
// 提供给AbstractRoutingDataSource的实现类,通过key选择数据源
public static String getDataSource() {
return handlerThreadLocal.get();
}
// 使用默认的数据源
public static void clean() {
handlerThreadLocal.remove();
}
}
编写AbstractRoutingDataSource的实现类,HandlerDataSource就是提供给我们动态选择数据源的数据的信息,我们这里编写一个根据当前线程来选择数据源,然后通过AOP拦截特定的注解,设置当前的数据源信息,也可以手动的设置当前的数据源,在编程的类中:
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
// 多数据源的选择实现类
public class MultipleDataSourceToChoose extends AbstractRoutingDataSource {
// 根据Key获取数据源的信息,上层抽象函数的钩子
@Override
protected Object determineCurrentLookupKey() {
return HandlerDataSource.getDataSource();
}
}
编写设置拦截数据源的注解,可以设置在具体的类上,或者在具体的方法上,value是当前数据源的一个别名用于标识我们的数据源的信息:
import java.lang.annotation.*;
// 创建拦截设置数据源的注解
@Target({ElementType.METHOD,ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DynamicSwitchDataSource {
String value();
}
AOP拦截类的实现,通过拦截上面的注解,在其执行之前处理设置当前执行SQL的数据源的信息,HandlerDataSource.putDataSource(….),这里的数据源信息从我们设置的注解上面获取信息,如果没有设置就是用默认的数据源的信息:
import org.apache.log4j.Logger;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import java.lang.reflect.Method;
// 使用AOP拦截特定的注解去动态的切换数据源
@Aspect
@Component
@Order(1)
public class HandlerDataSourceAop {
private static Logger logger = Logger.getLogger(HandlerDataSourceAop.class);
// @within在类上设置
// @annotation在方法上进行设置
@Pointcut("@within(com.mvc.datasource.DynamicSwitchDataSource)||@annotation(com.mvc.datasource.DynamicSwitchDataSource)")
public void pointcut() {}
@Before("pointcut()")
public void doBefore(JoinPoint joinPoint) {
Method method = ((MethodSignature)joinPoint.getSignature()).getMethod();
DynamicSwitchDataSource annotationClass = method.getAnnotation(DynamicSwitchDataSource.class);//获取方法上的注解
if(annotationClass == null){
annotationClass = joinPoint.getTarget().getClass().getAnnotation(DynamicSwitchDataSource.class);//获取类上面的注解
if(annotationClass == null) return;
}
//获取注解上的数据源的值的信息
String dataSourceKey = annotationClass.value();
if(dataSourceKey !=null){
//给当前的执行SQL的操作设置特殊的数据源的信息
HandlerDataSource.putDataSource(dataSourceKey);
}
logger.info("AOP动态切换数据源,className"+joinPoint.getTarget().getClass().getName()+"methodName"+method.getName()+";dataSourceKey:"+dataSourceKey==""?"默认数据源":dataSourceKey);
}
@After("pointcut()")
public void after(JoinPoint point) {
//清理掉当前设置的数据源,让默认的数据源不受影响
HandlerDataSource.clean();
}
}
在spring-mvc.xml、spring-basic.xml
分别配置aop的自动代理:
如果不在spring-mvc.xml
配置中加入aop扫描,切面只能被父容器装载,子容器是看不到的。
<!-- 配置自动为Spring容器中那些配置@aspectJ切面的bean创建代理 -->
<!-- proxy-target-class默认"false",更改为"true"使用CGLib动态代理 -->
<aop:aspectj-autoproxy proxy-target-class="true" />
配置两个数据源:
<bean id="readDataSource" class="com.zaxxer.hikari.HikariDataSource" destroy-method="close">
<property name="driverClassName" value="com.mysql.jdbc.Driver" />
<property name="jdbcUrl" value="jdbc:mysql://127.0.0.1:3306/read" />
<property name="username" value="root" />
<property name="password" value="123456" />
<property name="maximumPoolSize" value="10" />
<property name="minimumIdle" value="10" />
<!-- SQL查询,用来验证从连接池取出的连接 -->
<property name="connectionTestQuery" value="SELECT 1 FROM DUAL"/>
<!-- 等待连接池分配连接的最大时长(毫秒),超过这个时长还没可用的连接则发生SQLException, 缺省:30秒 -->
<property name="connectionTimeout" value="6000" />
</bean>
<bean id="writeDataSource" class="com.zaxxer.hikari.HikariDataSource" destroy-method="close">
<property name="driverClassName" value="com.mysql.jdbc.Driver" />
<property name="jdbcUrl" value="jdbc:mysql://127.0.0.1:3306/write" />
<property name="username" value="root" />
<property name="password" value="123456" />
<property name="maximumPoolSize" value="10" />
<property name="minimumIdle" value="10" />
<!-- SQL查询,用来验证从连接池取出的连接 -->
<property name="connectionTestQuery" value="SELECT 1 FROM DUAL"/>
<!-- 等待连接池分配连接的最大时长(毫秒),超过这个时长还没可用的连接则发生SQLException, 缺省:30秒 -->
<property name="connectionTimeout" value="6000" />
</bean>
配置之前我们实现的数据源选择的中间层AbstractRoutingDataSource的实现类,这里的key就是数据源信息的别名,通过这个key可以选择到数据源的信息。MultipleDataSourceToChoose就是上面写的数据源选择器的实现类:
<!-- 配置默认数据源选择器-->
<bean id="dataSource" class="com.mvc.datasource.MultipleDataSourceToChoose" lazy-init="true">
<description>数据源</description>
<property name="targetDataSources">
<!-- 选择数据源信息 -->
<map key-type="java.lang.String" value-type="javax.sql.DataSource">
<entry key="readDataSource" value-ref="readDataSource" />
<entry key="writeDataSource" value-ref="writeDataSource" />
</map>
</property>
<!-- 设置默认的目标数据源 -->
<property name="defaultTargetDataSource" ref="readDataSource" />
</bean>
配置SessionFactory:
<!-- 配置SessionFactory -->
<bean id="sqlSessionFactory"
class="org.mybatis.spring.SqlSessionFactoryBean"
p:dataSource-ref="dataSource"
p:configLocation="classpath:mybatis-configuration.xml"
p:mapperLocations="classpath:mapper/*.xml"/><!-- configLocation为mybatis属性 mapperLocations为所有mapper-->
<!-- spring与mybatis整合配置,扫描所有dao -->
<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer"
p:basePackage="com.mvc.mapper"
p:sqlSessionFactoryBeanName="sqlSessionFactory"/>
编写访问数据库相关代码:
mybatis-configuration.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>
<!-- 配置mybatis的缓存,延迟加载等等一系列属性 -->
<settings>
<setting name="logImpl" value="LOG4J"/>
<!-- 全局映射器启用缓存 -->
<setting name="cacheEnabled" value="true" />
<!-- 查询时,关闭关联对象即时加载以提高性能 -->
<setting name="lazyLoadingEnabled" value="true" />
<!-- 设置关联对象加载的形态,此处为按需加载字段(加载字段由SQL指 定),不会加载关联表的所有字段,以提高性能 -->
<setting name="aggressiveLazyLoading" value="false" />
<!-- 对于未知的SQL查询,允许返回不同的结果集以达到通用的效果 -->
<setting name="multipleResultSetsEnabled" value="true" />
<!-- 允许使用列标签代替列名 -->
<setting name="useColumnLabel" value="true" />
<!-- 允许使用自定义的主键值(比如由程序生成的UUID 32位编码作为键值),数据表的PK生成策略将被覆盖 -->
<!-- <setting name="useGeneratedKeys" value="true" /> -->
<!-- 给予被嵌套的resultMap以字段-属性的映射支持 -->
<setting name="autoMappingBehavior" value="FULL" />
<!-- 对于批量更新操作缓存SQL以提高性能 -->
<!--
<setting name="defaultExecutorType" value="BATCH" /> -->
<!-- 数据库超过25000秒仍未响应则超时 -->
<setting name="defaultStatementTimeout" value="2000" />
</settings>
<!-- 全局别名设置,在映射文件中只需写别名,而不必写出整个类路径 -->
<typeAliases>
<typeAlias alias="User" type="com.mvc.entity.User" />
</typeAliases>
</configuration>
UserMapper.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.mvc.mapper.UserMapper">
<resultMap id="BaseResultMap" type="com.mvc.entity.User">
<id column="id" property="id" jdbcType="INTEGER"/>
<result column="name" property="name" jdbcType="VARCHAR"/>
</resultMap>
<sql id="Base_Column_List">
id, name
</sql>
<select id="selectByPrimaryKey" resultMap="BaseResultMap" parameterType="java.lang.Integer">
select
<include refid="Base_Column_List"/>
from user
where id = #{id,jdbcType=INTEGER}
</select>
</mapper>
User:
public class User {
private Integer id;
private String name;
// 省略getter、setter、toString方法
}
UserMapper.java:
public interface UserMapper {
User selectByPrimaryKey(Integer id);
}
Service接口和实现类:
public interface IUserService {
User demo();
}
// 实现类
import com.mvc.datasource.DynamicSwitchDataSource;
import com.mvc.entity.User;
import com.mvc.mapper.UserMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class UserService implements IUserService{
@Autowired(required = false)
private UserMapper userMapper;
@Override
@DynamicSwitchDataSource("readDataSource")
public User demo(){
return userMapper.selectByPrimaryKey(1);
}
}
最后启动测试。
实现原理
MultipleDataSourceToChoose的继承结构图,是DataSource的子类,由于无论我们是使用Mybatis还是使用Hibernate进行SQL操作的时候总会执行getConnection(),无论我们的数据源是否使用了数据库连接池,因为数据库连接池的主要作用就是保持一堆的Connection不进行关闭的处理,节省我们的关闭和打开连接的开销。Connection getConnection() throws SQLException;所以这句话总是要执行的,只是AbstractRoutingDataSource这个类给我们进行了一些中间的处理,在获取Connection的时候会去寻找保存的DataSource的引用,到底是选择哪个DataSource进行处理。
下面是代码分析:
targetDataSources,是一个Map对于数据源的引用:
对于实现SQL的Connection getConnection() throws SQLException的实现,其实就是代理模式找到之前Map的引用,通过key,而这个key就是我们灵活配置的key,通过这个key就可以寻找到这个值:
这里说的非常的详细,通过钩子函数让子类去实现,寻找特定的key,然后选择DataSource 的时候就可以很灵活的使用了:
当当网的sharding-jdbc分库分表技术
配置分库分表规则:
<!-- 配置分表规则器,sharding-columns:分表规则依赖的名(根据onlyid取模分表),algorithm-expression:分表规则实现表达式 -->
<rdb:strategy id="historySharding" sharding-columns="onlyid" algorithm-expression="history_${onlyid.longValue()%3}"/>
<rdb:data-source id="readShardingDataSource">
<!-- 这里填写关联数据源(多个数据源用逗号隔开) -->
<rdb:sharding-rule data-sources="readDataSource">
<rdb:table-rules>
<!-- logic-table:逻辑表名-->
<!-- actual-tables:数据库实际的表名,这里支持inline表达式,比如:history_${0..2}会解析成history_0,history_1,history_2-->
<rdb:table-rule logic-table="history" actual-tables="history_${0..2}" table-strategy="historySharding"/>
</rdb:table-rules>
<rdb:default-database-strategy sharding-columns="none" algorithm-class="com.dangdang.ddframe.rdb.sharding.api.strategy.database.NoneDatabaseShardingAlgorithm"/>
<rdb:default-table-strategy sharding-columns="none" algorithm-class="com.dangdang.ddframe.rdb.sharding.api.strategy.table.NoneTableShardingAlgorithm"/>
</rdb:sharding-rule>
</rdb:data-source>
<rdb:data-source id="writeShardingDataSource">
<rdb:sharding-rule data-sources="writeDataSource">
<rdb:table-rules>
<rdb:table-rule logic-table="history" actual-tables="history_${0..2}" table-strategy="historySharding"/>
</rdb:table-rules>
<rdb:default-database-strategy sharding-columns="none" algorithm-class="com.dangdang.ddframe.rdb.sharding.api.strategy.database.NoneDatabaseShardingAlgorithm"/>
<rdb:default-table-strategy sharding-columns="none" algorithm-class="com.dangdang.ddframe.rdb.sharding.api.strategy.table.NoneTableShardingAlgorithm"/>
</rdb:sharding-rule>
</rdb:data-source>
修改数据源指向rdb配置的分库分表数据源定义:
<!-- 配置默认数据源选择器-->
<bean id="dataSource" class="com.mvc.datasource.MultipleDataSourceToChoose" lazy-init="true">
<description>数据源</description>
<property name="targetDataSources">
<!-- 选择数据源信息 -->
<map key-type="java.lang.String" value-type="javax.sql.DataSource">
<entry key="readDataSource" value-ref="readShardingDataSource" />
<entry key="writeDataSource" value-ref="writeShardingDataSource" />
</map>
</property>
<!-- 设置默认的目标数据源 -->
<property name="defaultTargetDataSource" ref="readShardingDataSource" />
</bean>
sql脚本:
CREATE TABLE `history` (
`id` bigint(255) NOT NULL AUTO_INCREMENT,
`onlyid` bigint(255) NOT NULL,
`name` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 6 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
SpringAOP拦截Controller和Service问题
spring配置:
<context:component-scan base-package="com.yq" >
<context:exclude-filter type="annotation" expression="org.springframework.stereotype.Controller"/>
</context:component-scan>
<aop:aspectj-autoproxy />
spring-mvc配置:
<context:component-scan base-package="com.yq" use-default-filters="false">
<context:include-filter type="annotation" expression="org.springframework.stereotype.Controller"/>
</context:component-scan>
<aop:aspectj-autoproxy />
切面类:
@Component
@Aspect
public class LogAspect {
@Pointcut("execution(* com.yq.service..*(..))")
private void servicePointCut() {}
@Pointcut("execution(* com.yq.controller..*(..))")
private void controllerPointCut() {}
@Around("servicePointCut()")
public Object logAroundService(ProceedingJoinPoint jointPoint) throws Throwable {
System.out.println(jointPoint+"开始");
Object proceed = jointPoint.proceed();
System.out.println(jointPoint+"结束");
return proceed;
}
@Around("controllerPointCut()")
public Object logAroundController(ProceedingJoinPoint jointPoint) throws Throwable {
System.out.println(jointPoint+"开始");
Object proceed = jointPoint.proceed();
System.out.println(jointPoint+"结束");
return proceed;
}
}
注意:
- controller需要使用cglib动态代理才可以拦截,高版本spring可以自动选择jdk或者cglib代理,在低版本中aop:aspectj-autoproxy必须加上proxy-target-class="true"才能指定使用cglib代理。
- aop切面类
LogAspect
必须与目标类在同一个上下文环境。因为切面类LogAspect
要同时切入controller和service,而我的spring上下文环境不包含controller,spring-mvc上下文环境不包含service,所以需要在spring和spring-mvc配置文件中都配置<aop:aspectj-autoproxy />
参考
基于springboot的mysql实现读写分离
AbstractRoutingDataSource动态数据源切换,AOP实现动态数据源切换
sharding-jdbc结合mybatis实现分库分表功能
spring aop拦截controller和service
spring boot使用AbstractRoutingDataSource实现动态数据源切换
@Transactional导致AbstractRoutingDataSource动态数据源无法切换的解决方法
Spring Boot + Mybatis 实现动态数据源