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;
|
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");
|
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-face {
|
||||||
font-family: "Noto Sans KR";
|
font-family: "Noto Sans KR";
|
||||||
font-weight: 100 900;
|
font-weight: 100 900;
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-display: swap;
|
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 {
|
:root {
|
||||||
--primary-color: #0049b4;
|
--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');
|
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-face {
|
||||||
font-family: 'Noto Sans KR';
|
font-family: 'Noto Sans KR';
|
||||||
font-weight: 100 900;
|
font-weight: 100 900;
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-display: swap;
|
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