热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

SpringBoot整合SpringSecurity简单实现登入登出从零搭建教程

这篇文章主要给大家介绍了关于SpringBoot整合SpringSecurity简单实现登入登出从零搭建的相关资料,文中通过示例代码介绍的非常详细,对大家的学习或

这篇文章主要给大家介绍了关于Spring Boot整合Spring Security简单实现登入登出从零搭建的相关资料,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面来一起看看吧

前言

Spring Security是一个能够为基于Spring的企业应用系统提供声明式的安全访问控制解决方案的安全框架。它提供了一组可以在Spring应用上下文中配置的Bean,充分利用了Spring IoC,DI(控制反转Inversion of Control ,DI:Dependency Injection 依赖注入)和AOP(面向切面编程)功能,为应用系统提供声明式的安全访问控制功能,减少了为企业系统安全控制编写大量重复代码的工作。

本文主要给大家介绍了关于Spring Boot整合Spring Security实现登入登出的相关内容,下面话不多说了,来一起看看详细的介绍吧

技术栈 : SpringBoot + SpringSecurity + jpa + freemark ,完整项目地址 : https://github.com/EalenXie/spring-security-login  (本地下载)

方法如下:

1 . 新建一个spring-security-login的maven项目 ,pom.xml添加基本依赖 :

   4.0.0com.wuxicloudspring-security-login1.0 org.springframework.bootspring-boot-starter-parent1.5.6.RELEASE EalenXieSpringBoot整合SpringSecurity实现简单登入登出  org.springframework.bootspring-boot-starter org.springframework.bootspring-boot-starter-web org.springframework.bootspring-boot-starter-data-jpa org.springframework.bootspring-boot-starter-test org.springframework.bootspring-boot-starter-security org.springframework.bootspring-boot-starter-freemarker org.springframework.bootspring-boot-starter-aop com.alibabadruid1.0.24 com.alibabafastjson1.2.31 mysqlmysql-connector-javaruntime

2 . 准备你的数据库,设计表结构,要用户使用登入登出,新建用户表。

 DROP TABLE IF EXISTS `user`; CREATE TABLE `user` ( `id` int(11) NOT NULL AUTO_INCREMENT, `user_uuid` varchar(70) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL, `username` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL, `password` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL, `email` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL, `telephone` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL, `role` int(10) DEFAULT NULL, `image` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL, `last_ip` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL, `last_time` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL, PRIMARY KEY (`id`) USING BTREE ) ENGINE = InnoDB AUTO_INCREMENT = 2 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Compact; SET FOREIGN_KEY_CHECKS = 1;

3 . 用户对象User.java :

 import javax.persistence.*; /** * Created by EalenXie on 2018/7/5 15:17 */ @Entity @Table(name = "USER") public class User { @Id @GeneratedValue(strategy = GenerationType.AUTO) private Integer id; private String user_uuid; //用户UUID private String username; //用户名 private String password; //用户密码 private String email; //用户邮箱 private String telephone; //电话号码 private String role; //用户角色 private String image; //用户头像 private String last_ip; //上次登录IP private String last_time; //上次登录时间 public Integer getId() { return id; } public String getRole() { return role; } public void setRole(String role) { this.role = role; } public String getImage() { return image; } public void setImage(String image) { this.image = image; } public void setId(Integer id) { this.id = id; } public String getUsername() { return username; } public void setUsername(String username) { this.username = username; } public String getEmail() { return email; } public void setEmail(String email) { this.email = email; } public String getTelephone() { return telephone; } public void setTelephone(String telephone) { this.telephOne= telephone; } public String getPassword() { return password; } public void setPassword(String password) { this.password = password; } public String getUser_uuid() { return user_uuid; } public void setUser_uuid(String user_uuid) { this.user_uuid = user_uuid; } public String getLast_ip() { return last_ip; } public void setLast_ip(String last_ip) { this.last_ip = last_ip; } public String getLast_time() { return last_time; } public void setLast_time(String last_time) { this.last_time = last_time; } @Override public String toString() { return "User{" + "id=" + id + ", user_uuid='" + user_uuid + '\'' + ", username='" + username + '\'' + ", password='" + password + '\'' + ", email='" + email + '\'' + ", telephOne='" + telephone + '\'' + ", role='" + role + '\'' + ", image='" + image + '\'' + ", last_ip='" + last_ip + '\'' + ", last_time='" + last_time + '\'' + '}'; } }

 4 . application.yml配置一些基本属性

 spring: resources: static-locations: classpath:/ freemarker: template-loader-path: classpath:/templates/ suffix: .html content-type: text/html charset: UTF-8 datasource: url: jdbc:mysql://localhost:3306/yourdatabase username: yourname password: yourpass driver-class-name: com.mysql.jdbc.Driver type: com.alibaba.druid.pool.DruidDataSource server: port: 8083 error: whitelabel: enabled: true

5 . 考虑我们应用的效率 , 可以配置数据源和线程池 :

 package com.wuxicloud.config; import com.alibaba.druid.pool.DruidDataSource; import com.alibaba.druid.pool.DruidDataSourceFactory; import com.alibaba.druid.support.http.StatViewServlet; import com.alibaba.druid.support.http.WebStatFilter; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.boot.web.servlet.FilterRegistrationBean; import org.springframework.boot.web.servlet.ServletRegistrationBean; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.env.*; import javax.sql.DataSource; import java.util.HashMap; import java.util.Map; import java.util.Properties; @Configuration public class DruidConfig { private static final String DB_PREFIX = "spring.datasource."; @Autowired private Environment environment; @Bean @ConfigurationProperties(prefix = DB_PREFIX) public DataSource druidDataSource() { Properties dbProperties = new Properties(); Map map = new HashMap(); for (PropertySource propertySource : ((AbstractEnvironment) environment).getPropertySources()) { getPropertiesFromSource(propertySource, map); } dbProperties.putAll(map); DruidDataSource dds; try { dds = (DruidDataSource) DruidDataSourceFactory.createDataSource(dbProperties); dds.init(); } catch (Exception e) { throw new RuntimeException("load datasource error, dbProperties is :" + dbProperties, e); } return dds; } private void getPropertiesFromSource(PropertySource propertySource, Map map) { if (propertySource instanceof MapPropertySource) { for (String key : ((MapPropertySource) propertySource).getPropertyNames()) { if (key.startsWith(DB_PREFIX)) map.put(key.replaceFirst(DB_PREFIX, ""), propertySource.getProperty(key)); else if (key.startsWith(DB_PREFIX)) map.put(key.replaceFirst(DB_PREFIX, ""), propertySource.getProperty(key)); } } if (propertySource instanceof CompositePropertySource) { for (Prop[email protected]#码网ertySource s : ((CompositePropertySource) propertySource).getPropertySources()) { getPropertiesFromSource(s, map); } } } @Bean public ServletRegistrationBean druidServlet() { return new ServletRegistrationBean(new StatViewServlet(), "/druid/*"); } @Bean public FilterRegistrationBean filterRegistrationBean() { FilterRegistrationBean filterRegistratiOnBean= new FilterRegistrationBean(); filterRegistrationBean.setFilter(new WebStatFilter()); filterRegistrationBean.addUrlPatterns("/*"); filterRegistrationBean.addInitParameter("exclusions", "*.js,*.gif,*.jpg-600,*.png-600,*.css,*.ico,/druid/*"); return filterRegistrationBean; } }

配置线程池 :

 package com.wuxicloud.config; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.scheduling.annotation.EnableAsync; import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; import java.util.concurrent.Executor; import java.util.concurrent.ThreadPoolExecutor; @Configuration @EnableAsync public class ThreadPoolConfig { @Bean public Executor getExecutor() { ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); executor.setCorePoolSize(5);//线程池维护线程的最少数量 executor.setMaxPoolSize(30);//线程池维护线程的最大数量 executor.setQueueCapacity(8); //缓存队列 executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy()); //对拒绝task的处理策略 executor.setKeepAliveSeconds(60);//允许的空闲时间 executor.initialize(); return executor; } }

6.用户需要根据用户名进行登录,访问数据库 :

 import com.wuxicloud.model.User; import org.springframework.data.jpa.repository.JpaRepository; /** * Created by EalenXie on 2018/7/11 14:23 */ public interface UserRepository extends JpaRepository { User findByUsername(String username); }

7.构建真正用于SpringSecurity登录的安全用户(UserDetails),我这里使用新建了一个POJO来实现 :

 package com.wuxicloud.security; import com.wuxicloud.model.User; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.userdetails.UserDetails; import java.util.ArrayList; import java.util.Collection; public class SecurityUser extends User implements UserDetails { private static final long serialVersiOnUID= 1L; public SecurityUser(User user) { if (user != null) { this.setUser_uuid(user.getUser_uuid()); this.setUsername(user.getUsername()); this.setPassword(user.getPassword()); this.setEmail(user.getEmail()); this.setTelephone(user.getTelephone()); this.setRole(user.getRole()); this.setImage(user.getImage()); this.setLast_ip(user.getLast_ip()); this.setLast_time(user.getLast_time()); } } @Override public Collection getAuthorities() { Collection authorities = new ArrayList(); String username = this.getUsername(); if (username != null) { SimpleGrantedAuthority authority = new SimpleGrantedAuthority(username); authorities.add(authority); } return authorities; } @Override public boolean isAccountNonExpired() { return true; } @Override public boolean isAccountNonLocked() { return true; } @Override public boolean isCredentialsNonExpired() { return true; } @Override public boolean isEnabled() { return true; } }

8 . 核心配置,配置SpringSecurity访问策略,包括登录处理,登出处理,资源访问,密码基本加密。

 package com.wuxicloud.config; import com.wuxicloud.dao.UserRepository; import com.wuxicloud.model.User; import com.wuxicloud.security.SecurityUser; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; 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.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.core.Authentication; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler; import org.springframework.security.web.authentication.logout.LogoutSuccessHandler; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; /** * Created by EalenXie on 2018/1/11. */ @Configuration @EnableWebSecurity public class WebSecurityConfig extends WebSecurityConfigurerAdapter { private static final Logger logger = LoggerFactory.getLogger(WebSecurityConfig.class); @Override protected void configure(HttpSecurity http) throws Exception { //配置策略 http.csrf().disable(); http.authorizeRequests(). antMatchers("/static/**").permitAll().anyRequest().authenticated(). and().formLogin().loginPage("/login").permitAll().successHandler(loginSuccessHandler()). and().logout().permitAll().invalidateHttpSession(true). deleteCOOKIEs("JSESSIONID").logoutSuccessHandler(logoutSuccessHandler()). and().sessionManagement().maximumSessions(10).expiredUrl("/login"); } @Autowired public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(userDetailsService()).passwordEncoder(passwordEncoder()); auth.eraseCredentials(false); } @Bean public BCryptPasswordEncoder passwordEncoder() { //密码加密 return new BCryptPasswordEncoder(4); } @Bean public LogoutSuccessHandler logoutSuccessHandler() { //登出处理 return new LogoutSuccessHandler() { @Override public void onLogoutSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException { try { SecurityUser user = (SecurityUser) authentication.getPrincipal(); logger.info("USER : " + user.getUsername() + " LOGOUT SUCCESS ! "); } catch (Exception e) { logger.info("LOGOUT EXCEPTION , e : " + e.getMessage()); } httpServletResponse.sendRedirect("/login"); } }; } @Bean public SavedRequestAwareAuthenticationSuccessHandler loginSuccessHandler() { //登入处理 return new SavedRequestAwareAuthenticationSuccessHandler() { @Override public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException { User userDetails = (User) authentication.getPrincipal(); logger.info("USER : " + userDetails.getUsername() + " LOGIN SUCCESS ! "); super.onAuthenticationSuccess(request, response, authentication); } }; } @Bean public UserDetailsService userDetailsService() { //用户登录实现 return new UserDetailsService() { @Autowired private UserRepository userRepository; @Override public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException { User user = userRepository.findByUsername(s); if (user == null) throw new UsernameNotFoundException("Username " + s + " not found"); return new SecurityUser(user); } }; } }

9.至此,已经基本将配置搭建好了,从上面核心可以看出,配置的登录页的url 为/login,可以创建基本的Controller来验证登录了。

 package com.wuxicloud.web; import com.wuxicloud.model.User; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContext; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.context.request.RequestContextHolder; import org.springframework.web.context.request.ServletRequestAttributes; import javax.servlet.http.HttpServletRequest; /** * Created by EalenXie on 2018/1/11. */ @Controller public class LoginController { @RequestMapping(value = "/login", method = RequestMethod.GET) public String login() { return "login"; } @RequestMapping("/") public String root() { return "index"; } public User getUser() { //为了session从获取用户信息,可以配置如下 User user = new User(); SecurityContext ctx = SecurityContextHolder.getContext(); Authentication auth = ctx.getAuthentication(); if (auth.getPrincipal() instanceof UserDetails) user = (User) auth.getPrincipal(); return user; } public HttpServletRequest getRequest() { return ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest(); } }

11 . SpringBoot基本的启动类 Application.class

 package com.wuxicloud; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; /** * Created by EalenXie on 2018/7/11 15:01 */ @SpringBootApplication public class Application { public static void main(String[] args) { SpringApplication.run(Application.class, args); } }

11.根据Freemark和Controller里面可看出配置的视图为 /templates/index.html和/templates/index.login。所以创建基本的登录页面和登录成功页面。

login.html

     用户名 :  密码 : 

注意 : 这里方法必须是POST,因为GET在controller被重写了,用户名的name属性必须是username,密码的name属性必须是password

index.html

    欢迎你,${user.username}
注销

注意 : 为了从session中获取到登录的用户信息,根据配置SpringSecurity的用户信息会放在Session.SPRING_SECURITY_CONTEXT.authentication.principal里面,根据FreeMarker模板引擎的特点,可以通过这种方式进行获取 :

12 . 为了方便测试,我们在数据库中插入一条记录,注意,从WebSecurity.java配置可以知道密码会被加密,所以我们插入的用户密码应该是被加密的。

这里假如我们使用的密码为admin,则加密过后的字符串是 $2a$04$1OiUa3yEchBXQBJI8JaMyuKZNlwzWvfeQjKAHnwAEQwnacjt6ukqu

 测试类如下 :

 package com.wuxicloud.security; import org.junit.Test; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; /** * Created by EalenXie on 2018/7/11 15:13 */ public class TestEncoder { @Test public void encoder() { String password = "admin"; BCryptPasswordEncoder encoder = new BCryptPasswordEncoder(4); String enPassword = encoder.encode(password); System.out.println(enPassword); } }

测试登录,从上面的加密的密码我们插入一条数据到数据库中。

 INSERT INTO `USER` VALUES (1, 'd242ae49-4734-411e-8c8d-d2b09e87c3c8', 'EalenXie', '$2a$04$petEXpgcLKfdLN4TYFxK0u8ryAzmZDHLASWLX/XXm8hgQar1C892W', 'SSSSS', 'ssssssssss', 1, 'g', '0:0:0:0:0:0:0:1', '2018-07-11 11:26:27');

13 . 启动项目进行测试 ,访问 localhost:8083

 

点击登录,登录失败会留在当前页面重新登录,成功则进入index.html

 登录如果成功,可以看到后台打印登录成功的日志 :

 页面进入index.html :

点击注销 ,则回重新跳转到login.html,后台也会打印登出成功的日志 :

总结

以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,如果有疑问大家可以留言交流,谢谢大家对gaodaima编程笔记的支持。

以上就是Spring Boot整合Spring Security简单实现登入登出从零搭建教程的详细内容,更多请关注gaodaima编程笔记其它相关文章!



推荐阅读
  • 在基于.NET框架的分层架构实践中,为了实现各层之间的松散耦合,本文详细探讨了依赖注入(DI)和控制反转(IoC)容器的设计与实现。通过合理的依赖管理和对象创建,确保了各层之间的单向调用关系,从而提高了系统的可维护性和扩展性。此外,文章还介绍了几种常见的IoC容器实现方式及其应用场景,为开发者提供了实用的参考。 ... [详细]
  • 本文深入解析了Java面向对象编程的核心概念及其应用,重点探讨了面向对象的三大特性:封装、继承和多态。封装确保了数据的安全性和代码的可维护性;继承支持代码的重用和扩展;多态则增强了程序的灵活性和可扩展性。通过具体示例,文章详细阐述了这些特性在实际开发中的应用和优势。 ... [详细]
  • ButterKnife 是一款用于 Android 开发的注解库,主要用于简化视图和事件绑定。本文详细介绍了 ButterKnife 的基础用法,包括如何通过注解实现字段和方法的绑定,以及在实际项目中的应用示例。此外,文章还提到了截至 2016 年 4 月 29 日,ButterKnife 的最新版本为 8.0.1,为开发者提供了最新的功能和性能优化。 ... [详细]
  • Spring框架的核心组件与架构解析 ... [详细]
  • 本文详细介绍了在 Vue.js 前端框架中集成 vue-i18n 插件以实现多语言支持的方法。通过具体的配置步骤和示例代码,帮助开发者快速掌握如何在项目中实现国际化功能,提升用户体验。同时,文章还探讨了常见的多语言切换问题及解决方案,为开发人员提供了实用的参考。 ... [详细]
  • 如何撰写PHP电商项目的实战经验? ... [详细]
  • 近年来,BPM(业务流程管理)系统在国内市场逐渐普及,多家厂商在这一领域崭露头角。本文将对当前主要的BPM厂商进行概述,并分析其各自的优势。目前,市场上较为成熟的BPM产品主要分为两类:一类是综合型厂商,如IBM和SAP,这些企业在整体解决方案方面具有明显优势;另一类则是专注于BPM领域的专业厂商,它们在特定行业或应用场景中表现出色。通过对比分析,本文旨在为企业选择合适的BPM系统提供参考。 ... [详细]
  • AppFog 是一个基于 CloudFoundry 的多语言 PaaS(平台即服务)提供商,允许用户在其平台上轻松构建和部署 Web 应用程序。本文将通过详细的图文步骤,指导读者如何在 AppFog 免费云平台上成功部署 WordPress,帮助用户快速搭建个人博客或网站。 ... [详细]
  • 在CentOS上部署和配置FreeSWITCH
    在CentOS系统上部署和配置FreeSWITCH的过程涉及多个步骤。本文详细介绍了从源代码安装FreeSWITCH的方法,包括必要的依赖项安装、编译和配置过程。此外,还提供了常见的配置选项和故障排除技巧,帮助用户顺利完成部署并确保系统的稳定运行。 ... [详细]
  • 本文作为“实现简易版Spring系列”的第五篇,继前文深入探讨了Spring框架的核心技术之一——控制反转(IoC)之后,将重点转向另一个关键技术——面向切面编程(AOP)。对于使用Spring框架进行开发的开发者来说,AOP是一个不可或缺的概念。了解AOP的背景及其基本原理,对于掌握这一技术至关重要。本文将通过具体示例,详细解析AOP的实现机制,帮助读者更好地理解和应用这一技术。 ... [详细]
  • 掌握PHP框架开发与应用的核心知识点:构建高效PHP框架所需的技术与能力综述
    掌握PHP框架开发与应用的核心知识点对于构建高效PHP框架至关重要。本文综述了开发PHP框架所需的关键技术和能力,包括但不限于对PHP语言的深入理解、设计模式的应用、数据库操作、安全性措施以及性能优化等方面。对于初学者而言,熟悉主流框架如Laravel、Symfony等的实际应用场景,有助于更好地理解和掌握自定义框架开发的精髓。 ... [详细]
  • ### 摘要`mkdir` 命令用于在指定位置创建新的目录。其基本格式为 `mkdir [选项] 目录名称`。通过该命令,用户可以在文件系统中创建一个或多个以指定名称命名的文件夹。执行此操作的用户需要具备相应的权限。此外,`mkdir` 还支持多种选项,如 `-p` 用于递归创建多级目录,确保路径中的所有层级都存在。掌握这些基本用法和选项,有助于提高在 Linux 系统中的文件管理效率。 ... [详细]
  • K3Cloud 平台字符串解密技术详解与应用
    在 K3Cloud 平台中,配置文件内的敏感信息如密码会被加密处理。通过深入研究,我们发现可以通过 Kingdee.BOS.Api 提供的接口对这些加密字符串进行解密。本文详细介绍了这一解密技术的具体实现方法及其应用场景,为开发者提供了宝贵的参考和实践指导。此外,还探讨了该技术在数据安全和系统管理中的重要性,以及如何在实际项目中高效地应用这些技术,确保系统的稳定性和安全性。 ... [详细]
  • 深入解析 OpenCV 2 中 Mat 对象的类型、深度与步长属性
    在OpenCV 2中,`Mat`类作为核心组件,对于图像处理至关重要。本文将深入探讨`Mat`对象的类型、深度与步长属性,这些属性是理解和优化图像操作的基础。通过具体示例,我们将展示如何利用这些属性实现高效的图像缩小功能。此外,还将讨论这些属性在实际应用中的重要性和常见误区,帮助读者更好地掌握`Mat`类的使用方法。 ... [详细]
  • 本文推荐了六款高效的Java Web应用开发工具,并详细介绍了它们的实用功能。其中,分布式敏捷开发系统架构“zheng”项目,基于Spring、Spring MVC和MyBatis技术栈,提供了完整的分布式敏捷开发解决方案,支持快速构建高性能的企业级应用。此外,该工具还集成了多种中间件和服务,进一步提升了开发效率和系统的可维护性。 ... [详细]
author-avatar
wy
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有