Springboot 整合JWT (token)+mybatis+自定义注解 实现简单的登录拦截模块

简介: Springboot 整合JWT (token)+mybatis+自定义注解 实现简单的登录拦截模块

这个实例的登录模块大概简单包含以下三个小功能:


用户注册


用户输入帐号密码,后台使用Spring Security的BCryptPasswordEncoder 进行密码加密,存库。


用户登录


用户输入帐号密码,后台查库使用Spring Security的BCryptPasswordEncoder进行密码校验,若登录成功,则返回JWT生成的token,带有过期时间。


token校验


用户访问其他接口,需要带着token访问,后台使用JWT token校验,错误或者过期则拦截,正常则继续访问。



那么接下来我们一起开始实现下。


项目最终目录结构:

image.png

首先准备个简单的数据库表 user_info:


CREATE TABLE `user_info`  (
  `UI_ID` int(11) NOT NULL AUTO_INCREMENT COMMENT '用户编号,主键自增',
  `UI_USER_NAME` varchar(30) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '用户名',
  `UI_PASSWORD` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '用户密码',
  `UI_STATUS` char(1) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT 'O' COMMENT 'O:正常,D:已删除',
  `UI_CREATE_TIME` bigint(12) NULL DEFAULT NULL COMMENT '用户创建时间',
  `UI_ROLES` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  PRIMARY KEY (`UI_ID`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 10 CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '用户信息表' ROW_FORMAT = Dynamic;


凑合用下,大概这个样子:


image.png


接着pom.xml用到的核心jar:


        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>2.0.0</version>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-jdbc</artifactId>
        </dependency>
        <!-- druid数据源驱动 1.1.10解决springboot从1.0——2.0版本问题-->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid-spring-boot-starter</artifactId>
            <version>1.1.10</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        <dependency>
            <groupId>com.auth0</groupId>
            <artifactId>java-jwt</artifactId>
            <version>3.4.0</version>
        </dependency>
        <!-- lombok -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.16.10</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>


然后是application.yml(里面的数据库连接url和driverClassName我使用了日志监控,你们不使用需要换一下):


如果想使用就导入依赖(使用这个东西的效果和教程可以参考我这篇,配合logback日志框架一起使用效果更佳  https://blog.csdn.net/qq_35387940/article/details/102563845):


        <!--监控sql日志-->
        <dependency>
            <groupId>org.bgee.log4jdbc-log4j2</groupId>
            <artifactId>log4jdbc-log4j2-jdbc4.1</artifactId>
            <version>1.16</version>
        </dependency>
#配置项目名称
spring:
 application:
   name: ElegantDemo
#数据库连接
 datasource:
  druid:
    username: root
    password: root
    url: jdbc:log4jdbc:mysql://localhost:3306/mylocal?useSSL=false&useUnicode=true&characterEncoding=UTF-8&serverTimezone=GMT%2B8&zeroDateTimeBehavior=convertToNull
    driverClassName: net.sf.log4jdbc.sql.jdbcapi.DriverSpy
    initialSize: 5
    minIdle: 5
    maxActive: 20
    maxWait: 60000
    timeBetweenEvictionRunsMillis: 60000
    minEvictableIdleTimeMillis: 300000
    validationQuery: SELECT 1 FROM DUAL
    testWhileIdle: true
    testOnBorrow: false
    testOnReturn: false
    poolPreparedStatements: true
    maxPoolPreparedStatementPerConnectionSize: 20
    useGlobalDataSourceStat: true
    connectionProperties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=5000
#配置端口
server:
  port: 8037
#单位 分钟
EXPIRE_TIME: 20


接着,我们先创建2个注解,分别是CheckToken 和 PassToken,用于更加灵活地标注哪些接口需要校验token,哪些不需要校验:


CheckToken.java


import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
 * @Author : JCccc
 * @CreateTime : 2019/11/27
 * @Description :
 **/
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface CheckToken {
    boolean required() default true;
}
/*
@Target:注解的作用目标
@Target(ElementType.TYPE)——接口、类、枚举、注解
@Target(ElementType.FIELD)——字段、枚举的常量
@Target(ElementType.METHOD)——方法
@Target(ElementType.PARAMETER)——方法参数
@Target(ElementType.CONSTRUCTOR) ——构造函数
@Target(ElementType.LOCAL_VARIABLE)——局部变量
@Target(ElementType.ANNOTATION_TYPE)——注解
@Target(ElementType.PACKAGE)——包*/
/*
@Retention:注解的保留位置
        RetentionPolicy.SOURCE:这种类型的Annotations只在源代码级别保留,编译时就会被忽略,在class字节码文件中不包含。
        RetentionPolicy.CLASS:这种类型的Annotations编译时被保留,默认的保留策略,在class文件中存在,但JVM将会忽略,运行时无法获得。
        RetentionPolicy.RUNTIME:这种类型的Annotations将被JVM保留,所以他们能在运行时被JVM或其他使用反射机制的代码所读取和使用。
@Document:说明该注解将被包含在javadoc中
@Inherited:说明子类可以继承父类中的该注解*/


PassToken.java


import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
 * @Author : JCccc
 * @CreateTime : 2019/11/27
 * @Description :
 **/
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface PassToken {
    boolean required() default true;
}
/*
@Target:注解的作用目标
@Target(ElementType.TYPE)——接口、类、枚举、注解
@Target(ElementType.FIELD)——字段、枚举的常量
@Target(ElementType.METHOD)——方法
@Target(ElementType.PARAMETER)——方法参数
@Target(ElementType.CONSTRUCTOR) ——构造函数
@Target(ElementType.LOCAL_VARIABLE)——局部变量
@Target(ElementType.ANNOTATION_TYPE)——注解
@Target(ElementType.PACKAGE)——包*/
/*
@Retention:注解的保留位置
        RetentionPolicy.SOURCE:这种类型的Annotations只在源代码级别保留,编译时就会被忽略,在class字节码文件中不包含。
        RetentionPolicy.CLASS:这种类型的Annotations编译时被保留,默认的保留策略,在class文件中存在,但JVM将会忽略,运行时无法获得。
        RetentionPolicy.RUNTIME:这种类型的Annotations将被JVM保留,所以他们能在运行时被JVM或其他使用反射机制的代码所读取和使用。
@Document:说明该注解将被包含在javadoc中
@Inherited:说明子类可以继承父类中的该注解*/


然后是弄个登录拦截器,这里主要用于校验token,AuthenticationInterceptor.java:


import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.exceptions.InvalidClaimException;
import com.auth0.jwt.exceptions.JWTDecodeException;
import com.auth0.jwt.exceptions.JWTVerificationException;
import com.auth0.jwt.exceptions.TokenExpiredException;
import com.demo.elegant.jwtToken.PassToken;
import com.demo.elegant.jwtToken.CheckToken;
import com.demo.elegant.pojo.User;
import com.demo.elegant.service.UserInfoService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.lang.reflect.Method;
/**
 * @Author : JCccc
 * @CreateTime : 2019/11/27
 * @Description :
 **/
public class AuthenticationInterceptor implements HandlerInterceptor {
    @Autowired
    UserInfoService userService;
    @Override
    public boolean preHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object object) throws Exception {
        String token = httpServletRequest.getHeader("token");// 从 http 请求头中取出 token
        // 如果不是映射到方法直接通过
        if (!(object instanceof HandlerMethod)) {
            return true;
        }
        HandlerMethod handlerMethod = (HandlerMethod) object;
        Method method = handlerMethod.getMethod();
        //检查是否有passToken注解,有则无需进行token校验
        if (method.isAnnotationPresent(PassToken.class)) {
            PassToken passToken = method.getAnnotation(PassToken.class);
            if (passToken.required()) {
                return true;
            }
        }
        //检查有没有CheckToken的注解
        if (method.isAnnotationPresent(CheckToken.class)) {
            CheckToken CheckToken = method.getAnnotation(CheckToken.class);
            if (CheckToken.required()) {
                // 执行认证
                if (token == null) {
                    throw new RuntimeException("无token,请重新登录");
                }
                // 获取 token 中的 user id
                String userId;
                try {
                    userId = JWT.decode(token).getAudience().get(0);
                } catch (JWTDecodeException j) {
                    throw new RuntimeException("您的token已坏掉了,请重新登录获取token");
                }
                User user = userService.getUserInfoById(Integer.valueOf(userId));
                if (user == null) {
                    throw new RuntimeException("用户不存在,请重新登录");
                }
                // 验证 token
                JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC256(user.getUI_PASSWORD())).build();
                try {
                    jwtVerifier.verify(token);
                }catch (InvalidClaimException e){
                    throw new RuntimeException("无效token,请重新登录获取token");
                }catch (TokenExpiredException e){
                    throw new RuntimeException("token已过期,请重新登录获取token");
                } catch (JWTVerificationException e) {
                    throw new RuntimeException(e.getMessage());
                }
                return true;
            }
        }
        return true;
    }
    @Override
    public void postHandle(HttpServletRequest httpServletRequest,
                           HttpServletResponse httpServletResponse,
                           Object o, ModelAndView modelAndView) throws Exception {
    }
    @Override
    public void afterCompletion(HttpServletRequest httpServletRequest,
                                HttpServletResponse httpServletResponse,
                                Object o, Exception e) throws Exception {
    }
}


拦截器手动配置类,InterceptorConfig.java:


import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
/**
 * @Author : JCccc
 * @CreateTime : 2019/11/27
 * @Description :
 **/
@Configuration
public class InterceptorConfig implements WebMvcConfigurer {
    @Bean
    public AuthenticationInterceptor authenticationInterceptor() {
        return new AuthenticationInterceptor();
    }
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(authenticationInterceptor())
                .addPathPatterns("/**");
    }
}


然后是普遍登录流程里需要用到的(包含token生成和解析方法,密码加密解密方法等等),


pojo :


User.java


import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
 * @Author : JCccc
 * @CreateTime : 2019/11/26
 * @Description :
 **/
@Data
@AllArgsConstructor
@NoArgsConstructor
public class User {
    private Integer UI_ID;
    private String UI_USER_NAME;
    private String UI_PASSWORD;
    private String UI_STATUS;
    private Long UI_CREATE_TIME;
    private String UI_ROLES;
}


mapper:


UserMapper.java


这里为了方便,我就采取mybatis注解方式去操作数据库了。


import com.demo.elegant.pojo.User;
import org.apache.ibatis.annotations.*;
/**
 * @Author : JCccc
 * @CreateTime : 2019/11/26
 * @Description :
 **/
@Mapper
public interface UserMapper {
    @Select("SELECT * FROM user_info WHERE UI_ID=#{userId}")
    User getUserInfoById(@Param("userId") Integer userId);
    @Select("SELECT * FROM user_info WHERE UI_USER_NAME=#{userName}")
    User getUserInfoByName(@Param("userName") String userName);
    @Insert("INSERT INTO user_info ( UI_USER_NAME, UI_PASSWORD, UI_STATUS,UI_CREATE_TIME, UI_ROLES )   VALUES ( #{UI_USER_NAME}, #{UI_PASSWORD},#{UI_STATUS},#{UI_CREATE_TIME},#{UI_ROLES}) ")
    @Options(useGeneratedKeys = true, keyProperty = "UI_ID")
    int addUser( User User);
}


service:


UserInfoService.java


import com.demo.elegant.pojo.User;
/**
 * @Author : JCccc
 * @CreateTime : 2019/11/26
 * @Description :
 **/
public interface UserInfoService {
    User getUserInfoById( Integer userId);
    User getUserInfoByName( String userName);
    int addUser( User User);
}


TokenService


import com.demo.elegant.pojo.User;
import java.util.Date;
/**
 * @Author : JCccc
 * @CreateTime : 2019/11/27
 * @Description :
 **/
public interface TokenService {
    public  String getToken(User user, Date date);
}


serviceImpl:


UserInfoServiceImpl.java


import com.demo.elegant.mapper.UserMapper;
import com.demo.elegant.pojo.User;
import com.demo.elegant.service.UserInfoService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
/**
 * @Author : JCccc
 * @CreateTime : 2019/11/26
 * @Description :
 **/
@Service
public class UserInfoServiceImpl implements UserInfoService {
    @Autowired
    UserMapper userMapper;
    @Override
    public User getUserInfoById(Integer userId) {
        return userMapper.getUserInfoById(userId);
    }
    @Override
    public User getUserInfoByName(String userName) {
        return userMapper.getUserInfoByName(userName);
    }
    @Override
    public int addUser(User User) {
        return userMapper.addUser(User);
    }
}


TokenServiceImpl.java


import com.auth0.jwt.JWT;
import com.auth0.jwt.algorithms.Algorithm;
import com.demo.elegant.pojo.User;
import com.demo.elegant.service.TokenService;
import org.springframework.stereotype.Service;
import java.util.Date;
/**
 * @Author : JCccc
 * @CreateTime : 2019/11/27
 * @Description :
 **/
@Service
public class TokenServiceImpl implements TokenService {
    @Override
    public String getToken(User user, Date date) {
        String token="";
        token= JWT.create()
                .withAudience(String.valueOf(user.getUI_ID()))  
                .withExpiresAt(date) //过期时间配置
                .sign(Algorithm.HMAC256(user.getUI_PASSWORD()));
        return token;
    }
}


最后是我们的登录接口,注册接口, UserInfoController.java:


import com.demo.elegant.jwtToken.PassToken;
import com.demo.elegant.jwtToken.CheckToken;
import com.demo.elegant.pojo.User;
import com.demo.elegant.service.TokenService;
import com.demo.elegant.service.UserInfoService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.web.bind.annotation.*;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
/**
 * @Author : JCccc
 * @CreateTime : 2019/11/26
 * @Description :
 **/
@RestController
@RequestMapping("/user")
public class UserInfoController {
    @Autowired
    UserInfoService userService;
    @Autowired
    TokenService tokenService;
    @Value("${EXPIRE_TIME}")
    private String EXPIRE_TIME;
    @CheckToken
    @GetMapping("/getUserByName/{userName}")
    public String getUser(@PathVariable("userName") String userName) {
        User userInfoByName = userService.getUserInfoByName(userName);
        return userInfoByName.toString();
    }
    //注册
    @PassToken
    @PostMapping("/register")
    public String register(@RequestBody  Map map) {
        BCryptPasswordEncoder bCryptPasswordEncoder=new BCryptPasswordEncoder();
        String encodePwd = bCryptPasswordEncoder.encode(String.valueOf(map.get("password")));
        User User=new User();
        User.setUI_USER_NAME(String.valueOf(map.get("username")));
        User.setUI_PASSWORD(encodePwd);
        User.setUI_STATUS("0");
        User.setUI_CREATE_TIME(System.currentTimeMillis());
        User.setUI_ROLES(String.valueOf(map.get("roles")));
        int i = userService.addUser(User);
        if (i==1){
            return "注册成功";
        }
        return "注册失败";
    }
    //登录
    @PostMapping("/login")
    public Map<String, Object> login(@RequestBody Map user){
        Map result=new HashMap();
        User userForBase=userService.getUserInfoByName(String.valueOf(user.get("username")));
        if(userForBase==null){
            result.put("message","登录失败,用户不存在");
            return result;
        }else {
            BCryptPasswordEncoder bCryptPasswordEncoder=new BCryptPasswordEncoder();
            String dbPwd=userForBase.getUI_PASSWORD();
            boolean matchesResult = bCryptPasswordEncoder.matches(String.valueOf(user.get("password")),dbPwd);
            if (!matchesResult){
                result.put("message","登录失败,密码错误");
                return result;
            }else {
                Date expiresDate = new Date(System.currentTimeMillis()+Integer.valueOf(EXPIRE_TIME)*60*1000);
                String token = tokenService.getToken(userForBase,expiresDate);
                result.put("token", token);
                result.put("expireTime", EXPIRE_TIME);
                result.put("userId", userForBase.getUI_ID());
                return result;
            }
        }
    }
    @CheckToken
    @GetMapping("/afterLogin")
    public String afterLogin(){
        return "你已通过验证,成功进入系统";
    }
}


PS: 注册接口里面所使用的密码加密与登录接口校验时使用的密码校验都是用的Spring Security的BCryptPasswordEncoder。


最后在启动类上,让Spring Security不要自动装载 ,毕竟这里只想用下BCryptPasswordEncoder。


@SpringBootApplication(exclude = {org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration.class})
public class ElegantApplication {
    public static void main(String[] args) {
        SpringApplication.run(ElegantApplication.class, args);
    }
}


最后就是简单的测试了,用postman先调用下注册接口,往数据库表里面添加下用户:


image.png


然后用这个帐号去调登录接口:


密码输错的时候,可以看到BCryptPasswordEncoder校验  出来了


image.png


那么输入正确的密码,可以看到登录成功后,token成功获取:


image.png


那么接下来带着token去访问其他接口,可以看到接口带的token是正常通过校验的:


image.png


在yml里面,咱们设置的过期时间值是20分钟,过期的时候,在拦截器里面也有相关的校验:


image.png


这些异常基本都已经被我提取出来了,检token过期的,检验token合理性的等等。


过期token返回示例:


image.png


错误token返回示例:


image.png


还有很多其他出错的情况,可以根据源码一个个提取,也可以直接不管,统一返回错误即可。


ok,这篇简单的整合jwt token登录注册模块教程就到此结束。

相关文章
|
12天前
|
缓存 监控 Java
SpringBoot @Scheduled 注解详解
使用`@Scheduled`注解实现方法周期性执行,支持固定间隔、延迟或Cron表达式触发,基于Spring Task,适用于日志清理、数据同步等定时任务场景。需启用`@EnableScheduling`,注意线程阻塞与分布式重复问题,推荐结合`@Async`异步处理,提升任务调度效率。
272 127
|
27天前
|
XML 安全 Java
使用 Spring 的 @Aspect 和 @Pointcut 注解简化面向方面的编程 (AOP)
面向方面编程(AOP)通过分离横切关注点,如日志、安全和事务,提升代码模块化与可维护性。Spring 提供了对 AOP 的强大支持,核心注解 `@Aspect` 和 `@Pointcut` 使得定义切面与切入点变得简洁直观。`@Aspect` 标记切面类,集中处理通用逻辑;`@Pointcut` 则通过表达式定义通知的应用位置,提高代码可读性与复用性。二者结合,使开发者能清晰划分业务逻辑与辅助功能,简化维护并提升系统灵活性。Spring AOP 借助代理机制实现运行时织入,与 Spring 容器无缝集成,支持依赖注入与声明式配置,是构建清晰、高内聚应用的理想选择。
271 0
|
1月前
|
Java 测试技术 API
将 Spring 的 @Embedded 和 @Embeddable 注解与 JPA 结合使用的指南
Spring的@Embedded和@Embeddable注解简化了JPA中复杂对象的管理,允许将对象直接嵌入实体,减少冗余表与连接操作,提升数据库设计效率。本文详解其用法、优势及适用场景。
214 126
|
2月前
|
XML JSON Java
Spring框架中常见注解的使用规则与最佳实践
本文介绍了Spring框架中常见注解的使用规则与最佳实践,重点对比了URL参数与表单参数的区别,并详细说明了@RequestParam、@PathVariable、@RequestBody等注解的应用场景。同时通过表格和案例分析,帮助开发者正确选择参数绑定方式,避免常见误区,提升代码的可读性与安全性。
|
13天前
|
XML Java 数据格式
常用SpringBoot注解汇总与用法说明
这些注解的使用和组合是Spring Boot快速开发和微服务实现的基础,通过它们,可以有效地指导Spring容器进行类发现、自动装配、配置、代理和管理等核心功能。开发者应当根据项目实际需求,运用这些注解来优化代码结构和服务逻辑。
111 12
|
26天前
|
Java 测试技术 数据库
使用Spring的@Retryable注解进行自动重试
在现代软件开发中,容错性和弹性至关重要。Spring框架提供的`@Retryable`注解为处理瞬时故障提供了一种声明式、可配置的重试机制,使开发者能够以简洁的方式增强应用的自我恢复能力。本文深入解析了`@Retryable`的使用方法及其参数配置,并结合`@Recover`实现失败回退策略,帮助构建更健壮、可靠的应用程序。
106 1
使用Spring的@Retryable注解进行自动重试
|
26天前
|
传感器 Java 数据库
探索Spring Boot的@Conditional注解的上下文配置
Spring Boot 的 `@Conditional` 注解可根据不同条件动态控制 Bean 的加载,提升应用的灵活性与可配置性。本文深入解析其用法与优势,并结合实例展示如何通过自定义条件类实现环境适配的智能配置。
探索Spring Boot的@Conditional注解的上下文配置
|
26天前
|
智能设计 Java 测试技术
Spring中最大化@Lazy注解,实现资源高效利用
本文深入探讨了 Spring 框架中的 `@Lazy` 注解,介绍了其在资源管理和性能优化中的作用。通过延迟初始化 Bean,`@Lazy` 可显著提升应用启动速度,合理利用系统资源,并增强对 Bean 生命周期的控制。文章还分析了 `@Lazy` 的工作机制、使用场景、最佳实践以及常见陷阱与解决方案,帮助开发者更高效地构建可扩展、高性能的 Spring 应用程序。
Spring中最大化@Lazy注解,实现资源高效利用
|
27天前
|
安全 IDE Java
Spring 的@FieldDefaults和@Data:Lombok 注解以实现更简洁的代码
本文介绍了如何在 Spring 应用程序中使用 Project Lombok 的 `@Data` 和 `@FieldDefaults` 注解来减少样板代码,提升代码可读性和可维护性,并探讨了其适用场景与限制。
Spring 的@FieldDefaults和@Data:Lombok 注解以实现更简洁的代码