diff --git a/sentinel-core/src/main/java/com/alibaba/csp/sentinel/annotation/SentinelResource.java b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/annotation/SentinelResource.java new file mode 100644 index 0000000000..d23fa7d8c8 --- /dev/null +++ b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/annotation/SentinelResource.java @@ -0,0 +1,53 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed 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 com.alibaba.csp.sentinel.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import com.alibaba.csp.sentinel.EntryType; + +/** + * @author Eric Zhao + */ +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +@Inherited +public @interface SentinelResource { + + /** + * @return name of the Sentinel resource + */ + String value(); + + /** + * @return the entry type (inbound or outbound), outbound by default + */ + EntryType entryType() default EntryType.OUT; + + /** + * @return name of the block exception function, empty by default + */ + String blockHandler() default ""; + + /** + * @return name of the fallback function, empty by default + */ + String fallback() default ""; +} diff --git a/sentinel-extension/pom.xml b/sentinel-extension/pom.xml index e72add499a..080f3f29e6 100755 --- a/sentinel-extension/pom.xml +++ b/sentinel-extension/pom.xml @@ -15,6 +15,8 @@ sentinel-datasource-extension sentinel-datasource-nacos sentinel-datasource-zookeeper + + sentinel-annotation-aspectj diff --git a/sentinel-extension/sentinel-annotation-aspectj/README.md b/sentinel-extension/sentinel-annotation-aspectj/README.md new file mode 100644 index 0000000000..36227658eb --- /dev/null +++ b/sentinel-extension/sentinel-annotation-aspectj/README.md @@ -0,0 +1,63 @@ +# Sentinel Annotation AspectJ + +This extension is an AOP implementation using AspectJ for Sentinel annotations. +Currently only runtime waving is supported. + +## Annotation + +The `@SentinelResource` annotation indicates a resource definition, including: + +- `value`: Resource name, required (cannot be empty) +- `entryType`: Resource entry type (inbound or outbound), `EntryType.OUT` by default +- `fallback`: Fallback method when degraded (optional). The fallback method should be located in the same class with original method. The signature of the fallback method should match the original method (parameter types and return type) +- `blockHandler`: Handler method that handles `BlockException` when blocked. The block handler method should be located in the same class with original method, and the signature should match original method, with the last additional parameter type `BlockException`. + +For example: + +```java +@SentinelResource(value = "abc", fallback = "doFallback", blockHandler = "handleException") +public String doSomething(long i) { + return "Hello " + i; +} + +public String doFallback(long i) { + // Return fallback value. + return "Oops, degraded"; +} + +public String handleException(long i, BlockException ex) { + // Handle the block exception here. + return null; +} +``` + +## Configuration + +### AspectJ + +If you are using AspectJ directly, you can add the Sentinel annotation aspect to +your `aop.xml`: + +```xml + + + +``` + +### Spring AOP + +If you are using Spring AOP, you should add a configuration to register the aspect +as a Spring bean: + +```java +@Configuration +public class SentinelAspectConfiguration { + + @Bean + public SentinelResourceAspect sentinelResourceAspect() { + return new SentinelResourceAspect(); + } +} +``` + +An example for using Sentinel Annotation AspectJ with Spring Boot can be found [here](https://github.com/alibaba/Sentinel/tree/master/sentinel-demo/sentinel-demo-annotation-spring-aop). \ No newline at end of file diff --git a/sentinel-extension/sentinel-annotation-aspectj/pom.xml b/sentinel-extension/sentinel-annotation-aspectj/pom.xml new file mode 100644 index 0000000000..fdd2ff91ab --- /dev/null +++ b/sentinel-extension/sentinel-annotation-aspectj/pom.xml @@ -0,0 +1,37 @@ + + + + sentinel-extension + com.alibaba.csp + 0.1.1-SNAPSHOT + + 4.0.0 + + sentinel-annotation-aspectj + jar + + + 1.9.1 + + + + + com.alibaba.csp + sentinel-core + + + + org.aspectj + aspectjrt + ${aspectj.version} + + + org.aspectj + aspectjweaver + ${aspectj.version} + + + + \ No newline at end of file diff --git a/sentinel-extension/sentinel-annotation-aspectj/src/main/java/com/alibaba/csp/sentinel/annotation/aspectj/MethodWrapper.java b/sentinel-extension/sentinel-annotation-aspectj/src/main/java/com/alibaba/csp/sentinel/annotation/aspectj/MethodWrapper.java new file mode 100644 index 0000000000..a587678b67 --- /dev/null +++ b/sentinel-extension/sentinel-annotation-aspectj/src/main/java/com/alibaba/csp/sentinel/annotation/aspectj/MethodWrapper.java @@ -0,0 +1,51 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed 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 com.alibaba.csp.sentinel.annotation.aspectj; + +import java.lang.reflect.Method; + +/** + * @author Eric Zhao + */ +class MethodWrapper { + + private final Method method; + private final boolean present; + + private MethodWrapper(Method method, boolean present) { + this.method = method; + this.present = present; + } + + static MethodWrapper wrap(Method method) { + if (method == null) { + return none(); + } + return new MethodWrapper(method, true); + } + + static MethodWrapper none() { + return new MethodWrapper(null, false); + } + + Method getMethod() { + return method; + } + + boolean isPresent() { + return present; + } +} diff --git a/sentinel-extension/sentinel-annotation-aspectj/src/main/java/com/alibaba/csp/sentinel/annotation/aspectj/ResourceMetadataRegistry.java b/sentinel-extension/sentinel-annotation-aspectj/src/main/java/com/alibaba/csp/sentinel/annotation/aspectj/ResourceMetadataRegistry.java new file mode 100644 index 0000000000..1fb0ce7f01 --- /dev/null +++ b/sentinel-extension/sentinel-annotation-aspectj/src/main/java/com/alibaba/csp/sentinel/annotation/aspectj/ResourceMetadataRegistry.java @@ -0,0 +1,57 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed 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 com.alibaba.csp.sentinel.annotation.aspectj; + +import java.lang.reflect.Method; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import com.alibaba.csp.sentinel.util.StringUtil; + +/** + * @author Eric Zhao + */ +public final class ResourceMetadataRegistry { + + private static final Map FALLBACK_MAP = new ConcurrentHashMap(); + private static final Map BLOCK_HANDLER_MAP = new ConcurrentHashMap(); + + public static MethodWrapper lookupFallback(Class clazz, String name) { + return FALLBACK_MAP.get(getKey(clazz, name)); + } + + public static MethodWrapper lookupBlockHandler(Class clazz, String name) { + return BLOCK_HANDLER_MAP.get(getKey(clazz, name)); + } + + public static void updateFallbackFor(Class clazz, String name, Method method) { + if (clazz == null || StringUtil.isBlank(name)) { + throw new IllegalArgumentException("Bad argument"); + } + FALLBACK_MAP.put(getKey(clazz, name), MethodWrapper.wrap(method)); + } + + public static void updateBlockHandlerFor(Class clazz, String name, Method method) { + if (clazz == null || StringUtil.isBlank(name)) { + throw new IllegalArgumentException("Bad argument"); + } + BLOCK_HANDLER_MAP.put(getKey(clazz, name), MethodWrapper.wrap(method)); + } + + public static String getKey(Class clazz, String name) { + return String.format("%s:%s", clazz.getCanonicalName(), name); + } +} diff --git a/sentinel-extension/sentinel-annotation-aspectj/src/main/java/com/alibaba/csp/sentinel/annotation/aspectj/SentinelResourceAspect.java b/sentinel-extension/sentinel-annotation-aspectj/src/main/java/com/alibaba/csp/sentinel/annotation/aspectj/SentinelResourceAspect.java new file mode 100644 index 0000000000..f4cea7b8f0 --- /dev/null +++ b/sentinel-extension/sentinel-annotation-aspectj/src/main/java/com/alibaba/csp/sentinel/annotation/aspectj/SentinelResourceAspect.java @@ -0,0 +1,177 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed 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 com.alibaba.csp.sentinel.annotation.aspectj; + +import java.lang.reflect.Method; +import java.util.Arrays; + +import com.alibaba.csp.sentinel.Entry; +import com.alibaba.csp.sentinel.EntryType; +import com.alibaba.csp.sentinel.SphU; +import com.alibaba.csp.sentinel.annotation.SentinelResource; +import com.alibaba.csp.sentinel.context.ContextUtil; +import com.alibaba.csp.sentinel.log.RecordLog; +import com.alibaba.csp.sentinel.slots.block.BlockException; +import com.alibaba.csp.sentinel.slots.block.degrade.DegradeException; +import com.alibaba.csp.sentinel.util.StringUtil; + +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.annotation.Pointcut; +import org.aspectj.lang.reflect.MethodSignature; + +/** + * Aspect for methods with {@link SentinelResource} annotation. + * + * @author Eric Zhao + */ +@Aspect +public class SentinelResourceAspect { + + @Pointcut("@annotation(com.alibaba.csp.sentinel.annotation.SentinelResource)") + public void sentinelResourceAnnotationPointcut() { + } + + @Around("sentinelResourceAnnotationPointcut()") + public Object invokeResourceWithSentinel(ProceedingJoinPoint pjp) throws Throwable { + Method originMethod = getMethod(pjp); + + SentinelResource annotation = originMethod.getAnnotation(SentinelResource.class); + if (annotation == null) { + // Should not go through here. + throw new IllegalStateException("Wrong state for SentinelResource annotation"); + } + String resourceName = annotation.value(); + EntryType entryType = annotation.entryType(); + Entry entry = null; + try { + ContextUtil.enter(resourceName); + entry = SphU.entry(resourceName, entryType); + Object result = pjp.proceed(); + return result; + } catch (BlockException ex) { + return handleBlockException(pjp, annotation, ex); + } finally { + if (entry != null) { + entry.exit(); + } + ContextUtil.exit(); + } + } + + private Object handleBlockException(ProceedingJoinPoint pjp, SentinelResource annotation, BlockException ex) + throws Exception { + // Execute fallback for degrading if configured. + Object[] originArgs = pjp.getArgs(); + if (isDegradeFailure(ex)) { + Method method = extractFallbackMethod(pjp, annotation.fallback()); + if (method != null) { + return method.invoke(pjp.getTarget(), originArgs); + } + } + // Execute block handler if configured. + Method blockHandler = extractBlockHandlerMethod(pjp, annotation.blockHandler()); + if (blockHandler != null) { + Object[] args = Arrays.copyOf(originArgs, originArgs.length + 1); + args[args.length - 1] = ex; + return blockHandler.invoke(pjp.getTarget(), args); + } + // If no block handler is present, then directly throw the exception. + throw ex; + } + + private boolean isDegradeFailure(/*@NonNull*/ BlockException ex) { + return ex instanceof DegradeException; + } + + private Method extractFallbackMethod(ProceedingJoinPoint pjp, String fallbackName) { + if (StringUtil.isBlank(fallbackName)) { + return null; + } + Class clazz = pjp.getTarget().getClass(); + MethodWrapper m = ResourceMetadataRegistry.lookupFallback(clazz, fallbackName); + if (m == null) { + // First time, resolve the fallback. + Method method = resolveFallbackInternal(pjp, fallbackName); + // Cache the method instance. + ResourceMetadataRegistry.updateFallbackFor(clazz, fallbackName, method); + return method; + } + if (!m.isPresent()) { + return null; + } + return m.getMethod(); + } + + private Method resolveFallbackInternal(ProceedingJoinPoint pjp, /*@NonNull*/ String name) { + Method originMethod = getMethod(pjp); + Class[] parameterTypes = originMethod.getParameterTypes(); + return findMethod(pjp.getTarget().getClass(), name, originMethod.getReturnType(), parameterTypes); + } + + private Method findMethod(Class clazz, String name, Class returnType, Class... parameterTypes) { + Method[] methods = clazz.getDeclaredMethods(); + for (Method method : methods) { + if (name.equals(method.getName()) && returnType.isAssignableFrom(method.getReturnType()) + && Arrays.equals(parameterTypes, method.getParameterTypes())) { + return method; + } + } + // Current class nou found, find in the super classes recursively. + Class superClass = clazz.getSuperclass(); + if (superClass != null && !Object.class.equals(superClass)) { + return findMethod(superClass, name, returnType, parameterTypes); + } else { + RecordLog.info( + String.format("[SentinelResourceAspect] Cannot find method [%s] in class [%s] with parameters %s", + name, clazz.getCanonicalName(), Arrays.toString(parameterTypes))); + return null; + } + } + + private Method extractBlockHandlerMethod(ProceedingJoinPoint pjp, String name) { + if (StringUtil.isBlank(name)) { + return null; + } + Class clazz = pjp.getTarget().getClass(); + MethodWrapper m = ResourceMetadataRegistry.lookupBlockHandler(clazz, name); + if (m == null) { + // First time, resolve the block handler. + Method method = resolveBlockHandlerInternal(pjp, name); + // Cache the method instance. + ResourceMetadataRegistry.updateBlockHandlerFor(clazz, name, method); + return method; + } + if (!m.isPresent()) { + return null; + } + return m.getMethod(); + } + + private Method resolveBlockHandlerInternal(ProceedingJoinPoint pjp, /*@NonNull*/ String name) { + Method originMethod = getMethod(pjp); + Class[] originList = originMethod.getParameterTypes(); + Class[] parameterTypes = Arrays.copyOf(originList, originList.length + 1); + parameterTypes[parameterTypes.length - 1] = BlockException.class; + return findMethod(pjp.getTarget().getClass(), name, originMethod.getReturnType(), parameterTypes); + } + + private Method getMethod(ProceedingJoinPoint joinPoint) { + MethodSignature signature = (MethodSignature)joinPoint.getSignature(); + return signature.getMethod(); + } +}