SpringBoot3+SpringSecurity6前后端分离的项目实践

 更新时间:2023年12月29日 10:11:29   作者:翰戈.summer  
SpringSecurity6 的用法和以前版本的有较大差别,本文主要介绍了SpringBoot3+SpringSecurity6前后端分离的项目实践,具有一定的参考价值,感兴趣的可以了解一下

网上能找到的SpringBoot项目一般都是SpringBoot2 + SpringSecurity5,甚至是SSM的项目。这些老版本的教程很多已经不适用了,对于现在大部分的初学者来说,学了可能也是经典白雪。我还是不愿学那些老版本的东西,所以自己摸索了一下新版的SpringBoot项目应该怎么写。学习的过程也是非常折磨人的,看了很多的教程才知道个大概。

导入依赖

SpringSecurity依赖

<!--SpringSecurity起步依赖-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>

JWT依赖

<!--jwt令牌-->
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt</artifactId>
    <version>0.9.1</version>
</dependency>

添加配置类

对Security进行配置,Security中很多的默认配置都可以用自定义的替换。

import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;

/**
 * @Description: SpringSecurity配置类
 * @Author: 翰戈.summer
 * @Date: 2023/11/17
 * @Param:
 * @Return:
 */
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig {

    private final UserDetailsService userDetailsService;

    /**
     * 加载用户信息
     */
    @Bean
    public UserDetailsService userDetailsService() {
        return userDetailsService;
    }

    /**
     * 密码编码器
     */
    @Bean
    public BCryptPasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    /**
     * 身份验证管理器
     */
    @Bean
    public AuthenticationManager authenticationManager(AuthenticationConfiguration configuration) throws Exception {
        return configuration.getAuthenticationManager();
    }

    /**
     * 处理身份验证
     */
    @Bean
    public AuthenticationProvider authenticationProvider() {
        DaoAuthenticationProvider daoAuthenticationProvider = new DaoAuthenticationProvider();
        daoAuthenticationProvider.setPasswordEncoder(passwordEncoder());
        daoAuthenticationProvider.setUserDetailsService(userDetailsService);
        return daoAuthenticationProvider;
    }

    /**
     * @Description: 配置SecurityFilterChain过滤器链
     * @Author: 翰戈.summer
     * @Date: 2023/11/17
     * @Param: HttpSecurity
     * @Return: SecurityFilterChain
     */
    @Bean
    public SecurityFilterChain defaultSecurityFilterChain(HttpSecurity httpSecurity) throws Exception {
        httpSecurity.authorizeHttpRequests(authorizeHttpRequests -> authorizeHttpRequests
                .requestMatchers(HttpMethod.POST, "/api/user/login").permitAll() //登录放行
                .anyRequest().authenticated()
        );
        httpSecurity.authenticationProvider(authenticationProvider());

        //禁用登录页面
        httpSecurity.formLogin(AbstractHttpConfigurer::disable);
        //禁用登出页面
        httpSecurity.logout(AbstractHttpConfigurer::disable);
        //禁用session
        httpSecurity.sessionManagement(AbstractHttpConfigurer::disable);
        //禁用httpBasic
        httpSecurity.httpBasic(AbstractHttpConfigurer::disable);
        //禁用csrf保护
        httpSecurity.csrf(AbstractHttpConfigurer::disable);

        return httpSecurity.build();
    }
}

实现UserDetailsService

其中UserMapper、AuthorityMapper需要自己创建,不是重点。这两个Mapper的作用是获取用户信息(用户名、密码、用户权限),封装到User中返回给Security。

import com.demo.mapper.AuthorityMapper;
import com.demo.mapper.UserMapper;
import com.demo.pojo.AuthorityEntity;
import com.demo.pojo.UserEntity;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;

import java.util.List;
import java.util.StringJoiner;

/**
 * @Description: 用户登录
 * @Author: 翰戈.summer
 * @Date: 2023/11/16
 * @Param:
 * @Return:
 */
@Service
@RequiredArgsConstructor
public class UserLoginDetailsServiceImpl implements UserDetailsService {

    private final UserMapper userMapper;
    private final AuthorityMapper authorityMapper;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        UserEntity userEntity = userMapper.selectUserByUsername(username);

        List<AuthorityEntity> authorities = authorityMapper.selectAuthorityByUsername(username);
        StringJoiner stringJoiner = new StringJoiner(",", "", "");

        authorities.forEach(authority -> stringJoiner.add(authority.getAuthorityName()));

        return new User(userEntity.getUsername(), userEntity.getPassword(),
                AuthorityUtils.commaSeparatedStringToAuthorityList(stringJoiner.toString())
        );
    }
}

实现UserDetails

登录操作会用到UserDetails,用于获取用户名和权限。

import lombok.AllArgsConstructor;
import lombok.NoArgsConstructor;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

import java.util.Collection;

/**
 * @Description: SpringSecurity用户实体类
 * @Author: 翰戈.summer
 * @Date: 2023/11/18
 * @Param:
 * @Return:
 */
@NoArgsConstructor
@AllArgsConstructor
public class UserDetailsEntity implements UserDetails {

    private String username;
    private String password;
    private Collection<? extends GrantedAuthority> authorities;

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return authorities;
    }

    @Override
    public String getPassword() {
        return password;
    }

    @Override
    public String getUsername() {
        return username;
    }

    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    @Override
    public boolean isEnabled() {
        return true;
    }

    @Override
    public String toString() {
        return "UserDetailsEntity{" +
                "username='" + username + '\'' +
                ", password='" + password + '\'' +
                ", authorities=" + authorities +
                '}';
    }
}

JWT工具类 

生成 jwt令牌 或解析,其中的JwtProperties(jwt令牌配置属性类)可以自己创建,不是重点。

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;

import java.util.Date;
import java.util.Map;

/**
 * @Description: 生成和解析jwt令牌
 * @Author: 翰戈.summer
 * @Date: 2023/11/16
 * @Param:
 * @Return:
 */
@Component
@RequiredArgsConstructor
public class JwtUtils {

    private final JwtProperties jwtProperties;

    /**
     * @Description: 生成令牌
     * @Author: 翰戈.summer
     * @Date: 2023/11/16
     * @Param: Map
     * @Return: String jwt
     */
    public String getJwt(Map<String, Object> claims) {

        String signingKey = jwtProperties.getSigningKey();
        Long expire = jwtProperties.getExpire();

        return Jwts.builder()
                .setClaims(claims) //设置载荷内容
                .signWith(SignatureAlgorithm.HS256, signingKey) //设置签名算法
                .setExpiration(new Date(System.currentTimeMillis() + expire)) //设置有效时间
                .compact();
    }

    /**
     * @Description: 解析令牌
     * @Author: 翰戈.summer
     * @Date: 2023/11/16
     * @Param: String jwt
     * @Return: Claims claims
     */
    public Claims parseJwt(String jwt) {

        String signingKey = jwtProperties.getSigningKey();

        return Jwts.parser()
                .setSigningKey(signingKey) //指定签名密钥
                .parseClaimsJws(jwt) //开始解析令牌
                .getBody();
    }
}

登录接口

用户登录成功并返回 jwt令牌,Result为统一响应的结果,UserLoginDTO用于封装用户登录信息,其中的UserDetails必须实现后才能获取到用户信息。

import lombok.RequiredArgsConstructor;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.Collection;
import java.util.HashMap;
import java.util.Map;

/**
 * @Description: 用户登录操作相关接口
 * @Author: 翰戈.summer
 * @Date: 2023/11/20
 * @Param:
 * @Return:
 */
@RestController
@RequestMapping("/api/user/login")
@RequiredArgsConstructor
public class UserLoginController {

    private final AuthenticationManager authenticationManager;

    private final JwtUtils jwtUtils;

    @PostMapping
    public Result<String> doLogin(@RequestBody UserLoginDTO userLoginDTO) {
        try {
            UsernamePasswordAuthenticationToken auth = new UsernamePasswordAuthenticationToken(userLoginDTO.getUsername(), userLoginDTO.getPassword());
            Authentication authentication = authenticationManager.authenticate(auth);
            SecurityContextHolder.getContext().setAuthentication(authentication);
            UserDetails userDetails = (UserDetails) authentication.getPrincipal();

            //获取用户权限信息
            String authorityString = "";
            Collection<? extends GrantedAuthority> authorities = userDetails.getAuthorities();
            for (GrantedAuthority authority : authorities) {
                authorityString = authority.getAuthority();
            }

            //用户身份验证成功,生成并返回jwt令牌
            Map<String, Object> claims = new HashMap<>();
            claims.put("username", userDetails.getUsername());
            claims.put("authorityString", authorityString);
            String jwtToken = jwtUtils.getJwt(claims);
            return Result.success(jwtToken);
        } catch (Exception ex) {
            //用户身份验证失败,返回登陆失败提示
            return Result.error("用户名或密码错误!");
        }
    }
}

自定义token过滤器

过滤器中抛出的异常是不会被全局异常处理器捕获到的,直接返回错误结果。这里用到了SpringContextUtils通过上下文来获取Bean组件,下面会提供。

过滤器属于Servlet(作用范围更大),拦截器属于SpringMVC(作用范围较小),全局异常处理器只能捕获到拦截器中的异常。在过滤器中无法初始化Bean组件,可以通过上下文来获取。

import com.fasterxml.jackson.databind.ObjectMapper;
import io.jsonwebtoken.Claims;
import jakarta.servlet.FilterChain;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;
import org.springframework.util.StringUtils;

import java.io.IOException;
import java.util.Collections;

/**
 * @Description: 自定义token验证过滤器,验证成功后将用户信息放入SecurityContext上下文
 * @Author: 翰戈.summer
 * @Date: 2023/11/18
 * @Param:
 * @Return:
 */
public class JwtAuthenticationFilter extends BasicAuthenticationFilter {

    public JwtAuthenticationFilter(AuthenticationManager authenticationManager) {
        super(authenticationManager);
    }

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws IOException {
        try {
            //获取请求头中的token
            String jwtToken = request.getHeader("token");
            if (!StringUtils.hasLength(jwtToken)) {
                //token不存在,交给其他过滤器处理
                filterChain.doFilter(request, response);
                return; //结束方法
            }

            //过滤器中无法初始化Bean组件,使用上下文获取
            JwtUtils jwtUtils = SpringContextUtils.getBean("jwtUtils");
            if (jwtUtils == null) {
                throw new RuntimeException();
            }

            //解析jwt令牌
            Claims claims;
            try {
                claims = jwtUtils.parseJwt(jwtToken);
            } catch (Exception ex) {
                throw new RuntimeException();
            }

            //获取用户信息
            String username = (String) claims.get("username"); //用户名
            String authorityString = (String) claims.get("authorityString"); //权限信息

            Authentication authentication = new UsernamePasswordAuthenticationToken(
                    username, null,
                    Collections.singleton(new SimpleGrantedAuthority(authorityString))
            );

            //将用户信息放入SecurityContext上下文
            SecurityContextHolder.getContext().setAuthentication(authentication);

            filterChain.doFilter(request, response);
        } catch (Exception ex) {
            //过滤器中抛出的异常无法被全局异常处理器捕获,直接返回错误结果
            response.setCharacterEncoding("utf-8");
            response.setContentType("application/json; charset=utf-8");
            String value = new ObjectMapper().writeValueAsString(Result.error("用户未登录!"));
            response.getWriter().write(value);
        }
    }
}

SpringContextUtils工具类

import jakarta.annotation.Nonnull;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;

/**
 * @Description: 用于创建上下文,实现ApplicationContextAware接口
 * @Author: 翰戈.summer
 * @Date: 2023/11/17
 * @Param:
 * @Return:
 */
@Component
public class SpringContextUtils implements ApplicationContextAware {

    private static ApplicationContext applicationContext;

    public static ApplicationContext getApplicationContext() {
        return applicationContext;
    }

    @Override
    public void setApplicationContext(@Nonnull ApplicationContext applicationContext) throws BeansException {
        SpringContextUtils.applicationContext = applicationContext;
    }

    @SuppressWarnings("unchecked")
    public static <T> T getBean(String name) throws BeansException {
        if (applicationContext == null) {
            return null;
        }
        return (T) applicationContext.getBean(name);
    }
}

添加自定义token验证过滤器

将自定义token验证过滤器,添加到UsernamePasswordAuthenticationFilter前面。

UsernamePasswordAuthenticationFilter实现了基于用户名和密码的认证逻辑,我们利用token进行身份验证,所以用不到这个过滤器。

    /**
     * @Description: 配置SecurityFilterChain过滤器链
     * @Author: 翰戈.summer
     * @Date: 2023/11/17
     * @Param: HttpSecurity
     * @Return: SecurityFilterChain
     */
    @Bean
    public SecurityFilterChain defaultSecurityFilterChain(HttpSecurity httpSecurity) throws Exception {
        httpSecurity.authorizeHttpRequests(authorizeHttpRequests -> authorizeHttpRequests
                .requestMatchers(HttpMethod.POST, "/api/user/login").permitAll() //登录放行
                .anyRequest().authenticated()
        );
        httpSecurity.authenticationProvider(authenticationProvider());

        //禁用登录页面
        httpSecurity.formLogin(AbstractHttpConfigurer::disable);
        //禁用登出页面
        httpSecurity.logout(AbstractHttpConfigurer::disable);
        //禁用session
        httpSecurity.sessionManagement(AbstractHttpConfigurer::disable);
        //禁用httpBasic
        httpSecurity.httpBasic(AbstractHttpConfigurer::disable);
        //禁用csrf保护
        httpSecurity.csrf(AbstractHttpConfigurer::disable);

        //通过上下文获取AuthenticationManager
        AuthenticationManager authenticationManager = SpringContextUtils.getBean("authenticationManager");
        //添加自定义token验证过滤器
        httpSecurity.addFilterBefore(new JwtAuthenticationFilter(authenticationManager), UsernamePasswordAuthenticationFilter.class);

        return httpSecurity.build();
    }

自定义用户未登录的处理

用户请求未携带token的处理,替换AuthenticationEntryPoint

import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;

import java.io.IOException;

/**
 * @Description: 自定义用户未登录的处理(未携带token)
 * @Author: 翰戈.summer
 * @Date: 2023/11/19
 * @Param:
 * @Return:
 */
@Component
public class AuthEntryPointHandler implements AuthenticationEntryPoint {

    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException {
        response.setCharacterEncoding("utf-8");
        response.setContentType("application/json; charset=utf-8");
        String value = new ObjectMapper().writeValueAsString(Result.error("未携带token!"));
        response.getWriter().write(value);
    }
}

自定义用户权限不足的处理

用户权限不足的处理,替换AccessDeniedHandler

import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.stereotype.Component;

import java.io.IOException;

/**
 * @Description: 自定义用户权限不足的处理
 * @Author: 翰戈.summer
 * @Date: 2023/11/19
 * @Param:
 * @Return:
 */
@Component
public class AuthAccessDeniedHandler implements AccessDeniedHandler {

    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException {
        response.setCharacterEncoding("utf-8");
        response.setContentType("application/json; charset=utf-8");
        String value = new ObjectMapper().writeValueAsString(Result.error("权限不足!"));
        response.getWriter().write(value);
    }
}

添加自定义处理器

修改 SecurityConfig 配置类,注入 AuthAccessDeniedHandler 和 AuthEntryPointHandler

    /**
     * @Description: 配置SecurityFilterChain过滤器链
     * @Author: 翰戈.summer
     * @Date: 2023/11/17
     * @Param: HttpSecurity
     * @Return: SecurityFilterChain
     */
    @Bean
    public SecurityFilterChain defaultSecurityFilterChain(HttpSecurity httpSecurity) throws Exception {
        httpSecurity.authorizeHttpRequests(authorizeHttpRequests -> authorizeHttpRequests
                .requestMatchers(HttpMethod.POST, "/api/user/login").permitAll() //登录放行
                .anyRequest().authenticated()
        );
        httpSecurity.authenticationProvider(authenticationProvider());

        //禁用登录页面
        httpSecurity.formLogin(AbstractHttpConfigurer::disable);
        //禁用登出页面
        httpSecurity.logout(AbstractHttpConfigurer::disable);
        //禁用session
        httpSecurity.sessionManagement(AbstractHttpConfigurer::disable);
        //禁用httpBasic
        httpSecurity.httpBasic(AbstractHttpConfigurer::disable);
        //禁用csrf保护
        httpSecurity.csrf(AbstractHttpConfigurer::disable);

        //通过上下文获取AuthenticationManager
        AuthenticationManager authenticationManager = SpringContextUtils.getBean("authenticationManager");
        //添加自定义token验证过滤器
        httpSecurity.addFilterBefore(new JwtAuthenticationFilter(authenticationManager), UsernamePasswordAuthenticationFilter.class);

        //自定义处理器
        httpSecurity.exceptionHandling(exceptionHandling -> exceptionHandling
                .accessDeniedHandler(authAccessDeniedHandler) //处理用户权限不足
                .authenticationEntryPoint(authEntryPointHandler) //处理用户未登录(未携带token)
        );

        return httpSecurity.build();
    }

静态资源放行

SpringBoot3 中使用 Swagger3 接口文档,在整合了 SpringSecurity 后会出现无法访问的情况,需要给静态资源放行。

在 SecurityConfig 中添加

    /**
     * 静态资源放行
     */
    @Bean
    public WebSecurityCustomizer webSecurityCustomizer() {
        return (web) -> web.ignoring().requestMatchers(
                "/doc.html",
                "/doc.html/**",
                "/v3/api-docs",
                "/v3/api-docs/**",
                "/webjars/**",
                "/authenticate",
                "/swagger-ui.html/**",
                "/swagger-resources",
                "/swagger-resources/**"
        );
    }

总结

SpringSecurity6 的用法和以前版本的有较大差别,比如WebSecurityConfigurerAdapter的废除,看到配置类继承了这个的都是过时的教程。因为不再继承,所以不能通过重写方法的方式去配置。另外很多配置的方式都变成使用Lambda表达式,或者是方法引用。

到此这篇关于SpringBoot3+SpringSecurity6前后端分离的项目实践的文章就介绍到这了,更多相关SpringBoot3+SpringSecurity6前后端分离内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • Springboot打包为Docker镜像并部署的实现

    Springboot打包为Docker镜像并部署的实现

    这篇文章主要介绍了Springboot打包为Docker镜像并部署的实现,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2020-12-12
  • 详解SpringMVC @RequestBody接收Json对象字符串

    详解SpringMVC @RequestBody接收Json对象字符串

    这篇文章主要介绍了详解SpringMVC @RequestBody接收Json对象字符串,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧
    2017-01-01
  • SpringBoot项目 文件上传临时目标被删除异常的处理方案

    SpringBoot项目 文件上传临时目标被删除异常的处理方案

    这篇文章主要介绍了SpringBoot项目 文件上传临时目标被删除异常的处理方案,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2021-07-07
  • springboot+angular4前后端分离 跨域问题解决详解

    springboot+angular4前后端分离 跨域问题解决详解

    这篇文章主要介绍了springboot+angular4前后端分离 跨域问题解决详解,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下
    2019-09-09
  • 员工管理系统java版

    员工管理系统java版

    这篇文章主要为大家详细介绍了java版的员工管理系统,,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2017-11-11
  • 用Java实现聊天程序

    用Java实现聊天程序

    这篇文章主要为大家详细介绍了用Java实现聊天程序,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2022-09-09
  • Hadoop之Mapreduce序列化

    Hadoop之Mapreduce序列化

    本文主要带我们了解Mapreduce序列化,序列化就是把内存中的对象,转换成字节序列(或其他数据传输协议)以便于存储到磁盘(持久化)和网络传输。想进一步了解更多的小伙伴,可以参考阅读本文
    2023-03-03
  • Java Mybatis架构设计深入了解

    Java Mybatis架构设计深入了解

    在本篇文章里小编给大家整理的是一篇关于Java Mybatis架构设计详解内容,对此有兴趣的朋友们可以参考下,希望能够给你带来帮助
    2021-11-11
  • 基于Java实现简单贪吃蛇游戏

    基于Java实现简单贪吃蛇游戏

    这篇文章主要为大家详细介绍了基于Java实现简单贪吃蛇,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2021-04-04
  • Java中Arrays.asList()方法详解及实例

    Java中Arrays.asList()方法详解及实例

    这篇文章主要介绍了Java中Arrays.asList()方法将数组作为列表时的一些差异的相关资料,需要的朋友可以参考下
    2017-06-06

最新评论