Skip to content

Commit

Permalink
feat($OpenFeign): create an aspect for Feign log
Browse files Browse the repository at this point in the history
  • Loading branch information
johnnymillergh committed Feb 1, 2022
1 parent 6d4dbbb commit 0808500
Show file tree
Hide file tree
Showing 4 changed files with 259 additions and 36 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -57,7 +58,8 @@
MafConfigurationProperties.class,
MafProjectProperties.class,
JwtConfigurationProperties.class,
ExcelImportConfigurationProperties.class
ExcelImportConfigurationProperties.class,
FeignClientConfigurationProperties.class
})
@Import({
WebMvcConfiguration.class,
Expand All @@ -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());
Expand All @@ -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);
}

Expand All @@ -131,33 +138,47 @@ public SpringBootStartupHelper springBootStartupHelper(MafProjectProperties mafP
public GlobalErrorController globalErrorController(ErrorAttributes errorAttributes,
ServerProperties serverProperties,
List<ErrorViewResolver> 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();
}
}
Original file line number Diff line number Diff line change
@@ -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;

/**
* <h1>FeignClientLogAspect</h1>
* <p>
* Change description here.
*
* @author Johnny Miller (鍾俊), email: johnnysviva@outlook.com, 2/1/22 1:10 PM
* @see
* <a href='https://github.com/spring-cloud/spring-cloud-openfeign/issues/322'>@FeignClient cannot be used as a pointcut by Spring aop</a>
**/
@Slf4j
@Aspect
public class FeignClientLogAspect {
private static final String LINE_SEPARATOR = System.lineSeparator();
private static final Set<Class<?>> 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");
}
}
Original file line number Diff line number Diff line change
@@ -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;

/**
* <h1>FeignClientConfigurationProperties</h1>
* <p>
* 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;
}
Loading

0 comments on commit 0808500

Please sign in to comment.