Actuator 인터셉터 엔드포인트 추가:
- `/actuator/mappings`에 누락된 Interceptor 목록 읽기용 엔드포인트 구현 - 내부 사용자 이메일 도메인 동적 주입 로직 UserTypeConfigurer 클래스에 추가 - 비동기 스타일 개선 및 CSS 메인 스타일 일부 수정
This commit is contained in:
@@ -0,0 +1,165 @@
|
||||
package com.eactive.apim.portal.config;
|
||||
|
||||
import java.lang.reflect.Array;
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.TreeMap;
|
||||
import org.springframework.boot.actuate.endpoint.annotation.Endpoint;
|
||||
import org.springframework.boot.actuate.endpoint.annotation.ReadOperation;
|
||||
import org.springframework.boot.actuate.info.Info;
|
||||
import org.springframework.boot.actuate.info.InfoContributor;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.util.ReflectionUtils;
|
||||
import org.springframework.web.servlet.HandlerInterceptor;
|
||||
import org.springframework.web.servlet.handler.AbstractHandlerMapping;
|
||||
import org.springframework.web.servlet.handler.MappedInterceptor;
|
||||
|
||||
/**
|
||||
* HandlerInterceptor 목록을 Actuator 로 노출한다.
|
||||
*
|
||||
* <p>배경: Actuator 의 {@code /actuator/mappings} 는 servlet filter / servlet / handler mapping 만
|
||||
* 보여주고 interceptor 는 항목 자체가 없다. {@code /actuator/beans} 로도 안 보이는데,
|
||||
* {@link PortalConfigWebDispatcherServlet#addInterceptors} 가 {@code new} 로 직접 생성해 등록하므로
|
||||
* 스프링 빈이 아니기 때문이다. 그래서 HandlerMapping 내부 목록을 직접 읽어서 노출한다.
|
||||
*
|
||||
* <p>노출 경로 2가지:
|
||||
* <ul>
|
||||
* <li>{@code GET /actuator/interceptors} — 전용 엔드포인트</li>
|
||||
* <li>{@code GET /actuator/info} 의 {@code interceptors} 항목 — Spring Boot Admin 은 커스텀
|
||||
* 엔드포인트용 화면이 없으므로, SBA UI(Details 탭의 Info 카드)에서 보려면 이쪽이 필요하다.
|
||||
* {@code management.info.env.enabled} 와 무관하게 InfoContributor 는 항상 동작한다.</li>
|
||||
* </ul>
|
||||
*
|
||||
* <p>이 설정은 actuator 가 classpath 에 있을 때만 활성화된다. actuator 는 build.gradle 에서
|
||||
* 로컬 전용(runtimeOnly + war 산출물 제외)이므로 WebLogic 배포본(dev/stage/prod)에서는
|
||||
* 조건이 거짓이 되어 이 클래스 자체가 로드되지 않는다.
|
||||
*/
|
||||
@Configuration
|
||||
@ConditionalOnClass(name = "org.springframework.boot.actuate.endpoint.annotation.Endpoint")
|
||||
public class InterceptorsEndpointConfig {
|
||||
|
||||
@Bean
|
||||
public InterceptorsEndpoint interceptorsEndpoint(ApplicationContext applicationContext) {
|
||||
return new InterceptorsEndpoint(applicationContext);
|
||||
}
|
||||
|
||||
@Endpoint(id = "interceptors")
|
||||
public static class InterceptorsEndpoint implements InfoContributor {
|
||||
|
||||
/** AbstractHandlerMapping 이 실제로 실행하는 interceptor 목록(private final). */
|
||||
private static final Field ADAPTED_INTERCEPTORS =
|
||||
ReflectionUtils.findField(AbstractHandlerMapping.class, "adaptedInterceptors");
|
||||
|
||||
/** MappedInterceptor 의 제외 패턴(private final PatternAdapter[]). include 는 public getter 가 있다. */
|
||||
private static final Field EXCLUDE_PATTERNS =
|
||||
ReflectionUtils.findField(MappedInterceptor.class, "excludePatterns");
|
||||
|
||||
static {
|
||||
if (ADAPTED_INTERCEPTORS != null) {
|
||||
ReflectionUtils.makeAccessible(ADAPTED_INTERCEPTORS);
|
||||
}
|
||||
if (EXCLUDE_PATTERNS != null) {
|
||||
ReflectionUtils.makeAccessible(EXCLUDE_PATTERNS);
|
||||
}
|
||||
}
|
||||
|
||||
private final ApplicationContext applicationContext;
|
||||
|
||||
public InterceptorsEndpoint(ApplicationContext applicationContext) {
|
||||
this.applicationContext = applicationContext;
|
||||
}
|
||||
|
||||
/**
|
||||
* HandlerMapping 빈 이름 → 등록 순서대로의 interceptor 목록.
|
||||
* 목록 순서가 곧 preHandle 실행 순서다(afterCompletion 은 역순).
|
||||
*/
|
||||
@ReadOperation
|
||||
public Map<String, List<Map<String, Object>>> interceptors() {
|
||||
Map<String, List<Map<String, Object>>> result = new TreeMap<>();
|
||||
Map<String, AbstractHandlerMapping> mappings =
|
||||
applicationContext.getBeansOfType(AbstractHandlerMapping.class);
|
||||
for (Map.Entry<String, AbstractHandlerMapping> entry : mappings.entrySet()) {
|
||||
List<Map<String, Object>> described = describe(entry.getValue());
|
||||
if (!described.isEmpty()) {
|
||||
result.put(entry.getKey(), described);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void contribute(Info.Builder builder) {
|
||||
builder.withDetail("interceptors", interceptors());
|
||||
}
|
||||
|
||||
private List<Map<String, Object>> describe(AbstractHandlerMapping mapping) {
|
||||
if (ADAPTED_INTERCEPTORS == null) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
Object raw = ReflectionUtils.getField(ADAPTED_INTERCEPTORS, mapping);
|
||||
if (!(raw instanceof List)) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
List<Map<String, Object>> described = new ArrayList<>();
|
||||
int order = 0;
|
||||
for (Object each : (List<?>) raw) {
|
||||
described.add(describe(order++, (HandlerInterceptor) each));
|
||||
}
|
||||
return described;
|
||||
}
|
||||
|
||||
private Map<String, Object> describe(int order, HandlerInterceptor interceptor) {
|
||||
Map<String, Object> row = new LinkedHashMap<>();
|
||||
row.put("order", order);
|
||||
if (interceptor instanceof MappedInterceptor) {
|
||||
MappedInterceptor mapped = (MappedInterceptor) interceptor;
|
||||
row.put("interceptor", mapped.getInterceptor().getClass().getName());
|
||||
row.put("include", nullSafe(mapped.getPathPatterns()));
|
||||
row.put("exclude", excludePatterns(mapped));
|
||||
} else {
|
||||
// WebMvcConfigurationSupport 가 자동으로 넣는 것들(ConversionServiceExposing 등).
|
||||
// 경로 제한이 없어 모든 요청에 적용된다.
|
||||
row.put("interceptor", interceptor.getClass().getName());
|
||||
row.put("include", Collections.singletonList("/**"));
|
||||
row.put("exclude", Collections.emptyList());
|
||||
}
|
||||
return row;
|
||||
}
|
||||
|
||||
/** PatternAdapter[] → 패턴 문자열 목록. PatternAdapter 는 package-private 이라 리플렉션으로 읽는다. */
|
||||
private List<String> excludePatterns(MappedInterceptor mapped) {
|
||||
if (EXCLUDE_PATTERNS == null) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
Object adapters = ReflectionUtils.getField(EXCLUDE_PATTERNS, mapped);
|
||||
if (adapters == null) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
int length = Array.getLength(adapters);
|
||||
List<String> patterns = new ArrayList<>(length);
|
||||
for (int i = 0; i < length; i++) {
|
||||
Object adapter = Array.get(adapters, i);
|
||||
Method getPatternString = ReflectionUtils.findMethod(adapter.getClass(), "getPatternString");
|
||||
if (getPatternString == null) {
|
||||
continue;
|
||||
}
|
||||
ReflectionUtils.makeAccessible(getPatternString);
|
||||
patterns.add((String) ReflectionUtils.invokeMethod(getPatternString, adapter));
|
||||
}
|
||||
return patterns;
|
||||
}
|
||||
|
||||
private List<String> nullSafe(String[] values) {
|
||||
return values == null ? Collections.emptyList() : Arrays.asList(values);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package com.eactive.apim.portal.config;
|
||||
|
||||
import com.eactive.apim.portal.common.util.UserTypeUtil;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
import javax.annotation.PostConstruct;
|
||||
|
||||
/**
|
||||
* application.yml 의 {@code portal.internal-user.email-domains} 값을
|
||||
* 정적 유틸리티인 {@link UserTypeUtil} 에 주입한다.
|
||||
*/
|
||||
@Slf4j
|
||||
@Configuration
|
||||
@RequiredArgsConstructor
|
||||
public class UserTypeConfigurer {
|
||||
|
||||
private final PortalProperties portalProperties;
|
||||
|
||||
@PostConstruct
|
||||
public void init() {
|
||||
UserTypeUtil.setInternalEmailDomains(portalProperties.getInternalUser().getEmailDomains());
|
||||
log.info("내부 사용자 이메일 도메인: {}", UserTypeUtil.getInternalEmailDomains());
|
||||
}
|
||||
}
|
||||
@@ -689,13 +689,17 @@ hr {
|
||||
font-display: swap;
|
||||
src: local("Spoqa Han Sans Bold"), url("/font/spoqa-hans/SpoqaHanSansBold.woff2") format("woff2"), url("/font/spoqa-hans/SpoqaHanSansBold.woff") format("woff"), url("/font/spoqa-hans/SpoqaHanSansBold.ttf") format("truetype");
|
||||
}
|
||||
/* Noto Sans KR (Variable) */
|
||||
/* Noto Sans KR (Variable)
|
||||
- 본문 3순위 폴백이므로 로컬 설치본을 최우선 사용한다.
|
||||
- 웹폰트는 한글 음절 전체(U+AC00–D7A3)·라틴·기호만 남긴 woff2 서브셋(약 1.1MB).
|
||||
원본 VariableFont TTF(9.9MB)는 전송 중 abort로 broken pipe 로그를 유발해 제거했다.
|
||||
서브셋 재생성은 docs/font-subset.md 참고. */
|
||||
@font-face {
|
||||
font-family: "Noto Sans KR";
|
||||
font-weight: 100 900;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
src: url("/font/noto-sans/NotoSansKR-VariableFont_wght.ttf") format("truetype");
|
||||
src: local("Noto Sans KR"), local("NotoSansKR"), local("Noto Sans CJK KR"), url("/font/noto-sans/NotoSansKR-subset.woff2") format("woff2");
|
||||
}
|
||||
:root {
|
||||
--primary-color: #0049b4;
|
||||
|
||||
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Binary file not shown.
Binary file not shown.
@@ -87,11 +87,18 @@
|
||||
url('/font/spoqa-hans/SpoqaHanSansBold.ttf') format('truetype');
|
||||
}
|
||||
|
||||
/* Noto Sans KR (Variable) */
|
||||
/* Noto Sans KR (Variable)
|
||||
- 본문 3순위 폴백이므로 로컬 설치본을 최우선 사용한다.
|
||||
- 웹폰트는 한글 음절 전체(U+AC00–D7A3)·라틴·기호만 남긴 woff2 서브셋(약 1.1MB).
|
||||
원본 VariableFont TTF(9.9MB)는 전송 중 abort로 broken pipe 로그를 유발해 제거했다.
|
||||
서브셋 재생성은 docs/font-subset.md 참고. */
|
||||
@font-face {
|
||||
font-family: 'Noto Sans KR';
|
||||
font-weight: 100 900;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
src: url('/font/noto-sans/NotoSansKR-VariableFont_wght.ttf') format('truetype');
|
||||
src: local('Noto Sans KR'),
|
||||
local('NotoSansKR'),
|
||||
local('Noto Sans CJK KR'),
|
||||
url('/font/noto-sans/NotoSansKR-subset.woff2') format('woff2');
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user