- 手撸一个动态数据源的Starter!
- 前言
- 一、准备工作
- 1,项目目录结构
- 2,POM文件
- 二、思路
- 三、编写代码
- 1,定义核心注解 Ds
- 2,定义切面
- 3,定义DataSourceContextHolder
- 4,定义抽象动态数据源模板类及其实现子类
- 5,定义application.yml配置类
- 6,定义AutoConfiguration
- 6,定义DataSourceCreator创建器
- 四 实测准备
- 1,数据库准备
- 2,表准备
- 3,项目准备
- 五 实测
- 总结
前言
该项目借鉴于苞米豆的开源项目dynamic-datasource-spring-boot-starter
码云地址
本项目地址:
码云地址
涉及知识点:
- SpringBoot自动装配
- ThreadLocal的使用
- ArrayDeque 后进先出栈的使用
- Aop相关知识点
提示:以下是本篇文章正文内容
一、准备工作 1,项目目录结构 2,POM文件二、思路4.0.0 com.xzq dynamic-spring-boot-starter 1.0-SNAPSHOT 8 8 1.2.8 3.4.3 org.springframework.boot spring-boot-starter-parent 2.5.6 com.alibaba druid-spring-boot-starter ${druid.version} org.springframework.boot spring-boot-starter org.springframework.boot spring-boot-starter-aop org.springframework.boot spring-boot-starter-jdbc com.baomidou mybatis-plus-boot-starter ${mybatis.plus.version} org.springframework.boot spring-boot-configuration-processor org.projectlombok lombok true org.apache.maven.plugins maven-source-plugin 2.1.2 attach-sources jar
整体的过程如上图所示。
- 我们通过核心注解Ds,将数据库路由Key标注Controller 或者Service或者Mapper的类或者方法之上。
- 然后通过Aop横切技术,在执行方法时进行拦截,获取到路由Key设置到ThreadLocal中。
- 最后由持久层获取Connection时,动态数据源会从ThreadLocal中获取路由Key,然后根据路由Key找到对应的数据源,由该数据源创建Connection连接。
- 通过Connection连接操作数据库
我们仔细思考会不会发现一些问题呢?
如果说是通过ThreadLocal存储当前的数据库路由Key,那么ThreadLocal中应该存储什么类型的数据呢?直接String?
直接String的话,当一个Service标注了Ds,假如我又调用了另外一个Service,另外的一个Service中也标注了Ds,既两个不同的Service是不同库的,如果ThreadLocal使用String类型的数据,我们使用Aop横切技术,第一个Service会被拦截,将路由Key设置到本地线程变量中,第二个Service执行,又被拦截又将当前路由Key设置到ThreadLocal中,或许有人问,这有什么问题?,那么我第一个Service调用完第二个Service后,数据库路由发生了改变,可是该Service中的方法还没有执行完毕,我后续的一些数据库操作实际上连接的都是第二个Service标注的数据源。这肯定会出问题的。这里先留下伏笔,小伙伴们可以思考一些使用什么样的数据结构可以避免。
com.xzq.dynamic.annotation.Ds
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE,ElementType.METHOD})
@Inherited
public @interface Ds {
String value() default "";
}
首先第一步,我们先自定义核心注解Ds,其中的value表示路由数据库的Key
通过元注解@Target的ElementType属性表示,该注解可作用在方法或者类,接口之上。
package com.xzq.dynamic.aop;
@Aspect
public class DsAspect {
private Logger logger = LoggerFactory.getLogger(DsAspect.class);
@Pointcut("@annotation(com.xzq.dynamic.annotation.Ds)")
public void aopPoint() {
}
@Around("aopPoint()")
public Object dbRouter(ProceedingJoinPoint pjp) throws Throwable {
Ds ds = findDsKey(pjp);
if (ds == null) {
return pjp.proceed();
}
String value = ds.value();
if (StringUtils.hasLength(value)) {
logger.info("拦截目标方法{},设置数据库路由Key: {}", ((MethodSignature) pjp.getSignature()).getMethod().getName(), value);
DataSourceContextHolder.push(value);
}
try {
return pjp.proceed();
} finally {
DataSourceContextHolder.poll();
}
}
private Ds findDsKey(ProceedingJoinPoint pjp) {
//获取目标对象
Class> target = pjp.getTarget().getClass();
//获取目标对象所有接口
Class>[] interfaces = target.getInterfaces();
//获取方法对象
Method method = ((MethodSignature) pjp.getSignature()).getMethod();
//Method优先级 > 类优先级 > 接口优先级
Ds ds = null;
if ((ds = getDs(method)) == null &&
(ds = getDs(new Class[]{target})) == null &&
(ds = getDs(interfaces)) == null) {
return null;
}
return ds;
}
private Ds getDs(Class>[] targets) {
for (Class> target : targets) {
Ds ds = target.getAnnotation(Ds.class);
if (ds != null) {
return ds;
}
}
return null;
}
private Ds getDs(Method method) {
return method.getAnnotation(Ds.class);
}
}
该切面的核心方法其实就是findDsKey(), 在该方法中获取切点的目标对象,方法对象,还有目标对象的实现接口。然后设置一个获取的优先级,方法上的Ds优先于类,实现类优先于接口。
这样可以避免在类上设置了路由,但是在该类中的某一个方法需要切换数据库也设置了路由,导致类覆盖方法的路由。
为什么还要获取接口的呢?我们一般都会设置到实现类上。但是如果用过Mybatis的小伙伴们肯定知道,Mybatis中并没有实现类,只有接口,实现类是在程序运行中创建的。通过JDK的动态代理。
但是这样写就行了吗?如果有对Aop比较熟悉的小伙伴肯定知道,这样其实是不行的,一开始我也是这样写的,后来测试的时候发现,如果将Ds注解标注在类上,切面并不会拦截该类的所有方法。(哎,还是楼主太菜对Aop基础的匮乏)
后面进行实际测试的时候会介绍如何绕开Aop的方法匹配器。
Spring中文文档
package com.xzq.dynamic.core;
public class DataSourceContextHolder {
private static final ThreadLocal> DB_KEY_HOLDER = new ThreadLocal>() {
@Override
protected Deque initialValue() {
return new ArrayDeque<>();
}
};
public static String peek(){
return DB_KEY_HOLDER.get().peek();
}
public static void push(String key){
DB_KEY_HOLDER.get().push(key);
}
public static void poll() {
Deque deque = DB_KEY_HOLDER.get();
deque.poll();
if (deque.isEmpty()) {
clean();
}
}
public static void clean() {
DB_KEY_HOLDER.remove();
}
}
创建了一个路由Key的持有类,其实也就是利用ThreadLocal来实现线程隔离。
在这里我们使用ArrayDeque当作ThreadLocal的数据类型,这是一个使用LIFO 队列,后进先出,其实也就是我们常说的栈结构。
通过使用这种数据类型,我们就可以实现,各个方法上定义的路由Key互不影响
动态路由抽象类
package com.xzq.dynamic.core;
@Data
public abstract class AbstractRoutingDataSource extends AbstractDataSource {
//多数据源容器
private Map dataSourceMap = new ConcurrentHashMap<>();
//默认主数据库
private String primaryKey;
@Override
public Connection getConnection() throws SQLException {
return determineDataSource().getConnection();
}
//抽象模板方法,交由子类实现
protected abstract DataSource determineDataSource();
@Override
public Connection getConnection(String username, String password) throws SQLException {
return determineDataSource().getConnection(username, password);
}
protected DataSource getDataSource(String dbKey) {
if (!StringUtils.hasLength(dbKey)) {
return dataSourceMap.get(primaryKey);
}
return dataSourceMap.get(dbKey);
}
}
动态路由实现类
package com.xzq.dynamic.core;
public class DynamicRoutingDataSource extends AbstractRoutingDataSource{
@Override
protected DataSource determineDataSource() {
return getDataSource(DataSourceContextHolder.peek());
}
}
其实Spring-jdbc中提供了一个抽象动态数据源模板类
有兴趣的小伙伴可以看一下。
我们在这里自己去定义了一个动态路由抽象模板类。该类的作用是什么呢?
首先先解释一下为啥叫他模板类,这里其实是应用了设计模式中的模板模式。由抽象类定义方法的执行顺序,再有子类实现方法。这里就不多说了。
该类实现了AbstractDataSource,重写了getConnection()方法。
我们定义了一个抽象模板方法,determineDataSource() 其作用就是获取真正的数据源,这也是我们动态数据源的核心方法,其DynamicRoutingDataSource 子类重写了该方法,通过ThreadLocal获取到路由key,从dataSourceMap中拿到匹配的数据源。
package com.xzq.dynamic.spring;
@ConfigurationProperties(prefix = DynamicProperties.PREFIX)
@Data
public class DynamicProperties {
public static final String PREFIX = "spring.datasource.dynamic";
private Map datasource = new linkedHashMap<>();
private DruidConfig druid;
private HikariCpConfig hikari;
private String primary;
private Class extends DataSource> type;
}
package com.xzq.dynamic.spring;
import lombok.Data;
import java.util.Properties;
@Data
public class DataSourceProperty {
private String driverClassName;
private String url;
private String username;
private String password;
}
package com.xzq.dynamic.spring.druid;
@Data
public class DruidConfig {
private Integer initialSize;
private Integer maxActive;
private Integer minIdle;
private Integer maxWait;
private Long minEvictableIdleTimeMillis;
private Long maxEvictableIdleTimeMillis;
String INITIAL_SIZE = "druid.initialSize";
String MAX_ACTIVE = "druid.maxActive";
String MIN_IDLE = "druid.minIdle";
String MAX_WAIT = "druid.maxWait";
String MIN_EVICTABLE_IDLE_TIME_MILLIS = "druid.minEvictableIdleTimeMillis";
String MAX_EVICTABLE_IDLE_TIME_MILLIS = "druid.maxEvictableIdleTimeMillis";
public Properties toPropertes() {
Properties properties = new Properties();
properties.setProperty(INITIAL_SIZE, String.valueOf(initialSize));
properties.setProperty(MAX_ACTIVE, String.valueOf(maxActive));
properties.setProperty(MIN_IDLE, String.valueOf(minIdle));
properties.setProperty(MAX_WAIT, String.valueOf(maxWait));
properties.setProperty(MIN_EVICTABLE_IDLE_TIME_MILLIS, String.valueOf(minEvictableIdleTimeMillis));
properties.setProperty(MAX_EVICTABLE_IDLE_TIME_MILLIS, String.valueOf(maxEvictableIdleTimeMillis));
return properties;
}
}
package com.xzq.dynamic.spring.hikari;
@Data
public class HikariCpConfig {
private String catalog;
private Long connectionTimeout;
private Long validationTimeout;
private Long idleTimeout;
private Long leakDetectionThreshold;
private Long maxLifetime;
private Integer maxPoolSize;
private Integer minIdle;
private Long initializationFailTimeout;
private String connectionInitSql;
private String connectionTestQuery;
private String dataSourceClassName;
private String dataSourceJndiName;
private String transactionIsolationName;
private Boolean isAutoCommit;
private Boolean isReadOnly;
private Boolean isIsolateInternalQueries;
private Boolean isRegisterMbeans;
private Boolean isAllowPoolSuspension;
private Properties dataSourceProperties;
private Properties healthCheckProperties;
private String schema;
private String exceptionOverrideClassName;
private Long keepaliveTime;
private Boolean sealed;
}
@ConfigurationProperties(prefix = DynamicProperties.PREFIX)该注解表明该类是一个Properties配置类。该注解需要@EnableConfigurationProperties(DynamicProperties.class)配合才能使用,我们会在后面的自动装配类中使用@EnableConfigurationProperties注解。
对boot比较熟悉的小伙伴们肯定知道这个注解,有时候我们会将application.yml中信息通过配置类得到。
该类配置完之后,我们就可以在application.yml中设置我们的动态数据源配置,如下所示
spring:
datasource:
dynamic:
datasource:
one:
driver-class-name: com.mysql.cj.jdbc.Driver
username: root
password: 123456
url: jdbc:mysql://127.0.0.1:3306/test?useUnicode=true&useSSL=false&characterEncoding=utf8
second:
driver-class-name: com.mysql.cj.jdbc.Driver
username: root
password: 123456
url: jdbc:mysql://127.0.0.1:3306/test2?useUnicode=true&useSSL=false&characterEncoding=utf8
druid:
initialSize: 10
maxActive: 50
minIdle: 5
maxWait: 60
minEvictableIdleTimeMillis: 30000
maxEvictableIdleTimeMillis: 30000
primary: one
type: com.alibaba.druid.pool.DruidDataSource
这里数据库类型就仅仅支持德鲁伊跟HikariCp两种连接池
这里提供一个小知识,用过boot的小伙伴们都知道,application.yml有提示功能,那么我们自己定义的有没有提示功能的,这个是可以有的。我们可以引入一个依赖,自动生成yml文件的提示JSON文件。
org.springframework.boot spring-boot-configuration-processor
这样在项目打包后,会根据配置类创建一个元数据JSON文件
这样我们在引入自己的Strater之后,就可以使用yml的自动提示功能。
package com.xzq.dynamic;
@EnableConfigurationProperties(DynamicProperties.class)
public class DynamicDataSourceAutoConfiguration implements InitializingBean {
private final DynamicProperties properties;
public DynamicDataSourceAutoConfiguration(DynamicProperties properties) {
this.properties = properties;
}
@Bean
public DsAspect dsAspect() {
return new DsAspect();
}
@Bean
@ConditionalOnMissingBean
public DataSource dataSource() {
DynamicRoutingDataSource routingDataSource = new DynamicRoutingDataSource();
routingDataSource.setPrimaryKey(properties.getPrimary());
//routingDataSource.setDataSourceMap(dataSourceMap);
return routingDataSource;
}
}
springBoot有约定大于配置的说法,我们写完这个配置类之后,如何装载到spring容器中呢?
约定来了,springBoot约定会去/resource/meta-INF/ 该目录下寻找一个spring.factories文件,该文件中定义了我们的配置类,spring就会默认将该配置类注入spring容器中。
所以我们需要自定义一个spring约定文件放在/resource/meta-INF/ 下
这样当我们的Starter被引入时就会被spring扫描到并将配置类注册到容器中。
我们基本上已经完成了该项目的骨架了。但是还缺少了重要的一步,我们需要在自动装配类中将数据源注入容器。
那么如何创建数据源?
又如何注入到容器中呢?
创建数据源我们肯定需要注册驱动,拿到driver-Class-Name,url,username,password等一些信息。这些信息在哪里呢?欸,就是用户输入到application.yml中的数据库信息了,前面我们定义了一个Properties配置类,可以拿到application.yml中的信息。所以说我们可以通过DynamicProperties来创建数据源。
那么创建数据源的方式该怎么做呢?
有啥可做的,仅说点废话,直接New一个DataSource,给他配置好属性一个@Bean注入到容器中,不就这点事吗?楼主一开始也是这样想的,也是这样做的。
看了苞米豆的源码后发现,卧槽,还能这样。这才是OOP!怪不得楼主只是一个码农。
下面来介绍如何创建数据源
package com.xzq.dynamic.creator;
public abstract class AbstractDataSourceCreator {
protected final DynamicProperties properties;
public AbstractDataSourceCreator(DynamicProperties properties) {
this.properties = properties;
}
public DataSource createrDataSource(DataSourceProperty property) {
return doCreateDataSource(property);
}
protected abstract DataSource doCreateDataSource(DataSourceProperty property);
}
这里还是老样子,定义抽象模板类,实际的创建方法交由子类实现
package com.xzq.dynamic.creator;
public class DruidDataSourceCreator extends AbstractDataSourceCreator implements DataSourceCreator{
private Logger logger = LoggerFactory.getLogger(DruidDataSourceCreator.class);
private final DruidConfig config;
public DruidDataSourceCreator(DynamicProperties properties) {
super(properties);
config = properties.getDruid();
}
@Override
protected DataSource doCreateDataSource(DataSourceProperty property) {
DruidDataSource druidDataSource = new DruidDataSource();
druidDataSource.setDriverClassName(property.getDriverClassName());
druidDataSource.setUsername(property.getUsername());
druidDataSource.setPassword(property.getPassword());
druidDataSource.setUrl(property.getUrl());
Properties properties = config.toPropertes();
druidDataSource.configFromPropety(properties);
try {
druidDataSource.init();
} catch (SQLException e) {
logger.error("druid init fail");
new RuntimeException("druid init fail", e);
}
return druidDataSource;
}
@Override
public boolean support(DataSourceProperty property) {
Class extends DataSource> type = properties.getType();
return (type != null && DRUID_DATASOURCE.equals(type.getName()));
}
}
package com.xzq.dynamic.creator;
public class HikariDataSourceCreator extends AbstractDataSourceCreator implements DataSourceCreator{
private HikariCpConfig config;
public HikariDataSourceCreator(DynamicProperties properties) {
super(properties);
config = properties.getHikari();
}
@Override
protected DataSource doCreateDataSource(DataSourceProperty property) {
HikariConfig config = new HikariConfig();
BeanUtils.copyProperties(this.config, config);
config.setUsername(property.getUsername());
config.setPassword(property.getPassword());
config.setJdbcUrl(property.getUrl());
config.setDriverClassName(property.getDriverClassName());
HikariDataSource hikariDataSource = new HikariDataSource(config);
return hikariDataSource;
}
@Override
public boolean support(DataSourceProperty property) {
Class extends DataSource> type = this.properties.getType();
return (type!=null && DbConstants.HIKARI_DATASOURCE.equals(type.getName()));
}
}
这里我只仅仅做了几个必要的连接池参数,其实德鲁伊跟hikariCp有很多连接池参数,有兴趣的小伙伴可以自己完善。
package com.xzq.dynamic.creator;
public class DefaultDataSourceCreator {
private List creators;
public DefaultDataSourceCreator(List creators) {
this.creators = creators;
}
public DataSource createDataSource(DataSourceProperty dataSourceProperty) {
DataSourceCreator dataSourceCreator = null;
for (DataSourceCreator creator : creators) {
if (creator.support(dataSourceProperty)) {
dataSourceCreator = creator;
break;
}
}
if (dataSourceCreator == null) {
//使用默认德鲁伊
dataSourceCreator = creators.get(0);
}
return dataSourceCreator.createrDataSource(dataSourceProperty);
}
}
这里我们在搞一个默认创建器,该创建器的作用其实也就是拿到所有类型的创建器之后,然后去匹配用户在application.yml中设置的连接池类型来进行匹配,匹配上了就用该连接池的创建器创建数据源。
那么我们该如何导入到spring容器中呢,这里我们可以再定义一个创建器的自动装配类
package com.xzq.dynamic;
@Configuration
@AllArgsConstructor
@EnableConfigurationProperties(DynamicProperties.class)
public class DynamicDataSourceCreatorAutoConfiguration {
private final DynamicProperties properties;
@Bean
@ConditionalOnMissingBean
public DefaultDataSourceCreator defaultDataSourceCreator(List creatorList) {
return new DefaultDataSourceCreator(creatorList);
}
@Bean
@ConditionalOnMissingBean
public DruidDataSourceCreator druidDataSourceCreator() {
return new DruidDataSourceCreator(properties);
}
@Bean
@ConditionalOnMissingBean
public HikariDataSourceCreator hikariDataSourceCreator() {
return new HikariDataSourceCreator(properties);
}
}
然后就是完善我们的动态数据源装配类
package com.xzq.dynamic;
@EnableConfigurationProperties(DynamicProperties.class)
@AutoConfigureBefore(value = DataSourceAutoConfiguration.class,name = "com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceAutoConfigure")
@import(DynamicDataSourceCreatorAutoConfiguration.class)
public class DynamicDataSourceAutoConfiguration implements InitializingBean {
private final DynamicProperties properties;
private final DefaultDataSourceCreator creator;
private Map dataSourceMap = new ConcurrentHashMap<>();
public DynamicDataSourceAutoConfiguration(DynamicProperties properties,DefaultDataSourceCreator creator) {
this.properties = properties;
this.creator = creator;
}
@Bean
public DsAspect dsAspect() {
return new DsAspect();
}
@Bean
@ConditionalOnMissingBean
public DataSource dataSource() {
DynamicRoutingDataSource routingDataSource = new DynamicRoutingDataSource();
routingDataSource.setPrimaryKey(properties.getPrimary());
routingDataSource.setDataSourceMap(dataSourceMap);
return routingDataSource;
}
@Override
public void afterPropertiesSet() throws Exception {
properties.getDatasource().forEach((k,v)->{
DataSource dataSource = creator.createDataSource(v);
dataSourceMap.put(k, dataSource);
});
}
}
OK ,到这里我们基本上已经完成了动态数据源的Starter,接下来让我们进行测试来看一下还有什么问题某有。
四 实测准备 1,数据库准备首先创建两个不同的库
create DATAbase test01; create DATAbase test02;2,表准备
准备两种表放在这两个库中
CREATE TABLE `user` (
`id` int NOT NULL AUTO_INCREMENT COMMENT '自增ID',
`userId` varchar(9) DEFAULT NULL COMMENT '用户ID',
`userNickName` varchar(32) DEFAULT NULL COMMENT '用户昵称',
`userHead` varchar(16) DEFAULT NULL COMMENT '用户头像',
`userPassword` varchar(64) DEFAULT NULL COMMENT '用户密码',
`createTime` datetime DEFAULT NULL COMMENT '创建时间',
`updateTime` datetime DEFAULT NULL COMMENT '更新时间',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb3;
INSERT INTO `test01`.`user` (`id`, `userId`, `userNickName`, `userHead`, `userPassword`, `createTime`, `updateTime`) VALUES ('1', '184172133', '小熊哥', '01_50', '123456', '2021-11-13 00:00:00', '2021-11-13 00:00:00');
INSERT INTO `test01`.`user` (`id`, `userId`, `userNickName`, `userHead`, `userPassword`, `createTime`, `updateTime`) VALUES ('2', '980765512', '你滴寒王', '02_50', '123456', '2021-11-13 00:00:00', '2021-11-13 00:00:00');
INSERT INTO `test01`.`user` (`id`, `userId`, `userNickName`, `userHead`, `userPassword`, `createTime`, `updateTime`) VALUES ('3', '796542178', 'EDG牛逼', '03_50', '123456', '2021-11-13 00:00:00', '2021-11-13 00:00:00');
CREATE TABLE `bs_city` (
`id` varchar(40) NOT NULL,
`name` varchar(255) NOT NULL,
`create_time` datetime NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
INSERT INTO `test02`.`bs_city` (`id`, `name`, `create_time`) VALUES ('1', '杭州', '2021-11-11 21:35:25');
INSERT INTO `test02`.`bs_city` (`id`, `name`, `create_time`) VALUES ('2', '郑州', '2021-11-11 21:35:36');
INSERT INTO `test02`.`bs_city` (`id`, `name`, `create_time`) VALUES ('3', '开封', '2021-11-11 21:35:42');
3,项目准备
创建一个springBoot项目,借助于苞米豆的代码生成器快速构建项目。
懒得搞的小伙伴可以去我的码云地址上拉下来直接用。顺便给个Star(那就更感激不尽了,哈哈)
spring:
datasource:
dynamic:
datasource:
one:
driver-class-name: com.mysql.cj.jdbc.Driver
username: root
password: 123456
url: jdbc:mysql://127.0.0.1:3306/test01?useUnicode=true&useSSL=false&characterEncoding=utf8
second:
driver-class-name: com.mysql.cj.jdbc.Driver
username: root
password: 123456
url: jdbc:mysql://127.0.0.1:3306/test02?useUnicode=true&useSSL=false&characterEncoding=utf8
druid:
initialSize: 10
maxActive: 50
minIdle: 5
maxWait: 60
minEvictableIdleTimeMillis: 30000
maxEvictableIdleTimeMillis: 30000
primary: one
type: com.alibaba.druid.pool.DruidDataSource
这里我们定义了两个数据源,one和second。默认是One
我们创建一个Controller并对其方法加上Ds注解
package com.xzq.controller;
@RestController
public class UserController {
@Autowired
private UserMapper userMapper;
@Autowired
private IUserService userService;
@RequestMapping("/test2")
@Ds("second")
public Object test(String id) {
return userService.getById(id);
}
}
启动项目测试
似乎看上去已经大功告成。
但是如果我们将Ds注解加到类上后,切面是不会对该类的方法进行拦截的。
那么为什么会这样呢?
首先我们先回顾一下我们的切面
可以看到我们的切入点表达式是注解类型的表达式。
看一下官网的介绍
可以看到切点表达式是针对方法的匹配
那我们在类上加上Ds注解,那么我们的切入点实现会根据切入点表达式去匹配方法是否拦截,而我们的方法由于没有加注解所以拦截不到。
那么PointCut的具体实现到底是什么呢? 匹配的逻辑又是什么呢?
其实PointCut切点中有两个重要的属性,分别是ClassFilter和MethodMatch
这是springAop中切点的接口定义。
那么这两个属性是干什么的呢,顾名思义,类过滤器就是针对类进行匹配,方法匹配器针对方法进行匹配。这两个肯定是先判断类符合规则不,如果符合在进行方法的匹配,先明确这一点。
那么是如何进行匹配的呢?没错就是切入点表达式,比如我们的切入点表达式是这样的
“@annotation(com.xzq.dynamic.annotation.Ds)”
那么类过滤器就是根据我们的目标类有没有这个注解进行过滤,显然我们的Ds标注在类上是符合的。
其次方法匹配器开始根据切入点表达式匹配方法有没有这个注解啊,欸,发现没有,那么就不拦截了。
以上只是我个人的一些猜想,那么实际情况是不是这样的呢?我们跟踪一下spring的aop的源码
这里就不展开对Aop的一长串跟踪了
其逻辑是这样的,首先开启@Aspectj 的自动代理,在项目启动后,AnnotationAwareAspectJAutoPr oxyCreator被注入到容器中,该类实现了BeanPostProcess,也就是说实现了对Bean的增强功能。
在每一个Bean的初始化方法前后,会调用AnnotationAwareAspectJAutoProxyCreator的postProcessBeforeInitialization方法和postProcessAfterInitialization方法,在postProcessAfterInitialization方法中会去获取所有的增强器,也就是去获取标注了@Aspect注解的类,然后将该类转成Advisor增强器,然后在拿着这些增强器匹配当前的Bean。如何匹配的最终会调用到上图所示的CanApply()方法,也就是是否可以使用的方法。
在这里先进行类匹配然后在方法匹配,匹配逻辑就是根据切入点表达式。
而我么你的Ds注解只作用在类上,没有作用在方法上,方法匹配器匹会匹配失败,所以造成拦截不住。
好了,重点来了,如何绕过方法匹配的匹配,直接类匹配上就不走方法匹配器,默认拦截该类的所有方法呢?
主要是通过spring提供的一个注解切点实现来绕过方法匹配器。
具体实现下次完善本文。
这次一定一键三联!



