Skip to content

配置讲解-全局公共配置,提升系统性能的基石

本文介绍的是大麦网中需要的公共配置包括哪些,以及工具类等其他的汇总说明,让小伙伴对整个项目结构更加的清晰

damai-common

依赖

xml
<dependency>
    <groupId>com.example</groupId>
    <artifactId>damai-common</artifactId>
    <version>${revision}</version>
</dependency>

下面会依次详细介绍此模块分别都有什么作用,由于本文篇幅有限,对于需要详细讲解的部分,有单独文档进行介绍,并都有跳转链接,阅读起来更加方便

ApiResponse  封装统一返回值

对于前后端分离的项目来说,后端接口返回的数据格式要进行规范化,通常是codemessagedata三个字段来组成。前端可通过code来判断接口执行是否成功,如果不成功,可以用message查看原因,如果成功,则获取data的数据。

使用**ApiResponse **就可以让返回的数据规范成同一个的格式,关于此部分的详细讲解,可跳转到相应文档

配置讲解-打造标准化数据返回结构

定制化JSON对象转换器

在前后端交过过程中,并不是后端直接写好接口后,把数据直接给了前端就完事了,除了上面说的要把数据格式进行统一规范外,还要需要考虑其他的问题,

比如long类型数据在传给前端会丢失精度、Date时间格式的定制、空值处理等问题。所以需要对json的序列化进行改造,而关于此部分的讲解,可跳转到相应文档

配置讲解-打造专属JSON转换器,解锁数据处理新姿势

Constant 全局常量

首先,我们来介绍一下为什么要使用常量,以及作用。在开发项目中,定义常量是一种普遍且推荐的做法。常量,顾名思义,是指在程序运行期间其值不会改变的特殊变量。使用常量不仅能提高代码的可读性和维护性,还有助于保证程序的稳定性和可靠性。

  1. 提高代码可读性:通过为常量赋予明确的名称,可以使代码更易于理解。例如,public static final String RESPONSE_OK = "OK"; 比起直接在代码中使用 "OK" 字符串,能更清晰地表达这个字符串的用途和意义。
  2. 便于维护:当一个值在多个地方使用时,如果将来需要修改这个值,只需在定义常量的地方修改一次即可,而不需要逐个查找并修改代码中的每一个实例。这极大地减少了修改引入错误的风险,并提高了维护效率。
  3. 避免魔法值:所谓魔法值,是指代码中直接使用的硬编码值,这些值对于阅读代码的人来说可能毫无意义。使用常量替代魔法值,可以让代码的意图更加明确,避免误解。
  4. 类型安全:使用常量可以提供类型安全,因为常量在定义时会明确其类型。这意味着如果错误地使用了不同类型的值,编译器就能立即指出,从而避免了运行时错误。
  5. 优化性能:虽然现代JVM优化已经非常高效,但在某些情况下,使用常量仍然可以提升性能。因为常量值在编译时就已经确定,这可以减少运行时的计算和内存分配。
  6. 支持配置的灵活性:在Spring Boot项目中,常量经常与配置文件搭配使用,以支持应用的配置外部化。这样,通过更改配置文件而不是硬编码值,可以轻松调整应用的行为,增强了项目的灵活性和可配置性。

多于微服务这种多个项目的架构特点来说,肯定存在全局都需要的值,比如说链路id,而这个常量就需要放在全局常量中

java
public class Constant {
    
    /**
     * 链路id
     * */
    public static final String TRACE_ID = "traceId";
    
    public static final String GRAY_FLAG_TRUE = "true";
    
    public static final String GRAY_FLAG_FALSE = "false";
    
    public static final String GRAY_PARAMETER = "gray";
    
    public static final String CODE = "code";
    
    public static final String USER_ID = "userId";
    
    public static final String JOB_INFO_ID = "jobInfoId";
    
    public static final String JOB_RUN_RECORD_ID = "jobRunRecordId";
    
    public static final String ALIPAY_NOTIFY_SUCCESS_RESULT = "success";
    
    public static final String ALIPAY_NOTIFY_FAILURE_RESULT = "failure";
    
    public static final String PREFIX_DISTINCTION_NAME = "prefix.distinction.name";
    
    public static final String DEFAULT_PREFIX_DISTINCTION_NAME = "damai";
    
    public static final String SPRING_INJECT_PREFIX_DISTINCTION_NAME = "${"+PREFIX_DISTINCTION_NAME+":"+DEFAULT_PREFIX_DISTINCTION_NAME+"}";
    
    public static final String SERVER_GRAY = "${spring.cloud.nacos.discovery.metadata.gray:false}";
    
}

唯一标识符

由于项目引用了第三方的中间件,比如Redis、Nacos、Kafka、Elasticsearch,在项目启动时,都要配置好连接这些的地址。

而对于刚接触开发的小伙伴来说,自己搭建这些属实是比较麻烦,如果没有掌握docker的话,更是费劲,可以说一步一个坎都不过分。本人自己已经搭建好这些中间件,而对于加入社区的小伙伴来说,可以直接连接这些,不用再自己搭建,

但有问题要考虑,就是大家一起使用的话,不能保证数据重复,比如Redis中的数据,Nacos中的数据、MQ中的数据、Elasticsearch中的数据,要保证每个人都操作自己独立的这一份数据。这就需要添加一个唯一标识来区分(默认为 damai)

可在配置文件中添加以下配置进行指定唯一标识符

yaml
prefix:
  distinction:
    # 默认就是damai
    name: damai

这样在这些中间件的数据前缀就会添加上你自己的标识了

SpringUtil

而唯一标识符的获取方式就是从SpringUtil中获取

java
public class SpringUtil implements ApplicationContextInitializer<ConfigurableApplicationContext> {
    
    private static ConfigurableApplicationContext configurableApplicationContext;
    
    
    public static String getPrefixDistinctionName(){
        return configurableApplicationContext.getEnvironment().getProperty(PREFIX_DISTINCTION_NAME,
                DEFAULT_PREFIX_DISTINCTION_NAME);
    }
    
    @Override
    public void initialize(final ConfigurableApplicationContext applicationContext) {
        configurableApplicationContext = applicationContext;
    }
}

SpringUtil 通过自动装配方式被 org.springframework.context.ApplicationContextInitializer 所指定,这样就能实现当Spring容器初始化的第一时间就能执行 initialize 方法,从而将applicationContext上下文,传递给SpringUtil 用于后续的使用中

而获取唯一标识符的 getPrefixDistinctionName 方法,就是从applicationContext上下文中的Environment 配置信息管理器中来获取 prefix.distinction.name 配置值的,如果获取不到,默认为 damai

enums包

此包下存放的都是枚举类型,包括基础错误参数配置BaseCode、以及业务状态参数,比如订单状态OrderStatus,证件类型IdType

SpringEnvironment

在Spring加载Bean对象时,有时需要对生成的对象进行替换,来加载指定加载额外的对象,比如:

java
@Bean("serviceLockInfoHandle")
public LockInfoHandle serviceLockInfoHandle(){
    return new RepeatExecuteLimitLockInfoHandle();
}

@Bean("serviceLockInfoHandle")
@Primary
public LockInfoHandle myServiceLockInfoHandle(){
    return new MyLockInfoHandle();
}

通过@Primary可以将MyLockInfoHandle的对象作为主要对象,但单纯这么做没有效果,还需要再添加配置,指定可以覆盖beanspring.main.allow-bean-definition-overriding = true

但这么配置还是有点问题,如果就在项目中的application.yml或者application.properties配置是没有问题的,但如果在Nacos配置中心进行配置,**就不会生效!**所以需要再代码中来修改

latex
org.springframework.boot.env.EnvironmentPostProcessor=\
  com.damai.environment.SpringEnvironment
java
public class SpringEnvironment implements EnvironmentPostProcessor {
    
    @Override
    public void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) {
        application.setAllowBeanDefinitionOverriding(true);
    }
}

exception包

此包存放的配置是用来统一拦截异常,并将异常信息进行日志打印,并将信息封装成统一的数据格式,返回给前端。关于此部分的详细讲解,可跳转到相关文档

配置讲解-打造异常处理的优雅之道

JWT

在Java项目中,JWT(JSON Web Token)已成为一种广泛使用的技术,用于在客户端和服务器之间安全地传输信息。它是一种开放标准(RFC 7519),旨在以紧凑且自包含的方式在双方之间安全地传输作为JSON对象编码的信息。

这些信息可以被验证和信任,因为它们是数字签名的。JWT的使用在现代Web应用和服务中尤其普遍,因为它提供了一种有效的方式来处理用户身份验证和授权。下面我们将深入探讨JWT在Java项目中的应用以及它带来的好处

JWT的结构

JWT通常由三个部分组成,它们之间用点(.)分隔:

  1. Header(头部):头部通常包含两部分信息:令牌的类型(即JWT)和所使用的签名算法,如HMAC SHA256或RSA
  2. Payload(有效载荷):载荷包含所要传递的信息,这些信息以称为声明(Claim)的形式表示。声明可以是关于实体(通常是用户)的以及其他任何数据。存在三种类型的声明:注册声明、公共声明和私有声明
  3. Signature(签名):为了创建签名部分,你必须拿头部的编码、有效载荷的编码、一个密钥,然后使用头部指定的算法进行签名

JWT的好处

  1. 自包含:JWT自包含了所有用户需要的信息,避免了多次数据库查询
  2. 紧凑:JWT的紧凑性使其在网络请求中传输成为可能,不会大幅增加请求负载
  3. 跨语言支持:JWT支持各种编程语言和环境,提高了系统的互操作性
  4. 安全:通过数字签名,JWT可以验证信息的来源和完整性。对于敏感数据,还可以对JWT进行加密
  5. 去中心化的授权:JWT不需要中央认证服务器,每个服务都可以独立验证Token

JWT在Java项目中的应用

在Java项目中,JWT常用于实现无状态认证机制。这意味着服务器不需要存储用户的登录信息或者会话状态,从而降低了系统的复杂性和扩展性限制。用户的状态在客户端保持,每次请求都会携带JWT,服务器通过验证JWT来识别用户身份和授权信息

大麦网中封装的jwt工具类:

java
@Slf4j
public class TokenUtil {
    
    /**
     * 指定签名的时候使用的签名算法,也就是header那部分。
     * 
     */
     private static final SignatureAlgorithm SIGNATURE_ALGORITHM = SignatureAlgorithm.HS256;
    /**
     * 用户登录成功后生成Jwt
     * 使用Hs256算法  私匙使用用户密码
     *
     * @param id        标识
     * @param info      登录成功的user对象
     * @param ttlMillis jwt过期时间
     * @param tokenSecret 私钥
     * @return
     */
    public static String createToken(String id, String info, long ttlMillis, String tokenSecret) {
        //生成JWT的时间
        long nowMillis = System.currentTimeMillis();
        
        //创建一个JwtBuilder,设置jwt的body
        JwtBuilder builder = Jwts.builder()
                //如果有私有声明,一定要先设置这个自己创建的私有的声明,这个是给builder的claim赋值,一旦写在标准的声明赋值之后,就是覆盖了那些标准的声明的
//                .setClaims(claims)
                //设置jti(JWT ID):是JWT的唯一标识,根据业务需要,这个可以设置为一个不重复的值,主要用来作为一次性token,从而回避重放攻击。
                .setId(id)
                //iat: jwt的签发时间
                .setIssuedAt(new Date(nowMillis))
                //代表这个JWT的主体,即它的所有人,这个是一个json格式的字符串。
                .setSubject(info)
                //设置签名使用的签名算法和签名使用的秘钥
                .signWith(SIGNATURE_ALGORITHM, tokenSecret);
        if (ttlMillis >= 0) {
            //设置过期时间
            builder.setExpiration(new Date(nowMillis + ttlMillis));
        }
        return builder.compact();
    }


    /**
     * Token的解密
     *
     * @param token 加密后的token
     * @param tokenSecret 私钥
     * @return
     */
    public static String parseToken(String token, String tokenSecret) {
        try {
            return Jwts.parser()
                    //设置签名的秘钥
                    .setSigningKey(tokenSecret)
                    //设置需要解析的jwt
                    .parseClaimsJws(token)
                    .getBody()
                    .getSubject();
        }catch (ExpiredJwtException jwtException) {
            log.error("parseToken error",jwtException);
            throw new DaMaiFrameException(BaseCode.TOKEN_EXPIRE);
        }
        
    }
    
    public static void main(String[] args) {
        
         String tokenSecret = "CSYZWECHAT";
        //生成token的实力
        JSONObject jsonObject = new JSONObject();
        jsonObject.put("001key", "001value");
        jsonObject.put("002key", "001value");
		
        String token1 = TokenUtil.createToken("1", jsonObject.toJSONString(), 10000, tokenSecret);
        System.out.println("token:" + token1);
        
        //解析token的示例
        String token2 = "eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiIxIiwiaWF0IjoxNjg4NTQyODM3LCJzdWIiOiJ7XCIwMDJrZXlcIjpcIjAwMXZhbHVlXCIsXCIwMDFrZXlcIjpcIjAwMXZhbHVlXCJ9IiwiZXhwIjoxNjg4NTQyODQ3fQ.vIKcAilTn_CR3VYssNE7rBpfuCSCH_RrkmsadLWf664";
        String subject = TokenUtil.parseToken(token2, tokenSecret);
        System.out.println("解析token后的值:" + subject);
    }
}

在大麦网中,jwt用于用户登录的业务逻辑中,关于用户登录的详细介绍,小伙伴可跳转到相关文档

业务讲解-用户登录和退出流程解析

BaseParameterHolder

ThreadLocal 是 Java 中一个非常重要的类,用于创建线程局部变量。它提供了一种将可变数据通过每个线程的局部变量的形式隔离开来的能力,从而实现了线程之间的数据隔离,确保每个线程都有自己独立的实例副本,互不干扰。这在处理多线程编程时特别有用,尤其是在需要避免对共享变量进行同步访问时

BaseParameterHolder是对ThreadLocal的封装,在使用上像Map一样,进行添加和查询键值对就可以,更加简单,并且配合定制化线程的使用,在BaseParameterHolder中的数据在 线程池组件 中都可以正常的获取

java
public class BaseParameterHolder {
    
    private static final ThreadLocal<Map<String, String>> THREAD_LOCAL_MAP = new ThreadLocal<>();
    
    
    public static void setParameter(String name, String value) {
        Map<String, String> map = THREAD_LOCAL_MAP.get();
        if (map == null) {
            map = new HashMap<>(64);
        }
        map.put(name, value);
        THREAD_LOCAL_MAP.set(map);
    }
    
    public static String getParameter(String name) {
        return Optional.ofNullable(THREAD_LOCAL_MAP.get()).map(map -> map.get(name)).orElse(null);
    }
    
    public static void removeParameter(String name) {
        Map<String, String> map = THREAD_LOCAL_MAP.get();
        if (map != null) {
            map.remove(name);
        }
    }
    
    public static ThreadLocal<Map<String, String>> getThreadLocal() {
        return THREAD_LOCAL_MAP;
    }
    
    public static Map<String, String> getParameterMap() {
        Map<String, String> map = THREAD_LOCAL_MAP.get();
        if (map == null) {
            map = new HashMap<>(64);
        }
        return map;
    }
    
    public static void setParameterMap(Map<String, String> map) {
        THREAD_LOCAL_MAP.set(map);
    }
    
    public static void removeParameterMap(){
        THREAD_LOCAL_MAP.remove();
    }
}

util包

此包的下为常用的工具类,包括AES加解密Base64的编解码时间和日期的操作Rsa签名Rsa加解密

更新: 2025-10-13 11:43:17
原文: https://www.yuque.com/u22210564/ykdrdh/hbz7d8t9ghwop2ct

Java 后端面试知识库