巅峰对决!Spring Boot VS .NET 6

Spring Boot 和 ASP.NET Core 都是企业中流行的 Web 框架, 对于喜欢 C# 的人会使用 ASP.NET Core, 而对于 Java 或 Kotlin 等基于 JVM 的语言,Spring Boot 是最受欢迎的。

这本文中,会对比这两个框架在以下方面有何不同:

•控制器•模型绑定和验证•异常处理•数据访问•依赖注入•认证与授权•性能


基础项目

这是一个有关订单的基础项目, 非常简单的后端 api, 客户可以创建一个订单来购买一个或多个产品, 我使用了 MySQL 作为数据库,下面是实体关系图。

264b86c273cc39321d44557887743301.png

这里使用的框架版本分别是, Spring Boot (v2.5.5) 和 .NET 6, 让我们开始对比吧!


1.控制器

控制器是负责处理传入请求的层, 为了在 Spring Boot 中定义一个控制器,我创建了一个类 ProductOrderController, 然后使用了 @RestController 和 @RequestMapping 注解, 然后在控制器的每个方法上, 可以使用下面的注解来定义支持的 HTTP 方法和路径(可选)。

•@GetMapping•@PostMapping•@PutMapping•@DeleteMapping•@PatchMapping

如果要绑定到路径变量, 我们可以将参数添加到用@PathVariable 注释的控制器方法中,并指定与参数同名的路由路径模板,下面的 getOrderById() 方法,我们将id绑定为路径变量。

@RestController
@RequestMapping("/v1/orders")
class ProductOrderController(private val productOrderService: IProductOrderService
) {@GetMappingfun getOrders(query: ProductOrderQuery): List<ProductOrderDto> = when {query.productId?.isNotEmpty() == true -> productOrderService.getByProductId(query.productId!!)query.customerId?.isNotEmpty() == true -> productOrderService.getByCustomerId(query.customerId!!)else -> productOrderService.getAllOrders()}@GetMapping("{id}")fun getOrderById(@PathVariable id: String): ProductOrderDto = productOrderService.getById(id)
}

在 .NET Core 中, 控制器和上面是相似的, 首先创建一个 ProductOrderController类, 并继承 ControllerBase ,标记 [ApiController] 特性, 然后通过 [Route] 特性指定基本路径, 然后在控制器的每个方法上, 可以使用下面的特性来定义支持的 HTTP 方法和路径(可选)。

[ApiController]
[Route("v1/orders")]
public class ProductOrderController : ControllerBase
{private readonly IProductOrderService _productOrderService;public ProductOrderController(IProductOrderService productOrderService){_productOrderService = productOrderService;}[HttpGet]public async Task<List<ProductOrderDto>> GetOrders([FromQuery] ProductOrderQuery query){List<ProductOrderDto> orders;if (!string.IsNullOrEmpty(query.ProductId)){orders = await _productOrderService.GetAllByProductId(query.ProductId);}else if (!string.IsNullOrEmpty(query.CustomerId)){orders = await _productOrderService.GetAllByCustomerId(query.CustomerId);}else{orders = await _productOrderService.GetAll();}return orders;}[HttpGet("{id}")]public async Task<ProductOrderDto> GetOrderById(string id) => await _productOrderService.GetById(id);
}


2.模型绑定和验证

在 Spring Boot 中, 我们只需要给控制器的方法的参数加上下面的注解

•@RequestParam → 从查询字符串绑定•@RequestBody → 从请求体绑定•@RequestHeader → 从请求头绑定

对比表单的请求,不需要给参数加注解就可以绑定。

@RestController
@RequestMapping("/v1/customer")
class CustomerController(private val customerService: CustomerService
) {@PostMapping("/register")fun register(@Valid @RequestBody form: RegisterForm) = customerService.register(form)@PostMapping("/login")fun login(@Valid @RequestBody form: LoginForm) = customerService.login(form)
}@RestController
@RequestMapping("/v1/orders")
class ProductOrderController(private val productOrderService: IProductOrderService
) {@GetMappingfun getOrders(query: ProductOrderQuery): List<ProductOrderDto> {...}
}

如果要对参数进行验证, 需要添加 spring-boot-starter-validation 依赖项, 然后给 DTO 的属性加上 @NotEmpty@Length 等注解, 最后给DTO加上 @Valid 即可。

.NET Core 和上面类似, 同样你可以使用下面的特性标记控制器的方法

•[FromQuery] → 从查询字符串绑定•[FromRoute] → 从路由数据绑定•[FromForm] → 从表单数据绑定•[FromBody] → 从请求体绑定•[FromHeader] → 从请求头绑定

[Route("v1/customer")][ApiController]public class CustomerController : ControllerBase{[HttpPost("register")]public async Task<AuthResultDto> Register([FromBody] RegisterForm form) => await _customerService.Register(form);[HttpPost("login")]public async Task<AuthResultDto> Login([FromBody] LoginForm form) => await _customerService.Login(form);}[Route("v1/orders")][ApiController]public class ProductOrderController : ControllerBase{[HttpGet]public async Task<List<ProductOrderDto>> GetOrders([FromQuery] ProductOrderQuery query){.....}}

模型验证也是类似的, 给 DTO 的属性上加上 [Required]、[MinLength]、[MaxLength] 等特性就可以了。

public class RegisterForm
{[Required(ErrorMessage = "Please enter user id")]public string UserId { get; set; }[Required(ErrorMessage = "Please enter name")]public string Name { get; set; }[Required(ErrorMessage = "Please enter password")][MinLength(6, ErrorMessage = "Password must have minimum of 6 characters")]public string Password { get; set; }
}


3.异常处理

Spring Boot 的异常处理,主要用 @RestControllerAdvice 和 ExceptionHandler

注解,如下

abstract class AppException(message: String) : RuntimeException(message) {abstract fun getResponse(): ResponseEntity<BaseResponseDto>
}
@RestControllerAdvice
class ControllerExceptionHandler : ResponseEntityExceptionHandler() {@ExceptionHandler(AppException::class)fun handleAppException(ex: AppException, handlerMethod: HandlerMethod): ResponseEntity<BaseResponseDto> {return ex.getResponse()}
}

在 ASP.NET Core 中,异常处理程序被注册为过滤器/中间件,我们可以创建一个异常处理类,并继承 IExceptionFilter 接口。

public class ControllerExceptionFilter : IExceptionFilter
{public void OnException(ExceptionContext context){if (context.Exception is AppException exception){context.Result = exception.GetResponse();}}
}

然后注册这个异常过滤器

var builder = WebApplication.CreateBuilder(args);// Add services to the container.builder.Services.AddControllers(options =>
{options.Filters.Add<ControllerExceptionFilter>();
});


4.数据访问

在 Spring Boot 中, 你可以使用 Hibernate ORM, 创建一个Repository 接口, 并继承 JpaRepository , 这样就有了开箱即用的基本查询方法,比如 findAll() 和 findById()。

您还可以在定义自定义查询方法。只要遵循严格的方法命名约定,Spring 就会构建这个存储库的实现,包括运行时的所有查询,魔法?是的!

interface IProductOrderRepository : JpaRepository<ProductOrder, String> {@EntityGraph(type = EntityGraph.EntityGraphType.FETCH, value = "product-order-graph")override fun findById(id: String): Optional<ProductOrder>@EntityGraph(type = EntityGraph.EntityGraphType.FETCH, value = "product-order-graph")fun findAllByCustomer(customer: Customer): List<ProductOrder>@EntityGraph(type = EntityGraph.EntityGraphType.FETCH, value = "product-order-graph")@Query("SELECT ord FROM ProductOrder ord JOIN OrderItem item ON item.productOrder = ord WHERE item.productId = :productId")fun findAllByProductId(productId: String): List<ProductOrder>
}

而在 .NET Core 中,我们可以使用官方的 Entity Framework ORM, 首先,我们需要创建一个 DB Context 类, 这是 ORM 框架用来连接数据库和运行查询的桥梁。

public class AppDbContext : DbContext
{public DbSet<Customer> Customer { get; set; }public DbSet<Product> Product { get; set; }public DbSet<ProductOrder> ProductOrder { get; set; }public DbSet<OrderItem> OrderItem { get; set; }public AppDbContext(DbContextOptions<AppDbContext> options) : base(options){Customer = Set<Customer>();Product = Set<Product>();ProductOrder = Set<ProductOrder>();OrderItem = Set<OrderItem>();}
}

接下来,还需要注册上面的 DB Context,并配置数据库连接字符串

var builder = WebApplication.CreateBuilder(args);// Add services to the container.
builder.Services.AddDbContext<AppDbContext>(options =>
{// Using Pomelo.EntityFrameworkCore.MySql libraryoptions.UseMySql(builder.Configuration.GetConnectionString("EaterMysql"), ServerVersion.Parse("8.0.21-mysql"));
});

在我们的 Repository 中,我们访问 DB 上下文中的 DbSet 字段来执行查询, 在这里,我们使用 LINQ,这是一组直接融入 C# 语言的 API,用于从各种数据源进行查询。这是我非常喜欢的一项功能,因为它提供了 Fluent API,例如 Where()、Include() 或 OrderBy(),这非常方便!

public class ProductOrderRepository : BaseRepository<ProductOrder>, IProductOrderRepository
{public ProductOrderRepository(AppDbContext context) : base(context){}public Task<ProductOrder?> GetById(string id) => _context.ProductOrder.Include(o => o.Customer).Include(o => o.Items).Where(o => o.Id == id).FirstOrDefaultAsync();public Task<List<ProductOrder>> GetAllByCustomer(Customer customer) => _context.ProductOrder.Include(o => o.Items).Where(o => o.Customer == customer).ToListAsync();public Task<List<ProductOrder>> GetAllByProductId(string productId) => _context.ProductOrder.Include(o => o.Customer).Include(o => o.Items).Where(o => o.Items.Any(item => item.ProductId == productId)).ToListAsync();
}


5.依赖注入

Spring Boot 中的依赖注入真的非常简单, 只需根据类的角色使用 @Component、**@Service @Repository** 等注解即可,在启动时,它会进行扫描,然后注册。

@Service
class ProductOrderService(private val customerRepository: ICustomerRepository,private val productOrderRepository: IProductOrderRepository,private val mapper: IMapper
) : IProductOrderService {// ...// ...// ...
}

在 .NET Core 中, 服务根据生命周期分成3中类型,单例的,范围的, 瞬时的,并且在启动时手动注册到 DI 容器中

var builder = WebApplication.CreateBuilder(args);// Add services to the container.// Services
builder.Services.AddSingleton<IPasswordEncoder, PasswordEncoder>();
builder.Services.AddSingleton<ITokenService, TokenService>();
builder.Services.AddScoped<IProductOrderService, ProductOrderService>();
builder.Services.AddScoped<ICustomerService, CustomerService>();// Repositories
builder.Services.AddScoped<IProductOrderRepository, ProductOrderRepository>();
builder.Services.AddScoped<ICustomerRepository, CustomerRepository>();


6.身份验证和授权

在 Spring Boot 中, 首先需要添加依赖 spring-boot-starter-security, 然后,在 build.gradle 文件(或 pom.xml,如果您使用 Maven)中为 JWT 库添加以下依赖项:

implementation("io.jsonwebtoken:jjwt-api:${jjwtVersion}")
implementation("io.jsonwebtoken:jjwt-impl:${jjwtVersion}")
implementation("io.jsonwebtoken:jjwt-jackson:${jjwtVersion}")

接下来, 需要创建一个负责 JWT 令牌解析和验证的过滤器/中间件, 然后重写 doFilterInternal 方法, 编写解析和验证逻辑。

class JwtAuthenticationFilter(private val tokenService: ITokenService
) : OncePerRequestFilter() {override fun doFilterInternal(request: HttpServletRequest,response: HttpServletResponse,filterChain: FilterChain) {val authorization = request.getHeader("Authorization")if (authorization == null || !authorization.startsWith("Bearer")) {return filterChain.doFilter(request, response)}val token = authorization.replaceFirst("Bearer ", "")val claims = try {tokenService.parse(token).body} catch (ex: JwtException) {SecurityContextHolder.clearContext()return}// Set authentication to tell Spring that the user is valid and authenticated.SecurityContextHolder.getContext().authentication = UsernamePasswordAuthenticationToken(claims.id, null, arrayListOf())filterChain.doFilter(request, response)}
}

要配置和强制执行身份验证,需要先创建一个继承WebSecurityConfigurerAdapter的配置类,并使用 @Configuration 注解, 在这里注册我们上面创建的 JWT 过滤器,并在configure方法中配置哪些端点应该进行身份验证。比如,我允许匿名访问客户登录和注册端点。其他所有内容都应进行身份验证

class ApiAccessDeniedHandler : AccessDeniedHandler {override fun handle(request: HttpServletRequest,response: HttpServletResponse,accessDeniedException: AccessDeniedException) {response.status = HttpStatus.FORBIDDEN.value()}
}
class AuthEntryPoint : AuthenticationEntryPoint {override fun commence(request: HttpServletRequest,response: HttpServletResponse,authException: AuthenticationException) {response.status = HttpStatus.UNAUTHORIZED.value()}
}
@Configuration
class SecurityConfig(tokenService: ITokenService
) : WebSecurityConfigurerAdapter() {private val jwtAuthenticationFilter = JwtAuthenticationFilter(tokenService)@Beanfun passwordEncoder(): PasswordEncoder = BCryptPasswordEncoder()override fun configure(http: HttpSecurity) {http.csrf().disable().cors().disable().addFilterAfter(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter::class.java).exceptionHandling().accessDeniedHandler(ApiAccessDeniedHandler()).authenticationEntryPoint(AuthEntryPoint()).and().sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and().authorizeRequests().antMatchers("/v1/customer/register", "/v1/customer/login").permitAll().anyRequest().authenticated()}
}

在 ASP.NET Core 中实现 JWT 身份验证和授权非常简单, 首先安装Microsoft.AspNetCore.Authentication.JwtBearer` NuGet 包, 然后,在 Program.cs 文件中配置一些设置,例如密钥、颁发者和到期时间。

var builder = WebApplication.CreateBuilder(args);// Configure JWT Authentication
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme).AddJwtBearer(options =>{options.SaveToken = true;options.RequireHttpsMetadata = true;options.TokenValidationParameters = new TokenValidationParameters(){ValidateAudience = false,ValidIssuer = builder.Configuration["JWT:ValidIssuer"],IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(builder.Configuration["JWT:Secret"])),ClockSkew = TimeSpan.FromSeconds(30)};});var app = builder.Build();// Enable Authentication & Authorization
app.UseAuthentication();
app.UseAuthorization();app.MapControllers();app.Run();

如果需要认证,就在控制或者方法上,加上 [Authorize] 特性, 同样,可以加上 [AllowAnonymous] 代表允许匿名访问。

[Route("v1/customer")]
[ApiController]
[Authorize]
public class CustomerController : ControllerBase
{[HttpPost("login")][AllowAnonymous]public async Task<AuthResultDto> Login([FromBody] LoginForm form) => await _customerService.Login(form);[HttpGet]public async Task<CustomerDto> GetProfile() => await _customerService.GetProfile();
}


7.性能

最后是关键的部分,性能, 这两个框架在 QPS 和 内存使用率方面的表现如何?

在这里,我做了一个负载测试,调用一个 API,通过 id 获取一个产品订单。

  测试环境

CPU:Intel Core i7–8750H( 4.10 GHz),6 核 12 线程 RAM:32 GB 操作系统:Windows 11

  测试设置

我使用的压力测试工具是 K6, 进行了2次测试, 因为我想看看程序预热后性能提高了多少。在每次测试中,前 30 秒将从 0 增加到 1000 个虚拟用户,然后在那里停留 1 分钟。然后再过 30 秒,测试将从 1000 用户减少到 0 用户。

我还将 Golang(使用 Gin 框架和 Gorm)添加到基准测试, 这里只是为了对比 我们都知道 Golang 非常快。

2624adb7fcab53fd27af5d81e8b1c34c.gif

  测试结果

b5fa8f7b6dd8c4f964259ea61ca8ec44.png

显然,Golang 是最快的,我检查了两者都执行了查询优化,确认没有 N+1 问题,所以在 QPS 上 .NET Core 胜出。

b7fce66f812e87c189acfb6806efb5be.png

在内存使用方面,Golang 当然是最小的(只有 113 MB!),其次是 .NET Core, 最后就是超过1 GB 内存的 Spring Boot, 另外我观察到的有趣的事情是,测试完成后,Golang 和 .NET Core 的内存消耗分别减少到 10 MB 和 100 MB 左右,而 Spring Boot 保持在 1 GB 以上,直到我终止进程。

最后,Spring Boot 和 ASP.NET Core 都是非常成熟的框架,您都可以考虑使用, 希望对您有用!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/293162.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

java 生成无重复 随机数

2019独角兽企业重金招聘Python工程师标准>>> 一、实现逻辑 1.需要一个固定的数据集。 2.从数据集中随机去除当前索引的数据&#xff0c;并移除生成。并重复生成多个。 二、编码 import java.util.ArrayList; import java.util.Calendar; import java.util.List; imp…

最诡异航空事件,幽灵航班包括驾驶人员,所有人都在高空中昏睡!而后整机坠毁!...

全世界只有3.14 % 的人关注了爆炸吧知识2005年8月14日&#xff0c;一架塞浦路斯的太阳神航空&#xff08;Helios Airways&#xff09;波音737-300客机&#xff0c;班次ZU-522&#xff08;HCY 522&#xff09;&#xff0c;机身编号5B-DBY&#xff0c;机上载有59名成年人及8名儿童…

[激励机制]浅谈内部竞争——如何让你的员工玩命干活?

我是标题党&#xff0c;标题是故意气你的&#xff0c;千万表拍我。公元2012年12月12号&#xff0c;Clark 拿出所有积蓄创办了一个公司&#xff0c;招了看上去还不错的5个员工组成了一个小型团队。紧接着&#xff0c;摆在他面前的一个很明显的问题就是——如何让他们玩命干活&am…

mybatis 查询的时间不对_程序员,Mybatis 你踩过坑吗?

点击上方“Java基基”&#xff0c;选择“设为星标”做积极的人&#xff0c;而不是积极废人&#xff01;源码精品专栏 中文详细注释的开源项目RPC 框架 Dubbo 源码解析网络应用框架 Netty 源码解析消息中间件 RocketMQ 源码解析数据库中间件 Sharding-JDBC 和 MyCAT 源码解析作业…

李洪强iOS开发之- 实现简单的弹窗

李洪强iOS开发之- 实现简单的弹窗 实现的效果: 112222222222223333333333333333

.NET 编码的基础知识

点击上方蓝字关注我们.NET 编码的一些基本概念和分析简单的类型概念Hex &#xff08;16进制&#xff09;byte 字节 范围是&#xff1a;0~255&#xff0c;二进制下的范围就是00000000~11111111&#xff0c;相当于1字节。byte[] 字节数组bit 比特&#xff0c;只有2种状态&#xf…

什么是MVC?MVC框架的优势和特点

目录 一、什么是MVC 二、MVC模式的组成部分和工作原理 1、模型&#xff08;Model&#xff09; 2、视图&#xff08;View&#xff09; 3、控制器&#xff08;Controller&#xff09; 三、MVC模式的工作过程如下&#xff1a; 用户发送请求&#xff0c;请求由控制器处理。 …

docker没有下载完全_会用Docker的人都别装了,这多简单呐

学术又官方的说法Docker 是一个开源的应用容器引擎&#xff0c;让开发者可以打包他们的应用以及依赖包到一个可移植的容器中,然后发布到任何流行的Linux机器或Windows 机器上,也可以实现虚拟化,容器是完全使用沙箱机制,相互之间不会有任何接口。没用过的人能看懂这段话&#xf…

WPF MVVM实例三

在没给大家讲解wpf mwm示例之前先给大家简单说下MVVM理论知识&#xff1a;WPF技术的主要特点是数据驱动UI,所以在使用WPF技术开发的过程中是以数据为核心的&#xff0c;WPF提供了数据绑定机制&#xff0c;当数据发生变化时&#xff0c;WPF会自动发出通知去更新UI。我们使用模式…

stringcstdlibctimecstdargctimectypecmathclimits

转载地址&#xff1a;http://blog.csdn.net/kz_ang/article/details/7767335 <string>头文件 string构造函数 string s  生成一个空字符串s string s(str)  拷贝构造函数,生成str对象的复制品 string s(str,stridx)  将字符串str对象内"始于位置stridx"…

C3P0 释放连接 的问题

2019独角兽企业重金招聘Python工程师标准>>> 记录一下使用C3P0 时犯的低级错误&#xff0c;没有关闭connection 达到连接池最大限制后造成程序假死的现象&#xff0c;以后得仔细再仔细了&#xff01; 转载于:https://my.oschina.net/fusxian/blog/146700

python随机抽取人名_python的random

python的random函数更多的random用法可参阅&#xff1a;random --- 生成伪随机数 - Python 3.7.4 文档​docs.python.org以下使用了&#xff1a;洗牌&#xff1a;random.shuffle随机抽取元素&#xff0c;且元素不重复&#xff1a;random.sample随机抽取元素&#xff0c;且元素可…

Windows 11/10 正式版全新 ISO 镜像下载

微软现已发布 Windows 11/10 正式版&#xff08;版本 21H2&#xff09;全新 ISO 镜像。Windows 11 版本 21H2 全新镜像集成了 2021 年 10 月更新、11 月更新和 12 月更新。Windows 10 版本 21H2 全新镜像集成了 2021 年 11 月更新和 12 月更新。版本区别Windows 11/10 消费者版…

java web filter 之一 基础实现

本文主要对filter的基本使用进行了讲解&#xff0c;其中涉及到了 filter是什么 一个filter处理一个jsp 多个filter处理一个jsp filter是什么 Filter 是java下的一种过滤器 &#xff0c;能实现对java web程序 客户端和服务器端消息的过滤&#xff0c;也就是在服务器段接受reques…

eclipse索引4超出范围_Python内置的4个重要基本数据结构:列表、元组、字典和集合

本章内容提要&#xff1a;列表及操作元组及操作字典及操作集合简介第2章介绍了数字(整数、浮点数)、逻辑值和字符串等Python内置的基本数据类型。在实际的操作中&#xff0c;仅仅依赖它们很难高效地完成复杂的数据处理任务。基于对基本数据类型的构建&#xff0c;Python拓展出列…

.NET 6 中 System.Text.Json 的新特性

1支持忽略循环引用在 .NET 6 之前&#xff0c;如果 System.Text.Json 检测到循环引用&#xff0c;就会抛出 JsonException 异常。在 .NET 6 中&#xff0c;你可以忽略它们。Category dotnet new() {Name ".NET 6", }; Category systemTextJson new() {Name "…