Preface
In the previous article, we how to implement an SPI supports key-value pairs. In this issue, let’s talk about how to implement an SPI with interceptor function
What is an interceptor
Refers to intercepting a method or field before being accessed, and then adding certain operations before or after
What is an interceptor chain
Refers to linking interceptors into a chain in a certain order. When accessing the intercepted method or field, the interceptors in the interceptor chain will be called in the order they were previously defined
Implement interceptor logic
This article realizes the core of the idea: uses the responsibility chain + dynamic agent
1. Define the interceptor interface
public interface Interceptor {
int HIGHEST_PRECEDENCE = Integer.MIN_VALUE;
int LOWEST_PRECEDENCE = Integer.MAX_VALUE;
Object intercept(Invocation invocation) throws Throwable;
default Object plugin(Object target) {
return Plugin.wrap(target, this);
}
int getOrder();
}
2. Customize the class interface annotations that need to be intercepted by the interceptor
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface Intercepts {
Signature[] value();
}
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({})
public @interface Signature {
Class<?> type();
String method();
Class<?>[] args();
}
3. Implement interceptor call logic through jdk dynamic proxy
public class Plugin implements InvocationHandler {
private final Object target;
private final Interceptor interceptor;
private final Map<Class<?>, Set<Method>> signatureMap;
private Plugin(Object target, Interceptor interceptor, Map<Class<?>, Set<Method>> signatureMap) {
this.target = target;
this.interceptor = interceptor;
this.signatureMap = signatureMap;
}
public static Object wrap(Object target, Interceptor interceptor) {
Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor);
Class<?> type = target.getClass();
Class<?>[] interfaces = getAllInterfaces(type, signatureMap);
if (interfaces.length > 0) {
return Proxy.newProxyInstance(
type.getClassLoader(),
interfaces,
new Plugin(target, interceptor, signatureMap));
}
return target;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
try {
Set<Method> methods = signatureMap.get(method.getDeclaringClass());
if (methods != null && methods.contains(method)) {
Invocation invocation = new Invocation(target, method, args);
return interceptor.intercept(invocation);
}
return method.invoke(target, args);
} catch (Exception e) {
throw ExceptionUtils.unwrapThrowable(e);
}
}
private static Map<Class<?>, Set<Method>> getSignatureMap(Interceptor interceptor) {
Intercepts interceptsAnnotation = interceptor.getClass().getAnnotation(Intercepts.class);
if (interceptsAnnotation == null) {
throw new PluginException("No @Intercepts annotation was found in interceptor " + interceptor.getClass().getName());
}
Signature[] sigs = interceptsAnnotation.value();
Map<Class<?>, Set<Method>> signatureMap = new HashMap<>();
for (Signature sig : sigs) {
Set<Method> methods = signatureMap.computeIfAbsent(sig.type(), k -> new HashSet<>());
try {
Method method = sig.type().getMethod(sig.method(), sig.args());
methods.add(method);
} catch (NoSuchMethodException e) {
throw new PluginException("Could not find method on " + sig.type() + " named " + sig.method() + ". Cause: " + e, e);
}
}
return signatureMap;
}
private static Class<?>[] getAllInterfaces(Class<?> type, Map<Class<?>, Set<Method>> signatureMap) {
Set<Class<?>> interfaces = new HashSet<>();
while (type != null) {
for (Class<?> c : type.getInterfaces()) {
if (signatureMap.containsKey(c)) {
interfaces.add(c);
}
}
type = type.getSuperclass();
}
return interfaces.toArray(new Class<?>[interfaces.size()]);
}
}
4. Construct an interceptor chain
public class InterceptorChain {
private final List<Interceptor> interceptors = new ArrayList<>();
public Object pluginAll(Object target) {
if(CollectionUtil.isNotEmpty(interceptors)){
for (Interceptor interceptor : getInterceptors()) {
target = interceptor.plugin(target);
}
}
return target;
}
public InterceptorChain addInterceptor(Interceptor interceptor) {
interceptors.add(interceptor);
return this;
}
public List<Interceptor> getInterceptors() {
List<Interceptor> interceptorsByOrder = interceptors.stream().sorted(Comparator.comparing(Interceptor::getOrder).reversed()).collect(Collectors.toList());
return Collections.unmodifiableList(interceptorsByOrder);
}
}
5. Binding with the previously implemented SPI through the interceptor chain
@Activate
@Slf4j
public class InterceptorExtensionFactory implements ExtensionFactory {
private InterceptorChain chain;
@Override
public <T> T getExtension(final String key, final Class<T> clazz) {
if(Objects.isNull(chain)){
log.warn("No InterceptorChain Is Config" );
return null;
}
if (clazz.isInterface() && clazz.isAnnotationPresent(SPI.class)) {
ExtensionLoader<T> extensionLoader = ExtensionLoader.getExtensionLoader(clazz);
if (!extensionLoader.getSupportedExtensions().isEmpty()) {
if(StringUtils.isBlank(key)){
return (T) chain.pluginAll(extensionLoader.getDefaultActivate());
}
return (T) chain.pluginAll(extensionLoader.getActivate(key));
}
}
return null;
}
public InterceptorChain getChain() {
return chain;
}
public InterceptorExtensionFactory setChain(InterceptorChain chain) {
this.chain = chain;
return this;
}
Sample demo
1. Define the interceptor and specify the class interface method to be intercepted
@Intercepts(@Signature(type = SqlDialect.class,method = "dialect",args = {}))
public class MysqlDialectInterceptor implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
System.out.println("MysqlDialectInterceptor");
return invocation.proceed();
}
@Override
public int getOrder() {
return HIGHEST_PRECEDENCE;
}
@Override
public Object plugin(Object target) {
if(target.toString().startsWith(MysqlDialect.class.getName())){
return Plugin.wrap(target,this);
}
return target;
}
}
2. Construct the interceptor chain and set it to the spi factory
@Before
public void before(){
InterceptorChain chain = new InterceptorChain();
chain.addInterceptor(new DialectInterceptor()).addInterceptor(new MysqlDialectInterceptor()).addInterceptor(new OracleDialectInterceptor());
factory = (InterceptorExtensionFactory) (ExtensionLoader.getExtensionLoader(ExtensionFactory.class).getActivate("interceptor"));
factory.setChain(chain);
}
3. Test
@Test
public void testMysqlDialectInterceptor(){
SqlDialect dialect = factory.getExtension("mysql",SqlDialect.class);
Assert.assertEquals("mysql",dialect.dialect());
}
It can be seen from the console output that the interceptor call logic has been implemented
Summarize
Reading the implementation of the interceptor in this article, sharp-eyed friends will find that you are not just copying the implementation of the mybatis interceptor. This is indeed the case, but I prefer to shamelessly call this a practice of learning. The implementation of the interceptor of mybatis is indeed quite clever, because our routine implementation of interceptor chain calls normally uses a similar recursive method, but mybatis uses a dynamic proxy.
Of course, the interceptor in this article has also added some easter eggs, such as adding a custom execution sequence function that the native mybatis interceptor does not provide. The native mybatis interceptor can only intercept Executor, ParameterHandler, StatementHandler, and ResultSetHandler. This article does not have this restriction, but there is a point to note because the interceptor implementation is based on jdk dynamic proxy, so the custom annotation class can only be specified as an interface, not a specific implementation
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。