1. 什么是 JWT？JWT（JSON Web Token）是一种用于在不同应用之间安全传输信息的开放标准（RFC 7519）。它是一种基于 JSON 的轻量级令牌，由三部分组成：头部（Header）、载荷（Payload）和签名（Signature）。JWT 被广泛用于实现身份验证和授权，特别适用于前后端分离的应用程序。令牌类似下面这一大长串：eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJxdWFueGlhb2hhIiwiaXNzIjoicXVhbnhpYW9oYSIsImlhdCI6MTY5Mjk1OTY2MSwiZXhwIjoxNjkyOTYzMjYxfQ.wbqbn23C9vAe5sQZRCBzrIM4SiN1eNl55NIONmHoiPHPHSSu0QJGgPGUin80hA4XgMHEqN1Wm5KJlmKKucUyGQ可以看到，由 header.payload.signature 三部分组成，你可以在此网站: https://jwt.io/ 上获得解析结果：2. 为什么要使用 JWT？JWT 提供了一种在客户端和服务器之间传输安全信息的简单方法，具有以下优点：无状态性（Stateless）：JWT 本身包含了所有必要的信息，无需在服务器端存储会话信息，每个请求都可以独立验证。灵活性：JWT 可以存储任意格式的数据，使其成为传递用户信息、权限、角色等的理想选择。安全性：JWT 使用签名进行验证，确保信息在传输过程中不被篡改。跨平台和跨语言：由于 JWT 使用 JSON 格式，它在不同的编程语言和平台之间都可以轻松传递。3. 开始动手3.1 添加 JWT 依赖这里我们选择 Java JWT : JSON Web Token for Java and Android (简称 JJWT) 库。首先，在父模块中的 pom.xml 中声明版本号： <properties> // 省略... <jjwt.version>0.11.2</jjwt.version> </properties> <dependencyManagement> <dependencies> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt-api</artifactId> <version>${jjwt.version}</version> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt-impl</artifactId> <version>${jjwt.version}</version> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt-jackson</artifactId> <version>${jjwt.version}</version> </dependency> </dependencies> </dependencyManagement> 然后，在jwt模块中的 pom.xml 文件中，引入该依赖，添加内容如下： <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt-api</artifactId> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt-impl</artifactId> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt-jackson</artifactId> </dependency>3.2 JwtTokenHelper 工具类@Component public class JwtTokenHelper implements InitializingBean { /** * 签发人 */ @Value("${jwt.issuer}") private String issuer; /** * 秘钥 */ private Key key; /** * JWT 解析 */ private JwtParser jwtParser; /** * 解码配置文件中配置的 Base 64 编码 key 为秘钥 * @param base64Key */ @Value("${jwt.secret}") public void setBase64Key(String base64Key) { key = Keys.hmacShaKeyFor(Base64.getDecoder().decode(base64Key)); } /** * 初始化 JwtParser * @throws Exception */ @Override public void afterPropertiesSet() throws Exception { // 考虑到不同服务器之间可能存在时钟偏移，setAllowedClockSkewSeconds 用于设置能够容忍的最大的时钟误差 jwtParser = Jwts.parserBuilder().requireIssuer(issuer) .setSigningKey(key).setAllowedClockSkewSeconds(10) .build(); } /** * 生成 Token * @param username * @return */ public String generateToken(String username) { LocalDateTime now = LocalDateTime.now(); // Token 一个小时后失效 LocalDateTime expireTime = now.plusHours(1); return Jwts.builder().setSubject(username) .setIssuer(issuer) .setIssuedAt(Date.from(now.atZone(ZoneId.systemDefault()).toInstant())) .setExpiration(Date.from(expireTime.atZone(ZoneId.systemDefault()).toInstant())) .signWith(key) .compact(); } /** * 解析 Token * @param token * @return */ public Jws<Claims> parseToken(String token) { try { return jwtParser.parseClaimsJws(token); } catch (SignatureException | MalformedJwtException | UnsupportedJwtException | IllegalArgumentException e) { throw new BadCredentialsException("Token 不可用", e); } catch (ExpiredJwtException e) { throw new CredentialsExpiredException("Token 失效", e); } } /** * 生成一个 Base64 的安全秘钥 * @return */ private static String generateBase64Key() { // 生成安全秘钥 Key secretKey = Keys.secretKeyFor(SignatureAlgorithm.HS512); // 将密钥进行 Base64 编码 String base64Key = Base64.getEncoder().encodeToString(secretKey.getEncoded()); return base64Key; } public static void main(String[] args) { String key = generateBase64Key(); System.out.println("key: " + key); } } 上述代码中，Token 令牌的初始化工作在 generateToken() 方法中完成，主要是通过 Jwts.builder 返回的 JwtBuilder 来做的。令牌的解析工作交给了 JwtParser 类，在 parseToken() 方法中完成。与之对应的，工具类中注入的一些参数，如 jwt 的签发人、秘钥，需要在 applicaiton.yml 中配置好：jwt: # 签发人 issuer: chengzi # 秘钥 secret: jElxcSUj38+Bnh73T68lNs0DfBSit6U3whQlcGO2XwnI+Bo3g4xsiCIPg8PV/L0fQMis08iupNwhe2PzYLB9Xg== 3.3 如何生成安全的秘钥？在 JwtTokenHelper 中，已经定义好了一个 generateBase64Key() 方法，它专门用于生成一个 Base64 的安全秘钥，执行 main() 方法即可，然后将生成好的秘钥配置到 yml 文件中。3.4 PasswordEncoder 密码加密在系统中，安全存储用户密码是至关重要的。使用明文存储密码容易受到攻击，相信大家都看过某些网站用户账户被黑，密码都是明文保存的新闻，因此使用密码加密技术来保护用户密码是必不可少的。在 jwt 模块中新建 config 包，并创建 PasswordEncoderConfig 配置类，代码如下：@Component public class PasswordEncoderConfig { @Bean public PasswordEncoder passwordEncoder() { // BCrypt 是一种安全且适合密码存储的哈希算法，它在进行哈希时会自动加入“盐”，增加密码的安全性。 return new BCryptPasswordEncoder(); } public static void main(String[] args) { BCryptPasswordEncoder encoder = new BCryptPasswordEncoder(); System.out.println(encoder.encode("chengzi")); } } PasswordEncoder 接口是 Spring Security 提供的密码加密接口，它定义了密码加密和密码验证的方法。通过实现这个接口，您可以将密码加密为不可逆的哈希值，以及在验证密码时对比哈希值。4. 实现 UserDetailsService：Spring Security 用户详情服务4.1 什么是 UserDetailsService？UserDetailsService 是 Spring Security 提供的接口，用于从应用程序的数据源（如数据库、LDAP、内存等）中加载用户信息。它是一个用于将用户详情加载到 Spring Security 的中心机制。UserDetailsService 主要负责两项工作：加载用户信息： 从数据源中加载用户的用户名、密码和角色等信息。创建 UserDetails 对象： 根据加载的用户信息，创建一个 Spring Security 所需的 UserDetails 对象，包含用户名、密码、角色和权限等。4.2 自定义实现类新建 service 包，并创建 UserDetailServiceImpl 实现类：@Service @Slf4j public class UserDetailServiceImpl implements UserDetailsService { @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { // 从数据库中查询 // ... // 暂时先写死，密码为 chengzi, 这里填写的密文，数据库中也是存储此种格式 // authorities 用于指定角色，这里写死为 ADMIN 管理员 return User.withUsername("chengzi") .password("$2a$10$n7RJ1q.RnXx5M3O6B0i0he04fZOPjIJpyWcKuicW1bFyFHWhlGose") .authorities("ADMIN") .build(); } } 上述代码中，我们实现了 UserDetailsService 接口，并重写了 loadUserByUsername() 方法，该方法用于根据用户名加载用户信息的逻辑 ，正常需要从数据库中查询，这里我们先写死，继续开发后面的功能，后续再回过头来改造。5. 自定义认证过滤器接下来，我们自定义一个用于认证的过滤器，新建 /filter 包，并创建JwtAuthenticationFilter 过滤器，代码如下：public class JwtAuthenticationFilter extends AbstractAuthenticationProcessingFilter { /** * 指定用户登录的访问地址 */ public JwtAuthenticationFilter() { super(new AntPathRequestMatcher("/login", "POST")); } @Override public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException, IOException, ServletException { ObjectMapper mapper = new ObjectMapper(); // 解析提交的 JSON 数据 JsonNode jsonNode = mapper.readTree(request.getInputStream()); JsonNode usernameNode = jsonNode.get("username"); JsonNode passwordNode = jsonNode.get("password"); // 判断用户名、密码是否为空 if (Objects.isNull(usernameNode) || Objects.isNull(passwordNode) || StringUtils.isBlank(usernameNode.textValue()) || StringUtils.isBlank(passwordNode.textValue())) { throw new UsernameOrPasswordNullException("用户名或密码不能为空"); } String username = usernameNode.textValue(); String password = passwordNode.textValue(); // 将用户名、密码封装到 Token 中 UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(username, password); return getAuthenticationManager().authenticate(usernamePasswordAuthenticationToken); } } 此过滤器继承了 AbstractAuthenticationProcessingFilter，用于处理 JWT（JSON Web Token）的用户身份验证过程。在构造函数中，调用了父类 AbstractAuthenticationProcessingFilter 的构造函数，通过 AntPathRequestMatcher 指定了处理用户登录的访问地址。这意味着当请求路径匹配 /login 并且请求方法为 POST 时，该过滤器将被触发。attemptAuthentication() 方法用于实现用户身份验证的具体逻辑。首先，我们解析了提交的 JSON 数据，并获取了用户名、密码，校验是否为空，若不为空，则将它们封装到 UsernamePasswordAuthenticationToken 中。最后，使用 getAuthenticationManager().authenticate() 来触发 Spring Security 的身份验证管理器执行实际的身份验证过程，然后返回身份验证结果。6. 自定义用户名或密码不能为空异常上面过滤器代码中，有个动作是校验用户名、密码是否为空，为空则抛出 UsernameOrPasswordNullException 异常，此类是自定义的得来的。新建包 /exception, 在此包中创建该类：public class UsernameOrPasswordNullException extends AuthenticationException { public UsernameOrPasswordNullException(String msg, Throwable cause) { super(msg, cause); } public UsernameOrPasswordNullException(String msg) { super(msg); } } 注意，需继承自 AuthenticationException，只有该类型异常，才能被后续自定义的认证失败处理器捕获到。7. 自定义处理器用户登录后，我们还需要处理其对应的结果，如登录成功，则返回 Token 令牌，登录失败，则返回对应的提示信息。在 Spring Security 中，AuthenticationFailureHandler 和 AuthenticationSuccessHandler 是用于处理身份验证失败和成功的接口。它们允许您在用户身份验证过程中自定义响应，以便更好地控制和定制用户体验。7.1 自定义认证成功处理器 RestAuthenticationSuccessHandler新建 /handler 包，并创建 RestAuthenticationSuccessHandler 类：@Component @Slf4j public class RestAuthenticationSuccessHandler implements AuthenticationSuccessHandler { @Autowired private JwtTokenHelper jwtTokenHelper; @Override public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException { // 从 authentication 对象中获取用户的 UserDetails 实例，这里是获取用户的用户名 UserDetails userDetails = (UserDetails) authentication.getPrincipal(); // 通过用户名生成 Token String username = userDetails.getUsername(); String token = jwtTokenHelper.generateToken(username); // 返回 Token LoginRspVO loginRspVO = LoginRspVO.builder().token(token).build(); ResultUtil.ok(response, Response.success(loginRspVO)); } } 此类实现了 Spring Security 的 AuthenticationSuccessHandler 接口，用于处理身份验证成功后的逻辑。首先，从 authentication 对象中获取用户的 UserDetails 实例，这里是主要是获取用户的用户名，然后通过用户名生成 Token 令牌，最后返回数据。7.2 自定义认证失败处理器在 /handler 包下，创建 RestAuthenticationFailureHandler 认证失败处理器：@Component @Slf4j public class RestAuthenticationFailureHandler implements AuthenticationFailureHandler { @Override public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException { log.warn("AuthenticationException: ", exception); if (exception instanceof UsernameOrPasswordNullException) { // 用户名或密码为空 ResultUtil.fail(response, Response.fail(exception.getMessage())); } else if (exception instanceof BadCredentialsException) { // 用户名或密码错误 ResultUtil.fail(response, Response.fail(ResponseCodeEnum.USERNAME_OR_PWD_ERROR)); } // 登录失败 ResultUtil.fail(response, Response.fail(ResponseCodeEnum.LOGIN_FAIL)); } } 通过自定义了一个实现了 Spring Security 的 AuthenticationFailureHandler 接口类，用于在用户身份验证失败后执行一些逻辑。首先，我们打印了异常日志，方便后续定位问题，然后对异常的类型进行判断，通过 ResultUtil 工具类，返回不同的错误信息，如用户名或者密码为空、用户名或密码错误等，若未判断出异常是什么类型，则统一提示为 登录失败。8. 自定义 JWT 认证功能配置完成了以上前置工作后，我们开始配置 JWT 认证相关的配置。在 /config 包下新建 JwtAuthenticationSecurityConfig, 代码如下：@Configuration public class JwtAuthenticationSecurityConfig extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> { @Autowired private RestAuthenticationSuccessHandler restAuthenticationSuccessHandler; @Autowired private RestAuthenticationFailureHandler restAuthenticationFailureHandler; @Autowired private PasswordEncoder passwordEncoder; @Autowired private UserDetailsService userDetailsService; @Override public void configure(HttpSecurity httpSecurity) throws Exception { // 自定义的用于 JWT 身份验证的过滤器 JwtAuthenticationFilter filter = new JwtAuthenticationFilter(); filter.setAuthenticationManager(httpSecurity.getSharedObject(AuthenticationManager.class)); // 设置登录认证对应的处理类（成功处理、失败处理） filter.setAuthenticationSuccessHandler(restAuthenticationSuccessHandler); filter.setAuthenticationFailureHandler(restAuthenticationFailureHandler); // 直接使用 DaoAuthenticationProvider, 它是 Spring Security 提供的默认的身份验证提供者之一 DaoAuthenticationProvider provider = new DaoAuthenticationProvider(); // 设置 userDetailService，用于获取用户的详细信息 provider.setUserDetailsService(userDetailsService); // 设置加密算法 provider.setPasswordEncoder(passwordEncoder); httpSecurity.authenticationProvider(provider); // 将这个过滤器添加到 UsernamePasswordAuthenticationFilter 之前执行 httpSecurity.addFilterBefore(filter, UsernamePasswordAuthenticationFilter.class); } } 上述代码是一个 Spring Security 配置类，用于配置 JWT（JSON Web Token）的身份验证机制。它继承了 Spring Security 的 SecurityConfigurerAdapter 类，用于在 Spring Security 配置中添加自定义的认证过滤器和提供者。通过重写 configure() 方法，我们将之前写好过滤器、认证成功、失败处理器，以及加密算法整合到了 httpSecurity 中。9. 应用 JWT 认证功能配置接下来，我们编辑验证模块中的 Spring Security 配置 WebSecurityConfig 类，修改内容如下：@Configuration @EnableWebSecurity public class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private JwtAuthenticationSecurityConfig jwtAuthenticationSecurityConfig; @Override protected void configure(HttpSecurity http) throws Exception { http.csrf().disable(). // 禁用 csrf formLogin().disable() // 禁用表单登录 .apply(jwtAuthenticationSecurityConfig) // 设置用户登录认证相关配置 .and() .authorizeHttpRequests() .mvcMatchers("/admin/**").authenticated() // 认证所有以 /admin 为前缀的 URL 资源 .anyRequest().permitAll() // 其他都需要放行，无需认证 .and() .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS); // 前后端分离，无需创建会话 } } 上述代码中，在 configure() 方法中，首先禁用了 CSRF（Cross-Site Request Forgery）攻击防护。在前后端分离的情况下，通常不需要启用 CSRF 防护。同时，还禁用了表单登录，并应用了 JWT 相关的配置类 JwtAuthenticationSecurityConfig。最后，配置会话管理这块，将会话策略设置为无状态（STATELESS），适用于前后端分离的情况，无需创建会话。10. 结语Spring Security JWT 提供了一种安全且灵活的方式来实现身份验证和授权，适用于前后端分离的应用程序。通过使用 JWT，您可以实现无状态的身份验证机制，提高应用程序的安全性和可维护性。
1. 前言配置SQL打印一方面可以了解到每个操作都具体执行的什么 SQL 语句， 另一方面通过打印执行耗时，也可以提前发现一些慢 SQL，提前做好优化， 省得 DBA 公开处刑。注意，生产环境不推荐打印执行 SQL，会有数据泄漏风险，仅推荐本地开发使用。2. 添加依赖在父项目的 pom.xml 文件中，声明 p6spy 依赖的版本号： <properties> // 省略... <p6spy.version>3.9.1</p6spy.version> </properties> <dependencyManagement> <dependencies> // 省略... <dependency> <groupId>p6spy</groupId> <artifactId>p6spy</artifactId> <version>${p6spy.version}</version> </dependency> </dependencies> </dependencyManagement> 然后在具体的业务模块中的 pom.xml 文件中，引入该依赖： <dependency> <groupId>p6spy</groupId> <artifactId>p6spy</artifactId> </dependency> 3. 添加配置3.1 修改 application-dev.yml 配置文件spring: datasource: driver-class-name: com.p6spy.engine.spy.P6SpyDriver url: jdbc:p6spy:mysql://127.0.0.1:3306/weblog?useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&useSSL=false&zeroDateTimeBehavior=convertToNull ... 注意：driver-class-name : 修改为 p6spy 提供的驱动类;url ： 修改为前缀为 jdbc:p6spy 跟着冒号，后面对应数据库连接地址；3.2 添加 p6spy 配置文件然后在模块的 resources 目录下添加 spy.properties 配置文件：配置文件内容如下：#3.2.1以上使用 modulelist=com.baomidou.mybatisplus.extension.p6spy.MybatisPlusLogFactory,com.p6spy.engine.outage.P6OutageFactory #3.2.1以下使用或者不配置 #modulelist=com.p6spy.engine.logging.P6LogFactory,com.p6spy.engine.outage.P6OutageFactory # 自定义日志打印 logMessageFormat=com.baomidou.mybatisplus.extension.p6spy.P6SpyLogger #日志输出到控制台 appender=com.baomidou.mybatisplus.extension.p6spy.StdoutLogger # 使用日志系统记录 sql #appender=com.p6spy.engine.spy.appender.Slf4JLogger # 设置 p6spy driver 代理 deregisterdrivers=true # 取消JDBC URL前缀 useprefix=true # 配置记录 Log 例外,可去掉的结果集有error,info,batch,debug,statement,commit,rollback,result,resultset. excludecategories=info,debug,result,commit,resultset # 日期格式 dateformat=yyyy-MM-dd HH:mm:ss # 实际驱动可多个 #driverlist=org.h2.Driver # 是否开启慢SQL记录 outagedetection=true # 慢SQL记录标准 2 秒 outagedetectioninterval=2 4. 生产环境不要启用 p6spyp6spy 组件请勿在生产环境使用，因为有性能损耗，推荐仅本地 dev 环境开发开启使用。 最终做法是，在 applcation-prod.yml 文件不启用 p6spy 组件，我们将 application-dev.yml 的数据库配置复制一份到 application-prod.yml 文件中，注意，要将驱动、前缀恢复正常，内容如下： driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://127.0.0.1:3306/windblog?useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&useSSL=false&zeroDateTimeBehavior=convertToNull username: root password: 1234565. 结语本文给大家介绍了在项目用使用 Mybatis Plus 持久层框架时，如何通过 p6spy 组件完整的打印 SQL 语句，以及执行耗时，此功能对于本地开发定位问题、杜绝慢查询都非常有帮助。
1. 什么是 MyBatis Plus？MyBatis Plus （简称 MP） 是一款持久层框架，说白话就是一款操作数据库的框架。它是一个 MyBatis 的增强工具，就像 iPhone手机一般都有个 plus 版本一样，它在 MyBatis 的基础上只做增强不做改变，为简化开发、提高效率而生。MyBatis Plus 的愿景是成为 MyBatis 最好的搭档，效率翻倍。2. MyBatis Plus 的优势快速开发：MyBatis Plus 提供了一系列的便捷功能，如自动生成 SQL 语句、通用 Mapper 等，使数据库操作更加高效，能够节省开发时间。更少的配置： Spring Boot 已经为我们提供了很多默认的配置，整合 MyBatis Plus 时只需少量的配置，减少了繁琐的配置步骤。内置分页插件：MyBatis Plus 内置了分页插件，无需额外的代码，就可以轻松实现分页查询。更好的支持： MyBatis Plus 在社区中有较广泛的使用，拥有活跃的维护者和开发者，您可以轻松找到解决方案和文档。3. 整合3.1 添加依赖在父项目的 pom.xml 文件中，声明 MP 的依赖版本号： <properties> // 省略... <mybatis-plus.version>3.5.2</mybatis-plus.version> </properties> <dependencyManagement> <dependencies> // 省略... <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>${mybatis-plus.version}</version> </dependency> </dependencies> </dependencyManagement> 然后，在具体的业务模块的 pom.xml 文件中，引入 MP 和 MySQL 依赖： <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> </dependency> 3.2 配置文件编辑 applicaiton-dev.yml 文件，添加数据库链接相关的配置，包含连接池的配置：spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://127.0.0.1:3306/weblog?useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&useSSL=false&zeroDateTimeBehavior=convertToNull username: root password: 123456 hikari: minimum-idle: 5 maximum-pool-size: 20 auto-commit: true idle-timeout: 30000 pool-name: Weblog-HikariCP max-lifetime: 1800000 connection-timeout: 30000 connection-test-query: SELECT 1 数据库链接池这块，我们使用的 Spring Boot 默认的 HikariCP，它是一个高性能的连接池实现 , 同时，它号称是速度最快的连接池：spring.datasource.hikari.minimum-idle: Hikari连接池中最小空闲连接数。spring.datasource.hikari.maximum-pool-size: Hikari连接池中允许的最大连接数。spring.datasource.hikari.auto-commit: 连接是否自动提交事务。spring.datasource.hikari.idle-timeout: 连接在连接池中闲置的最长时间，超过这个时间会被释放。spring.datasource.hikari.pool-name: 连接池的名字。spring.datasource.hikari.max-lifetime: 连接在连接池中的最大存活时间，超过这个时间会被强制关闭。spring.datasource.hikari.connection-timeout: 获取连接的超时时间。spring.datasource.hikari.connection-test-query: 用于测试连接是否可用的SQL查询，这里使用的是SELECT 1，表示执行这个查询来测试连接。然后，在 common 模块中的 config 包下，新建一个 MybatisPlusConfig 配置文件，代码如下：package com.windblog.common.config; import org.mybatis.spring.annotation.MapperScan; import org.springframework.context.annotation.Configuration; @Configuration @MapperScan("com.windblog.common.domain.mapper") public class MybatisPlusConfig { } @MapperScan : 扫描 MP 的 mapper 接口存放位置。3.3 映射xml如果自动以sql不采用注解的卸载mapper的话，而是映射到resource下的xml文件，需要在项目的yml配置中新增xml的路径mybatis-plus: mapper-locations: classpath:mapper/*.xml4. 结语至此，整合 Mabatis Plus 持久层框架就成功了，有了持久层框架，能够帮助开发人员更方便地进行数据库操作。同时，MyBatis Plus 的强大功能和 Spring Boot 的自动化配置使得整合变得非常简单，节省了开发人员的时间和精力。
1. 介绍在业务开发中，我们需要对前端传入的各种参数做格式、长度、合规性校验，如果全部用if去判断写在业务代码里，观感非常的不好，也不够轻量。2. JSR 380 参数校验注解Spring Boot 提供了简洁的方法，让我们能够利用 Java 校验 API (JSR 380) 中定义的注解进行参数校验。JSR 380，也被称为 Bean Validation 2.0，是 Java Bean 验证规范的一个版本。该规范定义了一系列注解，用于验证 Java Bean 对象的属性，确保它们满足某些条件或限制。以下是 JSR 380 中提供的主要验证注解及其描述：@NotNull: 验证对象值不应为 null。@AssertTrue: 验证布尔值是否为 true。@AssertFalse: 验证布尔值是否为 false。@Min(value): 验证数字是否不小于指定的最小值。@Max(value): 验证数字是否不大于指定的最大值。@DecimalMin(value): 验证数字值（可以是浮点数）是否不小于指定的最小值。@DecimalMax(value): 验证数字值（可以是浮点数）是否不大于指定的最大值。@Positive: 验证数字值是否为正数。@PositiveOrZero: 验证数字值是否为正数或零。@Negative: 验证数字值是否为负数。@NegativeOrZero: 验证数字值是否为负数或零。@Size(min, max): 验证元素（如字符串、集合或数组）的大小是否在给定的最小值和最大值之间。@Digits(integer, fraction): 验证数字是否在指定的位数范围内。例如，可以验证一个数字是否有两位整数和三位小数。@Past: 验证日期或时间是否在当前时间之前。@PastOrPresent: 验证日期或时间是否在当前时间或之前。@Future: 验证日期或时间是否在当前时间之后。@FutureOrPresent: 验证日期或时间是否在当前时间或之后。@Pattern(regexp): 验证字符串是否与给定的正则表达式匹配。@NotEmpty: 验证元素（如字符串、集合、Map 或数组）不为 null，并且其大小/长度大于0。@NotBlank: 验证字符串不为 null，且至少包含一个非空白字符。@Email: 验证字符串是否符合有效的电子邮件格式。除了上述的标准注解，JSR 380 也支持开发者定义和使用自己的自定义验证注解。此外，这个规范还提供了一系列的APIs和工具，用于执行验证和处理验证结果。大部分现代Java框架（如 Spring 和 Jakarta EE）都与 JSR 380 兼容，并支持其验证功能。3. 项目集成3.1 引入依赖pom.xml 文件添加参数校验依赖<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-validation</artifactId> </dependency>3.2 实体类参数校验package com.windblog.web.model; import lombok.Data; import javax.validation.constraints.*; @Data public class User { // 用户名 @NotBlank(message = "用户名不能为空") private String username; // 性别 @NotNull(message = "性别不能为空") private Integer sex; // 年龄 @NotNull(message = "年龄不能为空") @Min(value = 18, message = "年龄必须大于或等于 18") @Max(value = 100, message = "年龄必须小于或等于 100") private Integer age; // 邮箱 @NotBlank(message = "邮箱不能为空") @Email(message = "邮箱格式不正确") private String email; } 3.3 Controller 参数校验针对每个字段的校验注解添加完成后，还需要在 controller 层进行捕获，并将错误信息返回。编辑 TestController 类，代码如下：@RestController @Slf4j public class TestController { @PostMapping("/test") @ApiOperationLog(description = "测试接口") public ResponseEntity<String> test(@RequestBody @Validated User user, BindingResult bindingResult) { // 是否存在校验错误 if (bindingResult.hasErrors()) { // 获取校验不通过字段的提示信息 String errorMsg = bindingResult.getFieldErrors() .stream() .map(FieldError::getDefaultMessage) .collect(Collectors.joining(", ")); return ResponseEntity.badRequest().body(errorMsg); } // 返参 return ResponseEntity.ok("参数没有任何问题"); } } 解释一下上面代码中的关键部分：@Validated: 告诉 Spring 需要对 User 对象执行校验;BindingResult : 验证的结果对象，其中包含所有验证错误信息；4. 全局异常处理对于参数校验的异常，如果每个接口都需要处理，那么也太麻烦了，所以我们需要添加一个全局异常处理，统一帮我们去处理校验的不通过等其他异常4.1 自定义一个基础异常接口package com.windblog.common.exception; public interface BaseExceptionInterface { String getErrorCode(); String getErrorMessage(); } 4.2 自定义错误码枚举新建 enums 包，用于统一放置枚举类，在该包中，创建 ResponseCodeEnum 异常码枚举类，代码如下：package com.windblog.common.enums; import com.windblog.common.exception.BaseExceptionInterface; import lombok.AllArgsConstructor; import lombok.Getter; @Getter @AllArgsConstructor public enum ResponseCodeEnum implements BaseExceptionInterface { // ----------- 通用异常状态码 ----------- SYSTEM_ERROR("10000", "出错啦，后台小哥正在努力修复中..."), // ----------- 业务异常状态码 ----------- PRODUCT_NOT_FOUND("20000", "该产品不存在（测试使用）"), ; // 异常码 private String errorCode; // 错误信息 private String errorMessage; } 4.3 自定义业务异常 BizException创建 BizException ， Biz 是业务英文 Business 的缩写:package com.windblog.common.exception; import lombok.Getter; import lombok.Setter; @Getter @Setter public class BizException extends RuntimeException { // 异常码 private String errorCode; // 错误信息 private String errorMessage; public BizException(BaseExceptionInterface baseExceptionInterface) { this.errorCode = baseExceptionInterface.getErrorCode(); this.errorMessage = baseExceptionInterface.getErrorMessage(); } } 我们让 BizException 继承自运行时异常 RuntimeException, 同时定义了两个基本字段：errorCode : 异常码；errorMessage: 错误信息，用于提供给调用者；另外，还定义了一个构造器，入参是前面创建的 BaseExceptionInterface，通过它可以方便的构造一个业务异常。4.4 使用 @ControllerAdviceSpring Boot 提供了 @ControllerAdvice 注解，它允许我们定义一个全局的异常处理类。这个类可以捕获应用中抛出的所有异常，并根据需要进行处理。在 common 模块中的 pom.xml 中添加如下依赖，因为 @ControllerAdvice 注解在这个依赖中：<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency>4.5 创建全局异常处理类在 exception 包下，创建全局异常处理类 GlobalExceptionHandler ，代码如下： package com.windblog.common.exception; import com.windblog.common.utils.Response; import lombok.extern.slf4j.Slf4j; import org.springframework.web.bind.annotation.ControllerAdvice; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.ResponseBody; import javax.servlet.http.HttpServletRequest; @ControllerAdvice @Slf4j public class GlobalExceptionHandler { /** * 捕获自定义业务异常 * @return */ @ExceptionHandler({ BizException.class }) @ResponseBody public Response<Object> handleBizException(HttpServletRequest request, BizException e) { log.warn("{} request fail, errorCode: {}, errorMessage: {}", request.getRequestURI(), e.getErrorCode(), e.getErrorMessage()); return Response.fail(e); } } 上述代码中，通过 @ControllerAdvice 注解，我们将 GlobalExceptionHandler 声明为了全局异常处理类。在其中，定义了一个 handleBizException() 方法，并通过 @ExceptionHandler 注解指定只捕获 BizException 自定义业务异常。然后，打印了相关错误日志，并组合了统一的响应格式返回。4.6 捕获参数异常为了解放双手，我们可以通过全局异常处理器来捕获该异常，统一返回错误信息，改造 GlobalExceptionHandler 类，添加 handleMethodArgumentNotValidException() 方法，代码如下： /** * 捕获参数校验异常 * @return */ @ExceptionHandler({ MethodArgumentNotValidException.class }) @ResponseBody public Response<Object> handleMethodArgumentNotValidException(HttpServletRequest request, MethodArgumentNotValidException e) { // 参数错误异常码 String errorCode = ResponseCodeEnum.PARAM_NOT_VALID.getErrorCode(); // 获取 BindingResult BindingResult bindingResult = e.getBindingResult(); StringBuilder sb = new StringBuilder(); // 获取校验不通过的字段，并组合错误信息，格式为： email 邮箱格式不正确, 当前值: '123124qq.com'; Optional.ofNullable(bindingResult.getFieldErrors()).ifPresent(errors -> { errors.forEach(error -> sb.append(error.getField()) .append(" ") .append(error.getDefaultMessage()) .append(", 当前值: '") .append(error.getRejectedValue()) .append("'; ") ); }); // 错误信息 String errorMessage = sb.toString(); log.warn("{} request error, errorCode: {}, errorMessage: {}", request.getRequestURI(), errorCode, errorMessage); return Response.fail(errorCode, errorMessage); } 上述代码中，我们通过 @ExceptionHandler 注解捕获了 MethodArgumentNotValidException.class 类型的异常，并从异常实体类中获取了 BindingResult 对象，从而获取到具体哪些字段校验不通过，最终组合错误信息并返回。记得添加对应错误的枚举
1.前言在后端业务中，对每次请求的入参、被请求类、方法，以及出参、执行耗时等信息进行日志打印，是很有必要的，有了这些信息，当某个接口出现问题时，可以帮助我们快速完成问题的追踪。那么，Spring Boot 中要如何实现呢? 相对于这种与主流程有一定的功能解耦的相对独立小功能可以使用springboot的切面来在关键地方织入我们的程序。2.什么是自定义注解 (Custom Annotations)？Java 注解是从 Java 5 开始引入的，它为我们提供了一种元编程的方法，允许我们在不改变代码逻辑的情况下为代码添加元数据。这些元数据可以在编译时或运行时通过反射被访问。自定义注解就是用户定义的，用于为代码提供元数据的注解。例如，本小节中自定义的 @ApiOperationLog 注解，它用来表示一个方法在执行时需要被记录日志。3.什么是 AOP (面向切面编程)？AOP（Aspect-Oriented Programming，面向切面编程）是一个编程范式，它提供了一种能力，让开发者能够模块化跨多个对象的横切关注点（例如日志、事务管理、安全等）。主要概念包括：切点 (Pointcuts): 定义在哪里应用切面（即在哪里插入横切关注点的代码）。通知 (Advices): 定义在特定切点上要执行的代码。常见的通知类型有：前置通知、后置通知、环绕通知等。切面 (Aspects): 切面将切点和通知结合起来，定义了在何处和何时应用特定的逻辑。例如，使用AOP，我们可以为所有使用 @ApiOperationLog 注解的方法自动添加日志逻辑，而不需要在每个方法中手动添加。4.开始动手4.1 添加依赖添加 jackson 工具，它用于将出入参转为 json 字符串： <properties> ...省略 <jackson.version>2.15.2</jackson.version> </properties> <dependencies> ...省略 <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId> </dependency> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> <version>${jackson.version}</version> </dependency> </dependencies> 4.2 自定义注解新建一个名为 aspect 的包，用于放置切面相关的功能类接着，在其中创建一个名为 ApiOperationLog 的自定义注解：package com.wind.weblog.common.aspect; import java.lang.annotation.*; @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.METHOD}) @Documented public @interface ApiOperationLog { /** * 日志方法描述 * * @return */ String description() default ""; } 元注解说明：@interface用来声明这是一个注解，接口是interface来声明@Retention(RetentionPolicy.RUNTIME)： 这个元注解用于指定注解的保留策略，即注解在何时生效。RetentionPolicy.RUNTIME 表示该注解将在运行时保留，这意味着它可以通过反射在运行时被访问和解析。@Target({ElementType.METHOD})： 这个元注解用于指定注解的目标元素，即可以在哪些地方使用这个注解。ElementType.METHOD 表示该注解只能用于方法上。这意味着您只能在方法上使用这个特定的注解。@Documented： 这个元注解用于指定被注解的元素是否会出现在生成的Java文档中。如果一个注解使用了 @Documented，那么在生成文档时，被注解的元素及其注解信息会被包含在文档中。这可以帮助文档生成工具（如 JavaDoc）在生成文档时展示关于注解的信息。4.3 日志切面aspectj 注解说明在配置 AOP 切面之前，我们需要了解下 aspectj 相关注解的作用：@Aspect：声明该类为一个切面类；@Pointcut：定义一个切点，后面跟随一个表达式，表达式可以定义为切某个注解，也可以切某个 package 下的方法；切点定义好后，就是围绕这个切点做文章了：@Before: 在切点之前，织入相关代码；@After: 在切点之后，织入相关代码;@AfterReturning: 在切点返回内容后，织入相关代码，一般用于对返回值做些加工处理的场景；@AfterThrowing: 用来处理当织入的代码抛出异常后的逻辑处理;@Around: 环绕，可以在切入点前后织入代码，并且可以自由的控制何时执行切点；创建 JSON 转换方法在定义日志切面之前，我们先来创建一个 JSON 转换方法，这在日志切面中打印出入参为 JSON 字符串会用到。private String toJson(JoinPoint joinPoint) throws JsonProcessingException { ObjectMapper mapper = new ObjectMapper(); return mapper.writeValueAsString(joinPoint.getArgs()); }完整的切面类： package com.wind.weblog.common.aspect; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import com.wind.weblog.common.domain.dos.VisitorRecordDO; import com.wind.weblog.common.domain.mapper.VisitorMapper; import lombok.extern.slf4j.Slf4j; import org.aspectj.lang.JoinPoint; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.*; import org.springframework.beans.factory.annotation.Value; import org.springframework.scheduling.annotation.EnableScheduling; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Component; import org.springframework.web.context.request.RequestContextHolder; import org.springframework.web.context.request.ServletRequestAttributes; import javax.annotation.Resource; import javax.servlet.http.HttpServletRequest; import java.lang.reflect.Method; import java.util.Date; import java.util.concurrent.ConcurrentHashMap; import static com.wind.weblog.common.utils.AgentRegionUtils.getIpAddress; import static com.wind.weblog.common.utils.AgentRegionUtils.getIpRegion; @Aspect @Component @Slf4j @EnableScheduling public class ApiOperationLogAspect { /** * 换行符 */ private static final String LINE_SEPARATOR = System.lineSeparator(); /** * 使用线程安全的hashmap作为每天访客ip记录 */ private static final ConcurrentHashMap<String, String> AGENT_IP = new ConcurrentHashMap<>(8); /** * 定时更新访问ip记录 每天零点零一秒 */ private static final String TASK_SCHEDULE = "1 0 0 * * ?" ; /** * todo ip2region.xdb库对应的文件路径，需在配置文件中写入 */ @Value("${xdb.profile}") private String xdbPath; @Resource private VisitorMapper visitorMapper; /** 以所有添加 @WebLog 注解为切点 */ @Pointcut("@annotation(com.wind.weblog.common.aspect.ApiOperationLog)") public void apiOperationLog() { } /** * 在切点之前织入 * @param joinPoint * @throws Throwable */ @Before("apiOperationLog()") public void doBefore(JoinPoint joinPoint) throws Throwable { // 开始打印请求日志 ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); HttpServletRequest request = attributes.getRequest(); // 获取日志注解的描述信息 String methodDescription = getAspectLogDescription(joinPoint); // 打印请求相关参数 log.info("========== 请求开始: [{}], 入参: {} =================================== ", methodDescription, toJson(joinPoint)); log.warn("请求的类: {}, 方法: {}", joinPoint.getSignature().getDeclaringTypeName(), joinPoint.getSignature().getName()); // 根据request 获取ip地址并查询归属地 String ipAddress = getIpAddress(request); if (!AGENT_IP.containsKey(ipAddress)) { VisitorRecordDO visitorRecordDO = new VisitorRecordDO(); String region = getIpRegion(ipAddress, xdbPath); // 将新的访客IP和归属地信息放入map AGENT_IP.put(ipAddress, region); Date date = new Date(); visitorRecordDO.setVisitor("agent"); visitorRecordDO.setIpAddress(ipAddress); visitorRecordDO.setIpRegion(region); visitorRecordDO.setVisitTime(date); // todo 是否邮箱通知，默认为0不通知 visitorRecordDO.setIsNotify(0); visitorMapper.insert(visitorRecordDO); log.info("ipAddress: {}, Region: {}", ipAddress, region); } } /** * 每天零点零一秒执行，删除hashmap中的输出，更新当天访问ip记录 */ @Scheduled(cron = TASK_SCHEDULE) private void scheduledClearTask() { AGENT_IP.clear(); } /** * 在切点之后织入 * @throws Throwable */ @After("apiOperationLog()") public void doAfter() throws Throwable { // nothing } /** * 环绕 * @param proceedingJoinPoint * @return * @throws Throwable */ @Around("apiOperationLog()") public Object doAround(ProceedingJoinPoint proceedingJoinPoint) throws Throwable { long startTime = System.currentTimeMillis(); Object result = proceedingJoinPoint.proceed(); // 耗时 long timeConsuming = System.currentTimeMillis() - startTime; String methodDescription = getAspectLogDescription(proceedingJoinPoint); log.info("========== 请求结束: [{}], 耗时: {}ms, 出参: {} =================================== {}", methodDescription, timeConsuming, new ObjectMapper().writeValueAsString(result), LINE_SEPARATOR); return result; } /** * 获取切面注解的描述 * * @param joinPoint 切点 * @return 方法描述 * @throws Exception */ public String getAspectLogDescription(JoinPoint joinPoint) throws Exception { String targetName = joinPoint.getTarget().getClass().getName(); String methodName = joinPoint.getSignature().getName(); Object[] arguments = joinPoint.getArgs(); Class targetClass = Class.forName(targetName); Method[] methods = targetClass.getMethods(); StringBuilder description = new StringBuilder(""); for (Method method : methods) { if (method.getName().equals(methodName)) { Class[] clazzs = method.getParameterTypes(); if (clazzs.length == arguments.length) { description.append(method.getAnnotation(ApiOperationLog.class).description()); break; } } } return description.toString(); } /** * 转 json * @param joinPoint * @return */ private String toJson(JoinPoint joinPoint) throws JsonProcessingException { ObjectMapper mapper = new ObjectMapper(); return mapper.writeValueAsString(joinPoint.getArgs()); } } 添加包扫描在启动类 WeblogWebApplication 中，手动添加包扫描 @ComponentScan , 指定扫描 com.wind.weblog 包下面的所有类:5. 测试一下运行后的执行效果
1.介绍Logback 是日志框架 SLF4J 的一个实现，它被设计用来替代 log4j。Logback 提供了更高的性能，更丰富的日志功能和更好的配置选项。1.1 为什么要用它？在 Spring Boot 中，Logback 是默认的日志实现，至于官方为何用它作为默认日志组件，有以下几个原因：性能：Logback 在性能上超越了许多其他的日志实现，尤其是在高并发环境下。灵活性：Logback 提供了高度灵活的日志配置方式，支持从 XML、Groovy 以及编程式的方式进行配置。功能丰富：除了基本的日志功能，Logback 还提供了如日志归档、日志级别动态修改、事件监听等高级功能。与 SLF4J 集成：SLF4J 是一个日志门面（facade），使得应用程序可以在运行时更换日志实现。Logback 作为 SLF4J 的一个原生实现，可以无缝地与其集成。与 Spring Boot 的自动配置集成：Spring Boot 提供了对 Logback 的自动配置，这意味着开发者无需手动配置 Logback，只需提供一个简单的配置文件即可。2.引入依赖由于 Spring Boot 默认使用 Logback，所以当你在 pom.xml 中加入 spring-boot-starter-web 依赖时，它会自动包含 Logback 相关依赖，无需额外添加：<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency>3.自定义 Logback 配置为了满足特定的日志需求，我们通常会自定义 Logback 配置。这里需要注意，配置文件我们统一放置在 weblog-web 模块中，方便统一管理。然后在 src/main/resources 目录下，创建一个名为 logback-weblog.xml 的文件。<?xml version="1.0" encoding="UTF-8"?> <configuration > <jmxConfigurator/> <include resource="org/springframework/boot/logging/logback/defaults.xml" /> <property scope="context" name="appName" value="weblog" /> <property name="LOG_FILE" value="/app/weblog/logs/${appName}.%d{yyyy-MM-dd}"/> <property name="FILE_LOG_PATTERN" value="%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n"/> <!--<property name="CONSOLE_LOG_PATTERN" value="${FILE_LOG_PATTERN}"/>--> <appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender"> <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy"> <FileNamePattern>${LOG_FILE}-%i.log</FileNamePattern> <MaxHistory>30</MaxHistory> <TimeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP"> <maxFileSize>10MB</maxFileSize> </TimeBasedFileNamingAndTriggeringPolicy> </rollingPolicy> <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder"> <pattern>${FILE_LOG_PATTERN}</pattern> </encoder> </appender> <springProfile name="dev"> <include resource="org/springframework/boot/logging/logback/console-appender.xml" /> <root level="info"> <appender-ref ref="CONSOLE" /> </root> </springProfile> <springProfile name="prod"> <include resource="org/springframework/boot/logging/logback/console-appender.xml" /> <root level="INFO"> <appender-ref ref="FILE" /> </root> </springProfile> </configuration> 因为打印日志到文件只需要在生产环境开启就行了，所以，使日志生效的配置放到 application-prod.yml 文件中就行了：#================================================================= # log 日志 #================================================================= logging: config: classpath:logback-weblog.xml测试开发环境直接在控制台就可以看日志了4.打印日志为了测试一下日志是否能够正常打印，我们创建一个test类进行测试 package com.wind.weblog.web; import lombok.extern.slf4j.Slf4j; import org.junit.jupiter.api.Test; import org.springframework.boot.test.context.SpringBootTest; @Slf4j @SpringBootTest class WeblogWebApplicationTests { @Test void contextLoads() { } @Test void testLog() { log.info("这是一行 Info 级别日志"); log.warn("这是一行 Warn 级别日志"); log.error("这是一行 Error 级别日志"); // 占位符 String author = "chengzi"; log.info("这是一行带有占位符日志，作者：{}", author); } } 启动后控制台就能看到日志打印了
用宝塔轻松部署java springboot的前后端分离项目
1.介绍传统启动java项目需要上传jar然后用命令行nohup java -jar去启动，然后前端也是单独启动，同时还需配置nginx，稍不留神出了差错就要定位很久，而宝塔运维面板对于前后端分离的项目，只需一个步骤就可以集合管理前后端同时自动设置好nginx和域名等，很方便，还能实时监测系统占用查看日志也很方便，所以推荐这种方式2.前置准备云服务器安装完宝塔 这个直接百度有一键脚本宝塔安装jdk 软件商店搜索jdk即可 这次项目是用的1.8宝塔安装数据库 并将后端的配置中的正式环境数据库地址改为宝塔的数据库信息购买一个域名（没有就用ip:端口访问 最好是买一个）springboot项目已打包成jar前端项目已打包（这里是vue3项目 打包的是dist包）3.步骤创建一个文件夹，上传你的jar包和前端包注意前端包要解压网站里添加java项目不出意外你保存完就能看到项目运行了点击项目，在左侧可以看到项目的启动日志，方便看为啥挂了左侧的ssl可以申请免费域名证书，开启https访问，注意你先将你的域名解析到你的服务器然后访问你的域名就可以浏览你的网站了！
springboot项目配置Lombok 避免编写那些冗余的 Java 样板式代码
1.介绍Lombok 是一个超酷的 Java 库，它能让你避免编写那些冗余的 Java 样板式代码，如对象中的 get、set、toString 等方法，解放你的双手，堪称偷懒神器，在企业级项目开发中，是必会的一个库。1.1 简化 Getter 和 Setter 方法在传统的 Java 开发中，你经常需要为每个类的属性手动编写 Getter 和 Setter 方法，但是有了 Lombok，你只需要在属性上加上 @Getter 和 @Setter 注解，Lombok 就会为你自动生成这些方法。1.2 自动生成构造函数通过 @NoArgsConstructor、@RequiredArgsConstructor 或 @AllArgsConstructor 注解，你可以快速生成无参构造函数、带有必需参数的构造函数或者带有全部参数的构造函数。不用每个需要注入的对象都添加@Autowrite了1.3 自动生成 equals 和 hashCode 方法通过 @EqualsAndHashCode 注解，Lombok 会根据类的字段自动生成 equals() 和 hashCode() 方法，让你的类更易于比较和使用在集合中。1.4 日志记录更轻松使用 @Slf4j 注解，你可以直接在类中使用 log 对象，而无需手动创建日志记录器。1.5 简化异常抛出通过 @SneakyThrows 注解，你可以在方法中抛出受检异常，而无需显式地在方法上声明或捕获它们，但是用的较少，业务中一般的异常都需要catch去处理1.6 数据类简化使用 @Data 注解，Lombok 会为你自动生成所有常用方法，如 Getter、Setter、toString() 等，让你的数据类更加简洁1.7 链式调用使用 @Builder 注解，Lombok 可以帮你创建一个更优雅的构建器模式，让你的对象初始化更加流畅。例子：ArticleDO articleDO = ArticleDO.builder() .title(publishArticleReqVO.getTitle()) .titleImage(publishArticleReqVO.getTitleImage()) .description(publishArticleReqVO.getDescription()) .build();2.项目中添加 Lombok 依赖想要在项目中使用 Lombok, 首先, 你需要在项目中添加 Lombok 依赖。只需要在项目的 pom.xml 文件中添加以下依赖：<dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>1.18.28</version> <scope>provided</scope> </dependency>3.IDEA 配置 Lombok 插件TIP: IDEA 2020.3 及以上版本已经内置安装了 Lombok 插件，如果你的 IDEA 版本大于该版本，则不用管，否则需要按下面的步骤，来手动安装 Lombok 插件。除了添加依赖外，还需要在 IDEA 中安装 Lombok 插件，才能正式的使用 Lombok。根据你的系统依次点击菜单：Windows 系统：File -> Settings... -> Plugins;Mac 系统：IntelliJ IDEA -> Preferences -> Plugins;点击 Marketplace ， 进入插件市场, 输入关键词 lombok, 搜索该插件：然后，点击 Install 按钮开始安装，这里已经安装过了，所以显示的打钩。安装完成后，点击 Apply 按钮应用设置即可。
2023.12.5 更新项目运行中发现，引入了org.apache.poi太多的包，导致内部有依赖冲突且很不好排查，修改后直接使用aspose-words的包就能实现word转换pdf更新后的pom依赖<dependency> <groupId>com.luhuiguo</groupId> <artifactId>aspose-words</artifactId> <version>23.1</version> </dependency> <dependency> <groupId>cn.hutool</groupId> <artifactId>hutool-all</artifactId> <version>5.7.5</version> </dependency>核心代码：import com.aspose.words.Document; import com.aspose.words.License; import com.aspose.words.SaveFormat; import java.io.ByteArrayInputStream; import java.io.File; import java.io.FileOutputStream; import java.io.InputStream; public class WordToPdf { public static File docToPdf(String docPath, String pdfPath) { System.out.println("WORD转化PDF开始>>>>"); //转化用时 File pdfFile = new File(pdfPath); try { long old = System.currentTimeMillis(); String s = "<License><Data><Products><Product>Aspose.Total for Java</Product><Product>Aspose.Words for Java</Product></Products><EditionType>Enterprise</EditionType><SubscriptionExpiry>20991231</SubscriptionExpiry><LicenseExpiry>20991231</LicenseExpiry><SerialNumber>8bfe198c-7f0c-4ef8-8ff0-acc3237bf0d7</SerialNumber> <Signature>sNLLKGMUdF0r8O1kKilWAGdgfs2BvJb/2Xp8p5iuDVfZXmhppo+d0Ran1P9TKdjV4ABwAgKXxJ3jcQTqE/2IRfqwnPf8itN8aFZlV3TJPYeD3yWE7IT55Gz6EijUpC7aKeoohTb4w2fpox58wWoF3SNp6sK6jDfiAUGEHYJ9pjU=</Signature></License>"; ByteArrayInputStream is = new ByteArrayInputStream(s.getBytes()); License license = new License(); license.setLicense(is); Document document = new Document(docPath); FileOutputStream outputStream = new FileOutputStream(pdfFile); document.save(outputStream, SaveFormat.PDF); long now = System.currentTimeMillis(); outputStream.close(); is.close(); System.out.println("WORD转化PDF共耗时：" + ((now - old) / 1000.0) + "秒"); //转化用时 } catch (Exception e) { System.out.println("转化失败"); e.printStackTrace(); } return pdfFile; } public static File docToPdf(InputStream docPathInputStream, String pdfPath) { File pdfFile = new File(pdfPath); try { long old = System.currentTimeMillis(); String s = "<License><Data><Products><Product>Aspose.Total for Java</Product><Product>Aspose.Words for Java</Product></Products><EditionType>Enterprise</EditionType><SubscriptionExpiry>20991231</SubscriptionExpiry><LicenseExpiry>20991231</LicenseExpiry><SerialNumber>8bfe198c-7f0c-4ef8-8ff0-acc3237bf0d7</SerialNumber> <Signature>sNLLKGMUdF0r8O1kKilWAGdgfs2BvJb/2Xp8p5iuDVfZXmhppo+d0Ran1P9TKdjV4ABwAgKXxJ3jcQTqE/2IRfqwnPf8itN8aFZlV3TJPYeD3yWE7IT55Gz6EijUpC7aKeoohTb4w2fpox58wWoF3SNp6sK6jDfiAUGEHYJ9pjU=</Signature></License>"; ByteArrayInputStream is = new ByteArrayInputStream(s.getBytes()); License license = new License(); license.setLicense(is); Document document = new Document(docPathInputStream); FileOutputStream outputStream = new FileOutputStream(pdfFile); document.save(outputStream, SaveFormat.PDF); long now = System.currentTimeMillis(); outputStream.close(); is.close(); System.out.println("WORD转化PDF共耗时：" + ((now - old) / 1000.0) + "秒"); //转化用时 } catch (Exception e) { System.out.println("转化失败"); e.printStackTrace(); } return pdfFile; } public static void main(String[] args) { String docPath = "F:\\Users\\test\\Desktop\\test.docx"; String pdfPath = "F:\\Users\\test\\Desktop\\test-convert.pdf"; docToPdf(docPath,pdfPath); } }之前的版本最近做的一个项目中需要对接上上签实现自动签章和骑缝章的功能，但是用户这边的合同有docx也有pdf，而上上签只能对接pdf的格式，于是找了很多方法，不是表格转换有问题，就是图片转换有问题，要么就是格式上有点失真，最后找到一个比较完美的方案，记录一下，后台是springboot，文件涉及到公司机密就没有效果展示了。<dependency> <groupId>com.luhuiguo</groupId> <artifactId>aspose-words</artifactId> <version>23.1</version> </dependency> <dependency> <groupId>org.apache.poi</groupId> <artifactId>poi</artifactId> <version>5.2.0</version> </dependency> <dependency> <groupId>org.apache.poi</groupId> <artifactId>poi-ooxml</artifactId> <version>5.2.0</version> </dependency> <dependency> <groupId>org.apache.poi</groupId> <artifactId>poi-scratchpad</artifactId> <version>5.2.0</version> </dependency> <dependency> <groupId>org.apache.poi</groupId> <artifactId>poi-excelant</artifactId> <version>5.2.0</version> </dependency> <dependency> <groupId>com.itextpdf</groupId> <artifactId>itextpdf</artifactId> <version>5.5.13.2</version> </dependency> <dependency> <groupId>com.itextpdf</groupId> <artifactId>itext-asian</artifactId> <version>5.2.0</version> </dependency> <dependency> <groupId>cn.hutool</groupId> <artifactId>hutool-all</artifactId> <version>5.7.5</version> </dependency>核心代码import cn.hutool.core.util.StrUtil; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.File; import java.io.FileInputStream; import java.io.IOException; /** * @author fhey * @date 2023-04-20 11:15:58 * @description: 文件工具类 */ public class FileUtil { private static final Logger logger = LoggerFactory.getLogger(FileUtil.class); //获取新文件的全路径 public static String getNewFileFullPath(String sourceFilePath, String destFilePath, String ext) { File destFile = new File(destFilePath); if (destFile.isFile()) { return destFilePath; } File sourceFile = new File(sourceFilePath); String sourceFileName = sourceFile.getName(); if (sourceFile.isFile()) { return destFilePath + File.separator + sourceFileName.substring(0, sourceFileName.lastIndexOf(StrUtil.DOT)) + StrUtil.DOT + ext; } return destFilePath + File.separator + sourceFileName + StrUtil.DOT + ext; } //判断文件是否是图片 public static boolean isImage(File file) throws IOException { FileInputStream is = new FileInputStream(file); byte[] bytes = new byte[8]; is.read(bytes); is.close(); String type = bytesToHexString(bytes).toUpperCase(); if (type.contains("FFD8FF") //JPEG(jpg) || type.contains("89504E47") //PNG || type.contains("47494638") //GIF || type.contains("49492A00") //TIFF(tif) || type.contains("424D") //Bitmap(bmp) ) { return true; } return false; } //将文件头转换成16进制字符串 public static String bytesToHexString(byte[] src) { StringBuilder builder = new StringBuilder(); if (src == null || src.length <= 0) { return null; } for (int i = 0; i < src.length; i++) { int v = src[i] & 0xFF; String hv = Integer.toHexString(v); if (hv.length() < 2) { builder.append(0); } builder.append(hv); } return builder.toString(); } }使用示例public static void wordToPdf(String wordPath, String pdfPath) throws Exception { pdfPath = FileUtil.getNewFileFullPath(wordPath, pdfPath, "pdf"); File file = new File(pdfPath); FileOutputStream os = new FileOutputStream(file); Document doc = new Document(wordPath); doc.save(os, com.aspose.words.SaveFormat.PDF); } public static void main(String[] args) throws Exception { String filepath = "test.docx"; String outpath = "/tmp"; wordToPdf(filepath, outpath); }