From e0e794241217d8aa03491836ba4e2aa1c7f93b15 Mon Sep 17 00:00:00 2001 From: Doghole Date: Thu, 15 May 2025 01:39:54 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E6=96=B9=E6=B3=95=E7=BA=A7?= =?UTF-8?q?=20Caller=20=E9=94=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../emoney/component/CallerLockAspect.java | 54 ++++++++ .../rich/emoney/component/LockByCaller.java | 20 +++ .../rich/emoney/config/EmoneyAutoConfig.java | 2 + .../emoney/controller/IndexControllerV1.java | 59 ++++++-- .../config/ProxyConfigControllerV1.java | 28 ++++ .../emoney/entity/config/PlatformConfig.java | 2 + .../emoney/entity/config/ProxyConfig.java | 12 -- .../rich/emoney/patch/okhttp/PatchOkHttp.java | 1 + .../quant/rich/emoney/pojo/dto/IpInfo.java | 70 ++++++++++ .../rich/emoney/util/CallerLockUtil.java | 79 +++++++++++ .../quant/rich/emoney/util/GeoIPUtil.java | 127 ++++++++++++++++++ .../META-INF/spring-devtools.properties | 1 + 12 files changed, 430 insertions(+), 25 deletions(-) create mode 100644 src/main/java/quant/rich/emoney/component/CallerLockAspect.java create mode 100644 src/main/java/quant/rich/emoney/component/LockByCaller.java create mode 100644 src/main/java/quant/rich/emoney/controller/config/ProxyConfigControllerV1.java create mode 100644 src/main/java/quant/rich/emoney/pojo/dto/IpInfo.java create mode 100644 src/main/java/quant/rich/emoney/util/CallerLockUtil.java create mode 100644 src/main/java/quant/rich/emoney/util/GeoIPUtil.java create mode 100644 src/main/resources/META-INF/spring-devtools.properties diff --git a/src/main/java/quant/rich/emoney/component/CallerLockAspect.java b/src/main/java/quant/rich/emoney/component/CallerLockAspect.java new file mode 100644 index 0000000..6b4ecce --- /dev/null +++ b/src/main/java/quant/rich/emoney/component/CallerLockAspect.java @@ -0,0 +1,54 @@ +package quant.rich.emoney.component; + +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.*; +import org.aspectj.lang.reflect.MethodSignature; +import org.springframework.expression.*; +import org.springframework.expression.spel.standard.SpelExpressionParser; +import org.springframework.expression.spel.support.StandardEvaluationContext; +import org.springframework.stereotype.Component; + +import quant.rich.emoney.util.CallerLockUtil; + +import java.lang.reflect.Method; +import java.util.concurrent.locks.ReentrantLock; + +@Aspect +@Component +public class CallerLockAspect { + + private final SpelExpressionParser parser = new SpelExpressionParser(); + + @Around("@annotation(com.example.lock.LockByCaller)") + public Object around(ProceedingJoinPoint pjp) throws Throwable { + MethodSignature signature = (MethodSignature) pjp.getSignature(); + Method method = signature.getMethod(); + LockByCaller annotation = method.getAnnotation(LockByCaller.class); + + // 获取 SpEL key(如果有) + Object[] args = pjp.getArgs(); + String[] paramNames = signature.getParameterNames(); + Object[] extraKey = new Object[0]; + + if (!annotation.key().isEmpty()) { + EvaluationContext context = new StandardEvaluationContext(); + for (int i = 0; i < paramNames.length; i++) { + context.setVariable(paramNames[i], args[i]); + } + + Expression expression = parser.parseExpression(annotation.key()); + Object value = expression.getValue(context); + extraKey = (value == null) ? new Object[0] : new Object[]{value}; + } + + // 复用工具类的锁逻辑 + ReentrantLock lock = CallerLockUtil.acquireLock(extraKey); + + lock.lock(); + try { + return pjp.proceed(); + } finally { + lock.unlock(); + } + } +} \ No newline at end of file diff --git a/src/main/java/quant/rich/emoney/component/LockByCaller.java b/src/main/java/quant/rich/emoney/component/LockByCaller.java new file mode 100644 index 0000000..9a135f8 --- /dev/null +++ b/src/main/java/quant/rich/emoney/component/LockByCaller.java @@ -0,0 +1,20 @@ +package quant.rich.emoney.component; + +import java.lang.annotation.*; + +/** + * 在方法上添加此注解,可针对调用方加锁,即:
+ * 调用方法为 A 的,多次从 A 调用则加锁,从 B 调用时不受影响
+ * 需要开启 AOP:在任意配置类上增加注解:@EnableAspectJAutoProxy + * @see CallerLockAspect + */ +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface LockByCaller { + /** + * 可选参数,用于 SpEL 表达式获取 key + * 例如:@LockByCaller(key = "#userId") + */ + String key() default ""; +} \ No newline at end of file diff --git a/src/main/java/quant/rich/emoney/config/EmoneyAutoConfig.java b/src/main/java/quant/rich/emoney/config/EmoneyAutoConfig.java index 70e4c29..7439b7f 100644 --- a/src/main/java/quant/rich/emoney/config/EmoneyAutoConfig.java +++ b/src/main/java/quant/rich/emoney/config/EmoneyAutoConfig.java @@ -2,6 +2,7 @@ package quant.rich.emoney.config; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.EnableAspectJAutoProxy; import org.springframework.context.annotation.Import; import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; @@ -17,6 +18,7 @@ import quant.rich.emoney.service.ConfigService; * @author Doghole * */ +@EnableAspectJAutoProxy @Configuration @Import(ConfigAutoRegistrar.class) public class EmoneyAutoConfig implements WebMvcConfigurer { diff --git a/src/main/java/quant/rich/emoney/controller/IndexControllerV1.java b/src/main/java/quant/rich/emoney/controller/IndexControllerV1.java index c40357b..f672ff3 100644 --- a/src/main/java/quant/rich/emoney/controller/IndexControllerV1.java +++ b/src/main/java/quant/rich/emoney/controller/IndexControllerV1.java @@ -8,11 +8,16 @@ import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.ResponseBody; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; + import quant.rich.emoney.controller.common.BaseController; import quant.rich.emoney.entity.config.PlatformConfig; import quant.rich.emoney.exception.RException; import quant.rich.emoney.pojo.dto.R; +import quant.rich.emoney.service.AuthService; import quant.rich.emoney.service.ConfigService; +import quant.rich.emoney.util.EncryptUtils; @Controller @RequestMapping("/admin/v1") @@ -23,29 +28,57 @@ public class IndexControllerV1 extends BaseController { @Autowired ConfigService configService; + + @Autowired + AuthService authService; @GetMapping({ "", "/", "/index" }) public String index() { return "admin/v1/index"; } + @GetMapping("/getUserInfo") + @ResponseBody + public R getUserInfo() { + ObjectNode node = new ObjectMapper().valueToTree(platformConfig); + node.remove("password"); + node.remove("isInited"); + return R.ok(node); + } + @PostMapping("/changeUserInfo") @ResponseBody - public R changeUserInfo(String newUsername, String oldPassword, String newPassword) { - if (!platformConfig.getPassword().equals(oldPassword)) { - throw RException.badRequest("密码错误"); - } - if (StringUtils.isAllEmpty(newUsername, newPassword)) { - throw RException.badRequest("未更改任何信息"); - } - if (StringUtils.isNotEmpty(newUsername)) { - platformConfig.setUsername(newUsername); - } - if (StringUtils.isNotEmpty(newPassword)) { + public R changeUserInfo( + String username, + String password, + String newPassword, + String email) { + + if (passwordIsNotEmpty(newPassword)) { + if (!platformConfig.getPassword().equals(password)) { + throw RException.badRequest("密码错误"); + } platformConfig.setPassword(newPassword); } - configService.saveOrUpdate(platformConfig); - return R.ok(); + if (StringUtils.isNotEmpty(username)) { + platformConfig.setUsername(username); + } + else { + throw RException.badRequest("用户名不能为空"); + } + platformConfig.setEmail(email); + return R.judge(() -> { + if (configService.saveOrUpdate(platformConfig)) { + authService.setLogin(username, platformConfig.getPassword()); + return true; + } + return false; + }); } + static final String EMPTY_PASSWORD = EncryptUtils.sha3("", 224); + + static boolean passwordIsNotEmpty(String password) { + return StringUtils.isNotEmpty(password) && !password.equalsIgnoreCase(EMPTY_PASSWORD); + } } diff --git a/src/main/java/quant/rich/emoney/controller/config/ProxyConfigControllerV1.java b/src/main/java/quant/rich/emoney/controller/config/ProxyConfigControllerV1.java new file mode 100644 index 0000000..a573dc2 --- /dev/null +++ b/src/main/java/quant/rich/emoney/controller/config/ProxyConfigControllerV1.java @@ -0,0 +1,28 @@ +package quant.rich.emoney.controller.config; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.ResponseBody; + +import lombok.extern.slf4j.Slf4j; +import quant.rich.emoney.controller.common.BaseController; +import quant.rich.emoney.entity.config.ProxyConfig; +import quant.rich.emoney.pojo.dto.R; + +@Slf4j +@Controller +@RequestMapping("/admin/v1/config/proxy") +public class ProxyConfigControllerV1 extends BaseController { + + @Autowired + ProxyConfig proxyConfig; + + + @GetMapping("/refreshIpThroughProxy") + @ResponseBody + public R refreshIpThroughProxy() { + return R.ok(proxyConfig.refreshIpThroughProxy()); + } +} diff --git a/src/main/java/quant/rich/emoney/entity/config/PlatformConfig.java b/src/main/java/quant/rich/emoney/entity/config/PlatformConfig.java index bad2bfc..3dc8238 100644 --- a/src/main/java/quant/rich/emoney/entity/config/PlatformConfig.java +++ b/src/main/java/quant/rich/emoney/entity/config/PlatformConfig.java @@ -13,6 +13,8 @@ public class PlatformConfig implements IConfig { private String username; private String password; + + private String email; private Boolean isInited; diff --git a/src/main/java/quant/rich/emoney/entity/config/ProxyConfig.java b/src/main/java/quant/rich/emoney/entity/config/ProxyConfig.java index 1ffa053..06fe540 100644 --- a/src/main/java/quant/rich/emoney/entity/config/ProxyConfig.java +++ b/src/main/java/quant/rich/emoney/entity/config/ProxyConfig.java @@ -1,27 +1,15 @@ package quant.rich.emoney.entity.config; -import java.io.IOException; import java.net.InetSocketAddress; import java.net.Proxy; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.locks.ReentrantLock; - import com.fasterxml.jackson.annotation.JsonView; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.node.ObjectNode; - import lombok.Data; import lombok.experimental.Accessors; import lombok.extern.slf4j.Slf4j; -import okhttp3.OkHttpClient; -import okhttp3.Request; -import okhttp3.Response; -import quant.rich.emoney.client.OkHttpClientProvider; import quant.rich.emoney.interceptor.EnumOptionsInterceptor.EnumOptions; import quant.rich.emoney.interfaces.ConfigInfo; import quant.rich.emoney.interfaces.IConfig; import quant.rich.emoney.pojo.dto.IpInfo; -import quant.rich.emoney.util.CallerLockUtil; import quant.rich.emoney.util.GeoIPUtil; import quant.rich.emoney.validator.ProxyConfigValid; diff --git a/src/main/java/quant/rich/emoney/patch/okhttp/PatchOkHttp.java b/src/main/java/quant/rich/emoney/patch/okhttp/PatchOkHttp.java index d35b71a..305e15d 100644 --- a/src/main/java/quant/rich/emoney/patch/okhttp/PatchOkHttp.java +++ b/src/main/java/quant/rich/emoney/patch/okhttp/PatchOkHttp.java @@ -73,6 +73,7 @@ public class PatchOkHttp { *
  • 在方法体内,使用外部 RestartClassLoader 载入欲重定义的方法类,并在调用 ByteBuddy 时指定该方法类和 ClassLoader。
  • * *

    + *

    进一步地,如果注入很重要,请判断欲重定义类所属 ClassLoader 和外部 ClassLoader 是否一致,如果不一致的,终止程序。

    * @see org.springframework.boot.devtools.restart.classloader.RestartClassLoader * @see jdk.internal.loader.ClassLoaders */ diff --git a/src/main/java/quant/rich/emoney/pojo/dto/IpInfo.java b/src/main/java/quant/rich/emoney/pojo/dto/IpInfo.java new file mode 100644 index 0000000..a63761c --- /dev/null +++ b/src/main/java/quant/rich/emoney/pojo/dto/IpInfo.java @@ -0,0 +1,70 @@ +package quant.rich.emoney.pojo.dto; + +import java.util.Objects; + +import org.apache.commons.lang3.StringUtils; + +import lombok.Data; +import lombok.experimental.Accessors; + +@Data +@Accessors(chain=true) +public class IpInfo { + private String ip = "Unknown"; + private String ipv6; + private String country; + private String subdivision; + private String city; + + public static final IpInfo EMPTY = new IpInfo(); + + public IpInfo setIp(String ip) { + if (ip != null) this.ip = ip.trim(); + return this; + } + + public IpInfo setIpv6(String ipv6) { + if (ipv6 != null) this.ipv6 = ipv6.trim(); + return this; + } + + public String toString() { + + StringBuilder sb = new StringBuilder(); + sb.append("IP: ").append(getIp()); + if (StringUtils.isNoneBlank(getIpv6())) { + sb.append(", ").append("IPv6: ").append(getIpv6()); + } + sb.append(getGeoString()); + return sb.toString(); + } + + public String getString() { + return toString(); + } + + public String getGeoString() { + StringBuilder sb = new StringBuilder(); + sb.append("("); + if (StringUtils.isNotBlank(getCountry())) { + // 国家 + sb.append(getCountry()); + if (StringUtils.isNotBlank(getSubdivision())) { + sb.append(getSubdivision()); + } + if (StringUtils.isNotBlank(getCity()) && !Objects.equals(getCountry(), getCity())) { + // 有时候国家和地区与城市一致的,忽略 + sb.append(getCity()); + } + } + else { + sb.append("未知位置"); + } + sb.append(")"); + return sb.toString(); + } + + public boolean isEmpty() { + return StringUtils.isBlank(getIp()); + } +} diff --git a/src/main/java/quant/rich/emoney/util/CallerLockUtil.java b/src/main/java/quant/rich/emoney/util/CallerLockUtil.java new file mode 100644 index 0000000..20ff425 --- /dev/null +++ b/src/main/java/quant/rich/emoney/util/CallerLockUtil.java @@ -0,0 +1,79 @@ +package quant.rich.emoney.util; + + +import java.lang.StackWalker; +import java.lang.ref.WeakReference; +import java.util.Arrays; +import java.util.Objects; +import java.util.concurrent.*; +import java.util.concurrent.locks.ReentrantLock; + +public class CallerLockUtil { + + private static final StackWalker walker = StackWalker.getInstance(StackWalker.Option.RETAIN_CLASS_REFERENCE); + private static final ConcurrentMap> lockMap = new ConcurrentHashMap<>(); + + /** + * ✅ 方式一:一键加锁执行 + */ + public static void runWithCallerLock(Runnable task, Object... extraKeys) { + ReentrantLock lock = acquireLock(extraKeys); + lock.lock(); + try { + task.run(); + } finally { + lock.unlock(); + } + } + + /** + * ✅ 方式二:手动获取锁 + * 用于:lock() + try/finally + unlock() + */ + public static ReentrantLock acquireLock(Object... extraKeys) { + LockKey key = buildLockKey(extraKeys); + return lockMap.compute(key, (k, ref) -> { + ReentrantLock l = (ref == null || ref.get() == null) ? new ReentrantLock() : ref.get(); + return new WeakReference<>(l); + }).get(); + } + + /** + * 构造调用者方法 + 附加参数为 key + */ + private static LockKey buildLockKey(Object... extraKeys) { + String caller = walker.walk(frames -> + frames.skip(3).findFirst() // skip getStackTrace → buildLockKey → acquireLock → 调用者 + .map(f -> f.getClassName() + "#" + f.getMethodName()) + .orElse("unknown") + ); + return new LockKey(caller, extraKeys); + } + + /** + * 复合 Key 类(调用方法 + 参数) + */ + private static class LockKey { + private final String method; + private final Object[] extra; + private final int hash; + + LockKey(String method, Object[] extra) { + this.method = method; + this.extra = (extra == null) ? new Object[0] : extra; + this.hash = Objects.hash(method, Arrays.deepHashCode(this.extra)); + } + + @Override + public boolean equals(Object obj) { + if (!(obj instanceof LockKey)) return false; + LockKey other = (LockKey) obj; + return method.equals(other.method) && Arrays.deepEquals(this.extra, other.extra); + } + + @Override + public int hashCode() { + return hash; + } + } +} \ No newline at end of file diff --git a/src/main/java/quant/rich/emoney/util/GeoIPUtil.java b/src/main/java/quant/rich/emoney/util/GeoIPUtil.java new file mode 100644 index 0000000..dc1b31a --- /dev/null +++ b/src/main/java/quant/rich/emoney/util/GeoIPUtil.java @@ -0,0 +1,127 @@ +package quant.rich.emoney.util; + +import com.maxmind.geoip2.DatabaseReader; +import com.maxmind.geoip2.exception.GeoIp2Exception; +import com.maxmind.geoip2.model.CityResponse; +import com.maxmind.geoip2.record.Subdivision; + +import lombok.extern.slf4j.Slf4j; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.Response; +import quant.rich.emoney.client.OkHttpClientProvider; +import quant.rich.emoney.entity.config.ProxyConfig; +import quant.rich.emoney.pojo.dto.IpInfo; + +import java.io.File; +import java.io.IOException; +import java.net.InetAddress; +import java.net.Proxy; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.locks.ReentrantLock; + +@Slf4j +public class GeoIPUtil { + + private static DatabaseReader cityReader; + public static String LOCALE = "zh-CN"; + + static { + try { + cityReader = new DatabaseReader.Builder(new File("./conf/extra/GeoLite2-City.mmdb")).build(); + } catch (IOException e) { + throw new RuntimeException("IP 地址库初始化失败", e); + } + } + + public static IpInfo getIpInfoThroughProxy(ProxyConfig proxyConfig) { + ReentrantLock lock = CallerLockUtil.acquireLock(); + lock.lock(); + try { + Proxy proxy = proxyConfig.getProxy(); + boolean ignoreHttpsVerification = proxyConfig.getIgnoreHttpsVerification(); + // OkHttp 客户端配置 + OkHttpClient client = OkHttpClientProvider.getInstance( + proxy, ignoreHttpsVerification, + builder -> builder + .connectTimeout(10, TimeUnit.SECONDS) + .readTimeout(10, TimeUnit.SECONDS)); + + // 使用 httpbin.org/ip 获取当前请求的公网 IP + Request requestIpv4 = new Request.Builder() + .url("https://ipv4.icanhazip.com") + .build(); + + IpInfo ipInfo = new IpInfo(); + + try (Response response = client.newCall(requestIpv4).execute()) { + if (!response.isSuccessful()) { + log.warn("Request ipv4 failed with code: {}", response.code()); + return IpInfo.EMPTY; + } + + String responseBody = response.body().string(); + log.debug("Response ipv4 from proxy: {}", responseBody.trim()); + ipInfo.setIp(responseBody); + } catch (IOException e) { + log.warn("Proxy ipv4 error: {}", e.getMessage()); + return IpInfo.EMPTY; + } + Request requestIpv6 = new Request.Builder() + .url("https://ipv6.icanhazip.com") + .build(); + try (Response response = client.newCall(requestIpv6).execute()) { + if (!response.isSuccessful()) { + log.warn("Request ipv6 failed with code: {}", response.code()); + } + + String responseBody = response.body().string(); + log.debug("Response ipv6 from proxy: {}", responseBody.trim()); + ipInfo.setIpv6(responseBody); + } catch (IOException e) { + log.warn("Proxy ipv6 error {}", e.getMessage()); + } + return queryIpInfoGeoLite(ipInfo); + } + finally { + lock.unlock(); + } + } + + /** + * 以 IPv4 为准 + * @param ipInfo + */ + public static IpInfo queryIpInfoGeoLite(IpInfo ipInfo) { + try { + InetAddress ipv4Address = InetAddress.getByName(ipInfo.getIp()); + CityResponse response = cityReader.city(ipv4Address); + + if (response.getCountry() != null) { + String countryStr = response.getCountry().getNames().get(LOCALE); + ipInfo.setCountry(countryStr); + } + + if (response.getSubdivisions() != null && !response.getSubdivisions().isEmpty()) { + // 省 + Subdivision subdivision = response.getSubdivisions().get(0); + if (subdivision != null) { + String subdivisionStr = subdivision.getNames().get(LOCALE); + ipInfo.setSubdivision(subdivisionStr); + } + } + if (response.getCity() != null) { + // 市 + String cityStr = response.getCity().getNames().get(LOCALE); + ipInfo.setCity(cityStr); + } + } catch (IOException | GeoIp2Exception e) { + log.warn("Get city response from GeoLite2-City database failed: {}", e.getMessage()); + } + return ipInfo; + } + + public static void main(String[] args) { + System.out.format("GeoLite2-City %s\r\n", queryIpInfoGeoLite(new IpInfo().setIp("46.3.98.216"))); + } +} \ No newline at end of file diff --git a/src/main/resources/META-INF/spring-devtools.properties b/src/main/resources/META-INF/spring-devtools.properties new file mode 100644 index 0000000..582a033 --- /dev/null +++ b/src/main/resources/META-INF/spring-devtools.properties @@ -0,0 +1 @@ +restart.include.okhttp3=.*okhttp-.*\.jar \ No newline at end of file