From 0808500841e9c4d2f4b6bbbf53fd8e0204ea5388 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johnny=20Miller=20=28=E9=94=BA=E4=BF=8A=29?= Date: Tue, 1 Feb 2022 22:40:45 +0800 Subject: [PATCH] feat($OpenFeign): create an aspect for Feign log --- .../MafAutoConfiguration.java | 55 ++++-- .../aspect/FeignClientLogAspect.java | 171 ++++++++++++++++++ .../FeignClientConfigurationProperties.java | 30 +++ .../helper/SpringBootStartupHelper.java | 39 ++-- 4 files changed, 259 insertions(+), 36 deletions(-) create mode 100644 spring-cloud-starter/src/main/java/com/jmsoftware/maf/springcloudstarter/aspect/FeignClientLogAspect.java create mode 100644 spring-cloud-starter/src/main/java/com/jmsoftware/maf/springcloudstarter/configuration/FeignClientConfigurationProperties.java diff --git a/spring-cloud-starter/src/main/java/com/jmsoftware/maf/springcloudstarter/MafAutoConfiguration.java b/spring-cloud-starter/src/main/java/com/jmsoftware/maf/springcloudstarter/MafAutoConfiguration.java index 18974ac1..c4d66a0f 100644 --- a/spring-cloud-starter/src/main/java/com/jmsoftware/maf/springcloudstarter/MafAutoConfiguration.java +++ b/spring-cloud-starter/src/main/java/com/jmsoftware/maf/springcloudstarter/MafAutoConfiguration.java @@ -3,6 +3,7 @@ import com.baomidou.mybatisplus.core.exceptions.MybatisPlusException; import com.jmsoftware.maf.springcloudstarter.aspect.CommonExceptionControllerAdvice; import com.jmsoftware.maf.springcloudstarter.aspect.DatabaseExceptionControllerAdvice; +import com.jmsoftware.maf.springcloudstarter.aspect.FeignClientLogAspect; import com.jmsoftware.maf.springcloudstarter.aspect.WebRequestLogAspect; import com.jmsoftware.maf.springcloudstarter.configuration.*; import com.jmsoftware.maf.springcloudstarter.controller.CommonController; @@ -57,7 +58,8 @@ MafConfigurationProperties.class, MafProjectProperties.class, JwtConfigurationProperties.class, - ExcelImportConfigurationProperties.class + ExcelImportConfigurationProperties.class, + FeignClientConfigurationProperties.class }) @Import({ WebMvcConfiguration.class, @@ -75,6 +77,8 @@ WebSocketConfiguration.class }) public class MafAutoConfiguration { + private static final String INITIAL_MESSAGE = "Initial bean: '{}'"; + @PostConstruct public void postConstruct() { log.warn("Post construction of '{}'", this.getClass().getSimpleName()); @@ -83,46 +87,49 @@ public void postConstruct() { @Bean @ConditionalOnMissingBean public CommonExceptionControllerAdvice exceptionControllerAdvice() { - log.warn("Initial bean: '{}'", CommonExceptionControllerAdvice.class.getSimpleName()); + log.warn(INITIAL_MESSAGE, CommonExceptionControllerAdvice.class.getSimpleName()); return new CommonExceptionControllerAdvice(); } @Bean @ConditionalOnClass({MyBatisSystemException.class, MybatisPlusException.class, PersistenceException.class}) public DatabaseExceptionControllerAdvice databaseExceptionControllerAdvice() { - log.warn("Initial bean: '{}'", DatabaseExceptionControllerAdvice.class.getSimpleName()); + log.warn(INITIAL_MESSAGE, DatabaseExceptionControllerAdvice.class.getSimpleName()); return new DatabaseExceptionControllerAdvice(); } @Bean @ConditionalOnProperty(value = "maf.configuration.web-request-log-enabled") public WebRequestLogAspect webRequestLogAspect() { - log.warn("Initial bean: '{}'", WebRequestLogAspect.class.getSimpleName()); + log.warn(INITIAL_MESSAGE, WebRequestLogAspect.class.getSimpleName()); return new WebRequestLogAspect(); } @Bean public RedirectController redirectController() { - log.warn("Initial bean: '{}'", RedirectController.class.getSimpleName()); + log.warn(INITIAL_MESSAGE, RedirectController.class.getSimpleName()); return new RedirectController(); } @Bean public AccessLogFilter requestFilter(MafConfigurationProperties mafConfigurationProperties) { - log.warn("Initial bean: '{}'", AccessLogFilter.class.getSimpleName()); + log.warn(INITIAL_MESSAGE, AccessLogFilter.class.getSimpleName()); return new AccessLogFilter(mafConfigurationProperties); } @Bean public IpHelper ipHelper(Environment environment) { - log.warn("Initial bean: '{}'", IpHelper.class.getSimpleName()); + log.warn(INITIAL_MESSAGE, IpHelper.class.getSimpleName()); return new IpHelper(environment); } @Bean - public SpringBootStartupHelper springBootStartupHelper(MafProjectProperties mafProjectProperties, - IpHelper ipHelper, ApplicationContext applicationContext) { - log.warn("Initial bean: '{}'", SpringBootStartupHelper.class.getSimpleName()); + public SpringBootStartupHelper springBootStartupHelper( + MafProjectProperties mafProjectProperties, + IpHelper ipHelper, + ApplicationContext applicationContext + ) { + log.warn(INITIAL_MESSAGE, SpringBootStartupHelper.class.getSimpleName()); return new SpringBootStartupHelper(mafProjectProperties, ipHelper, applicationContext); } @@ -131,33 +138,47 @@ public SpringBootStartupHelper springBootStartupHelper(MafProjectProperties mafP public GlobalErrorController globalErrorController(ErrorAttributes errorAttributes, ServerProperties serverProperties, List errorViewResolvers) { - log.warn("Initial bean: '{}'", GlobalErrorController.class.getSimpleName()); + log.warn(INITIAL_MESSAGE, GlobalErrorController.class.getSimpleName()); return new GlobalErrorController(errorAttributes, serverProperties, errorViewResolvers); } @Bean public HttpApiScanHelper httpApiScanHelper(RequestMappingHandlerMapping requestMappingHandlerMapping) { - log.warn("Initial bean: '{}'", HttpApiScanHelper.class.getSimpleName()); + log.warn(INITIAL_MESSAGE, HttpApiScanHelper.class.getSimpleName()); return new HttpApiScanHelper(requestMappingHandlerMapping); } @Bean - public HttpApiResourceRemoteApiController httpApiResourceRemoteController(MafConfigurationProperties mafConfigurationProperties, - HttpApiScanHelper httpApiScanHelper) { - log.warn("Initial bean: '{}'", HttpApiResourceRemoteApiController.class.getSimpleName()); + public HttpApiResourceRemoteApiController httpApiResourceRemoteController( + MafConfigurationProperties mafConfigurationProperties, + HttpApiScanHelper httpApiScanHelper + ) { + log.warn(INITIAL_MESSAGE, HttpApiResourceRemoteApiController.class.getSimpleName()); return new HttpApiResourceRemoteApiController(mafConfigurationProperties, httpApiScanHelper); } @Bean @RefreshScope public CommonService commonService(MafProjectProperties mafProjectProperties) { - log.warn("Initial bean: '{}'", CommonServiceImpl.class.getSimpleName()); + log.warn(INITIAL_MESSAGE, CommonServiceImpl.class.getSimpleName()); return new CommonServiceImpl(mafProjectProperties); } @Bean public CommonController commonController(CommonService commonService) { - log.warn("Initial bean: '{}'", CommonController.class.getSimpleName()); + log.warn(INITIAL_MESSAGE, CommonController.class.getSimpleName()); return new CommonController(commonService); } + + @Bean + @ConditionalOnProperty( + prefix = "feign.client.config.default", + name = {"enabledAopLog"}, + havingValue = "true", + matchIfMissing = true + ) + public FeignClientLogAspect feignClientLogAspect() { + log.warn(INITIAL_MESSAGE, FeignClientLogAspect.class.getSimpleName()); + return new FeignClientLogAspect(); + } } diff --git a/spring-cloud-starter/src/main/java/com/jmsoftware/maf/springcloudstarter/aspect/FeignClientLogAspect.java b/spring-cloud-starter/src/main/java/com/jmsoftware/maf/springcloudstarter/aspect/FeignClientLogAspect.java new file mode 100644 index 00000000..64ca593d --- /dev/null +++ b/spring-cloud-starter/src/main/java/com/jmsoftware/maf/springcloudstarter/aspect/FeignClientLogAspect.java @@ -0,0 +1,171 @@ +package com.jmsoftware.maf.springcloudstarter.aspect; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.stream.StreamUtil; +import cn.hutool.core.text.CharSequenceUtil; +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.json.JSONUtil; +import lombok.extern.slf4j.Slf4j; +import lombok.val; +import org.aspectj.lang.JoinPoint; +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; +import org.springframework.cloud.openfeign.FeignClient; +import org.springframework.web.bind.annotation.*; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Method; +import java.time.Duration; +import java.time.Instant; +import java.util.Collections; +import java.util.Optional; +import java.util.Set; + +/** + *

FeignClientLogAspect

+ *

+ * Change description here. + * + * @author Johnny Miller (鍾俊), email: johnnysviva@outlook.com, 2/1/22 1:10 PM + * @see + * @FeignClient cannot be used as a pointcut by Spring aop + **/ +@Slf4j +@Aspect +public class FeignClientLogAspect { + private static final String LINE_SEPARATOR = System.lineSeparator(); + private static final Set> REQUEST_MAPPING_SET = Collections.unmodifiableSet(CollUtil.newHashSet( + RequestMapping.class, + GetMapping.class, + PostMapping.class, + PutMapping.class, + DeleteMapping.class, + PatchMapping.class + )); + private static final String BEFORE_TEMPLATE = LINE_SEPARATOR + + "============ FEIGN CLIENT LOG (@Before) ============" + LINE_SEPARATOR + + "Feign URL : [{}] {}" + LINE_SEPARATOR + + "Class Method : {}#{}" + LINE_SEPARATOR + + "Request Params :" + LINE_SEPARATOR + "{}"; + private static final String AROUND_TEMPLATE = LINE_SEPARATOR + + "============ FEIGN CLIENT LOG (@Around) ============" + LINE_SEPARATOR + + "Feign URL : [{}] {}" + LINE_SEPARATOR + + "Class Method : {}#{}" + LINE_SEPARATOR + + "Elapsed Time : {} ms" + LINE_SEPARATOR + + "Feign Request Params :" + LINE_SEPARATOR + "{}" + LINE_SEPARATOR + + "Feign Response Params:" + LINE_SEPARATOR + "{}"; + + @Pointcut("@within(org.springframework.cloud.openfeign.FeignClient)" + + " || @annotation(org.springframework.cloud.openfeign.FeignClient)") + public void feignClientPointcut() { + // Do nothing + } + + // @Before("feignClientPointcut()") + @SuppressWarnings({"unused", "AlibabaCommentsMustBeJavadocFormat"}) + public void beforeHandleRequest(JoinPoint joinPoint) { + val signature = joinPoint.getSignature(); + if (!(signature instanceof MethodSignature)) { + return; + } + log.info(BEFORE_TEMPLATE, + this.getFeignMethod((MethodSignature) signature).toUpperCase(), + this.getFeignUrl((MethodSignature) signature), + signature.getDeclaringTypeName(), signature.getName(), + JSONUtil.toJsonStr(joinPoint.getArgs())); + } + + @Around("feignClientPointcut()") + public Object feignClientAround(ProceedingJoinPoint joinPoint) throws Throwable { + val startInstant = Instant.now(); + val result = joinPoint.proceed(); + val duration = Duration.between(startInstant, Instant.now()); + val signature = joinPoint.getSignature(); + if (!(signature instanceof MethodSignature)) { + return result; + } + String response; + try { + response = JSONUtil.toJsonStr(result); + } catch (Exception e) { + log.warn("Failed to convert response to JSON string. {}", e.getMessage()); + response = ObjectUtil.toString(result); + } + log.info(AROUND_TEMPLATE, + this.getFeignMethod((MethodSignature) signature).toUpperCase(), + this.getFeignUrl((MethodSignature) signature), + signature.getDeclaringTypeName(), signature.getName(), + duration.toMillis(), + JSONUtil.toJsonStr(joinPoint.getArgs()), + response); + return result; + } + + @SuppressWarnings("HttpUrlsUsage") + private String getFeignUrl(MethodSignature methodSignature) { + val method = methodSignature.getMethod(); + val feignClientClass = method.getDeclaringClass(); + val feignClient = feignClientClass.getAnnotation(FeignClient.class); + val feignUrl = new StringBuilder("http://"); + this.concatDomain(feignClient, feignUrl); + this.concatPath(method, feignUrl); + return feignUrl.toString(); + } + + private String getFeignMethod(MethodSignature methodSignature) { + return Optional.ofNullable(methodSignature.getMethod().getAnnotations()) + .map(annotations -> annotations[0]) + .map(annotation -> CharSequenceUtil.removeAll( + annotation.annotationType().getSimpleName(), "Mapping" + )) + .orElse(""); + } + + private void concatPath(Method method, StringBuilder feignUrl) { + StreamUtil.of(method.getAnnotations()) + .filter(annotation -> REQUEST_MAPPING_SET.contains(annotation.annotationType())) + .findAny() + .ifPresent(annotation -> feignUrl.append(this.getFirstValue(annotation))); + } + + private String getFirstValue(Annotation annotation) { + if (annotation instanceof RequestMapping) { + return ((RequestMapping) annotation).value()[0]; + } + if (annotation instanceof GetMapping) { + return ((GetMapping) annotation).value()[0]; + } + if (annotation instanceof PostMapping) { + return ((PostMapping) annotation).value()[0]; + } + if (annotation instanceof PutMapping) { + return ((PutMapping) annotation).value()[0]; + } + if (annotation instanceof DeleteMapping) { + return ((DeleteMapping) annotation).value()[0]; + } + if (annotation instanceof PatchMapping) { + return ((PatchMapping) annotation).value()[0]; + } + return ""; + } + + private void concatDomain(FeignClient feignClient, StringBuilder feignUrl) { + if (CharSequenceUtil.isNotBlank(feignClient.value())) { + feignUrl.append(feignClient.value()); + return; + } + if (CharSequenceUtil.isNotBlank(feignClient.url())) { + feignUrl.append(feignClient.url()); + return; + } + if (CharSequenceUtil.isNotBlank(feignClient.name())) { + feignUrl.append(feignClient.name()); + return; + } + feignUrl.append("unrecognized"); + } +} diff --git a/spring-cloud-starter/src/main/java/com/jmsoftware/maf/springcloudstarter/configuration/FeignClientConfigurationProperties.java b/spring-cloud-starter/src/main/java/com/jmsoftware/maf/springcloudstarter/configuration/FeignClientConfigurationProperties.java new file mode 100644 index 00000000..17cb5ed6 --- /dev/null +++ b/spring-cloud-starter/src/main/java/com/jmsoftware/maf/springcloudstarter/configuration/FeignClientConfigurationProperties.java @@ -0,0 +1,30 @@ +package com.jmsoftware.maf.springcloudstarter.configuration; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.cloud.context.config.annotation.RefreshScope; +import org.springframework.context.annotation.Configuration; +import org.springframework.validation.annotation.Validated; + +import javax.validation.constraints.NotNull; + +/** + *

FeignClientConfigurationProperties

+ *

+ * Change description here. + * + * @author Johnny Miller (鍾俊), email: johnnysviva@outlook.com, 2/1/22 6:03 PM + **/ +@Data +@Validated +@RefreshScope +@Configuration +@ConfigurationProperties(prefix = FeignClientConfigurationProperties.PREFIX) +public class FeignClientConfigurationProperties { + public static final String PREFIX = "feign.client.config.default"; + /** + * Enabled AOP log for Feign clients. Default is true. True means enabled, false means disabled. + */ + @NotNull + private Boolean enabledAopLog = Boolean.TRUE; +} diff --git a/spring-cloud-starter/src/main/java/com/jmsoftware/maf/springcloudstarter/helper/SpringBootStartupHelper.java b/spring-cloud-starter/src/main/java/com/jmsoftware/maf/springcloudstarter/helper/SpringBootStartupHelper.java index 7a1b6b23..aa45828c 100644 --- a/spring-cloud-starter/src/main/java/com/jmsoftware/maf/springcloudstarter/helper/SpringBootStartupHelper.java +++ b/spring-cloud-starter/src/main/java/com/jmsoftware/maf/springcloudstarter/helper/SpringBootStartupHelper.java @@ -1,18 +1,14 @@ package com.jmsoftware.maf.springcloudstarter.helper; -import cn.hutool.core.lang.Console; import com.jmsoftware.maf.springcloudstarter.property.MafProjectProperties; import lombok.NonNull; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import lombok.val; import org.springframework.beans.factory.DisposableBean; -import org.springframework.beans.factory.support.DefaultListableBeanFactory; import org.springframework.context.ApplicationContext; import org.springframework.util.StopWatch; import java.time.Instant; -import java.time.ZoneId; import java.util.TimeZone; /** @@ -24,27 +20,32 @@ @RequiredArgsConstructor public class SpringBootStartupHelper implements DisposableBean { private static final String LINE_SEPARATOR = System.lineSeparator(); + @SuppressWarnings("HttpUrlsUsage") + private static final String TEMPLATE = LINE_SEPARATOR + + "🥳 Congratulations! 🎉" + LINE_SEPARATOR + + "🖥 {}@{} started! " + LINE_SEPARATOR + + "⚙️ Environment: {}" + LINE_SEPARATOR + + "⏳ Deployment duration: {} seconds ({} ms)" + LINE_SEPARATOR + + "⏰ App started at {} (timezone - {})" + LINE_SEPARATOR + + " App running at" + LINE_SEPARATOR + + " - Local: http://localhost:{}{}/" + LINE_SEPARATOR + + " - Network: http://{}:{}{}/"; private final MafProjectProperties mafProjectProperties; private final IpHelper ipHelper; private final ApplicationContext applicationContext; public void stop(@NonNull StopWatch stopWatch) { stopWatch.stop(); - Console.log("🥳 Congratulations! 🎉"); - Console.log("🖥 {}@{} started!", this.mafProjectProperties.getProjectArtifactId(), - this.mafProjectProperties.getVersion()); - Console.log("⚙️ Environment: {}", this.mafProjectProperties.getEnvironment()); - Console.log("⏳ Deployment duration: {} seconds ({} ms)", stopWatch.getTotalTimeSeconds(), - stopWatch.getTotalTimeMillis()); - Console.log("⏰ App started at {} (timezone - {})", Instant.now().atZone(ZoneId.of("UTC+8")), - TimeZone.getDefault().getDisplayName()); - Console.error(" App running at{} - Local: http://localhost:{}{}/{} - Network: http://{}:{}{}/", - LINE_SEPARATOR, this.ipHelper.getServerPort(), this.mafProjectProperties.getContextPath(), - LINE_SEPARATOR, this.ipHelper.getPublicIp(), this.ipHelper.getServerPort(), - this.mafProjectProperties.getContextPath()); - val defaultListableBeanFactory = - (DefaultListableBeanFactory) this.applicationContext.getAutowireCapableBeanFactory(); - defaultListableBeanFactory.destroyBean(this); + log.info( + TEMPLATE, + this.mafProjectProperties.getProjectArtifactId(), this.mafProjectProperties.getVersion(), + this.mafProjectProperties.getEnvironment(), + stopWatch.getTotalTimeSeconds(), stopWatch.getTotalTimeMillis(), + Instant.now(), TimeZone.getDefault().getDisplayName(), + this.ipHelper.getServerPort(), this.mafProjectProperties.getContextPath(), + this.ipHelper.getPublicIp(), this.ipHelper.getServerPort(), this.mafProjectProperties.getContextPath() + ); + this.applicationContext.getAutowireCapableBeanFactory().destroyBean(this); } @Override