巅峰对决!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名儿童…

c#代码实现GPS数据的有效性校验

用于校验GPS报文指令的有效性 很简单的代码&#xff0c;留存吧 public static bool Verify(string gpsInfo) { if (gpsInfo null || "".Equals(gpsInfo)) return false; char p gpsInfo[0]; char c (cha…

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

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

Android之TrafficStats实现流量实时监测

---恢复内容开始---TrafficStats类是由Android提供的一个从你的手机开机开始&#xff0c;累计到现在使用的流量总量&#xff0c;或者统计某个或多个进程或应用所使用的流量&#xff0c;当然这个流量包括的Wifi和移动数据网Gprs。这里只针对手机所使用的流量作介绍&#xff0c;至…

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

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

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

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

数据挖掘模型生命周期管理

为成功地利用预测模型&#xff0c;您需要从开发阶段直至生产环境对模型进行全面管理。模型生命周期管理是由以下阶段组成的高效交替过程&#xff1a; • 确定业务目标 • 访问和管理数据 • 开发模型 • 验证模型 • 部署模型 • 监控模型 确定业务目标 第一步确定所需模型以及…

.NET 编码的基础知识

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

Android之android.os.NetworkOnMainThreadException异常

今天用handler.post(Runnable);的时候出现了android.os.NetworkOnMainThreadException Runnable里面使用的访问网络请求&#xff0c;网络请求是不可以放在主线程里面的&#xff0c;所以出现了这个问题 因为我想执行 handler.post(runnable); Runnable runnable new Runnable(…

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

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

每日英语:The First Day On A Job Is Tough Work

Why is the first day on the job often the worst?New employees tend to be greeted with stacks of benefits paperwork, technology hassles and dull presentations about company culture. hassle&#xff1a;激战&#xff0c;争辩&#xff0c;麻烦事    But some c…

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

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

数据挖掘领域十大经典算法初探

一、C4.5 C4.5&#xff0c;是机器学习算法中的一个分类决策树算法&#xff0c; 它是决策树(决策树也就是做决策的节点间的组织方式像一棵树&#xff0c;其实是一个倒树)核心算法 ID3的改进算法&#xff0c;所以基本上了解了一半决策树构造方法就能构造它。 决策树构造方法其实就…

WPF MVVM实例三

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

linux命令chown和chmod什么区别

chown一般用来 更改属主。也就是文件所属用户。chmod功能要比chown要强大。可更改文件所有属性和权限。只有管理员账户才有权限用此命令。chown 是修改文件的所有者(owner),和所属组(group)chmod 是修改文件的执行属性(所属组,所属者以及其他人所有的权限,比如 读,写,执行)

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"…

背包模板

背包模板&#xff0c;自己总结&#xff0c;做题可直接套用。 0-1背包 有N件物品和一个容量为V的背包。第i件物品的费用是c[i]&#xff0c;价值是w[i]。求解将哪些物品装入背包可使价值总和最大。 公式&#xff1a; f[i][v]max{f[i-1][v],f[i-1][v-c[i]]w[i]} 伪代码&#xff1a…