GraphQL 实践与服务搭建

news2025/8/12 18:31:40

原文链接: GraphQL 实践与服务搭建

GraphQL 既是一种用于 API 的查询语言也是一个满足你数据查询的运行时。 GraphQL 对你的 API 中的数据提供了一套易于理解的完整描述,使得客户端能够准确地获得它需要的数据,而且没有任何冗余,也让 API 更容易地随着时间推移而演进,还能用于构建强大的开发者工具。

大概率你听说过 GraphQL,知道它是一种与 Rest API 架构属于 API 接口的查询语言。但大概率你也与我一样没有尝试过 GraphQL。

事实上从 2012 年 Facebook 首次将 GraphQL 应用于移动应用,到 GraphQL 规范于 2015 年实现开源。可如今现状是 GraphQL 不温不火,时不时又有新的文章介绍,不知道的还以为是什么新技术。

:::tip 目标
本文将上手使用 GraphQL,并用 Nestjs 与 Strapi 这两个 Node 框架搭建 GraphQL 服务。
:::

关于 GraphQL 介绍,详见官网 GraphQL | A query language for your API 或相关介绍视频 GraphQL 速览:React/Vue 的最佳搭档

GraphQL 与 Restful API 相比

Restful API

Restful 架构的设计范式侧重于分配 HTTP 请求方法(GET、POST、PUT、PA TCH、DELETE)和 URL 端点之间的关系。如下图

但是实际复杂的业务中,单靠 Restful 接口,需要发送多条请求,例如获取博客中某篇博文数据与作者数据

GET /blog/1

GET /blog/1/author

要么单独另写一个接口,如getBlogAndAuthor,这样直接为调用方“定制”一个接口,请求一条就得到就调用方想要的数据。但是另写一个getBlogAndAuthor 就破坏了 Restful API 接口风格,并且在复杂的业务中,比如说还要获取博文的评论等等,后端就要额外提供一个接口,可以说非常繁琐了。

有没有这样一个功能,将这些接口做一下聚合,然后将结果的集合返回给前端呢?在目前比较流行微服务架构体系下,有一个专门的中间层专门来处理这个事情,这个中间层叫 BFF(Backend For Frontend)。可以参阅 BFF——服务于前端的后端

但这些接口一般来说都比较重,里面有很多当前页面并不需要的字段,那还有没有一种请求:客户端只需要发送一次请求就能获取所需要的字段

有,也就是接下来要说的 GraphQL

GraphQL

REST API 构建在请求方法(method)和端点(endpoint)之间的连接上,而 GraphQL API 被设计为只通过一个端点,即 /graphql,始终使用 POST 请求进行查询,其集中的 API 如 http://localhost:3000/graphql,所有的操作都通过这个接口来执行,这会在后面的操作中在展示到。

:::info
但是想要一条请求就能得到客户端想要的数据字段,那么服务端必然要做比较多的任务😟(想想也是,后端啥都不干,前端就啥都能获取,怎么可能嘛)。

而服务端要做的就是搭建一个 GraphQL 服务,这在后面也会操作到,也算是本文的重点。
:::

接下来便会在客户端中体验下 GraphQL,看看 GraphQL 究竟有多好用。

在线体验 GraphQL

可以到 官网 中简单尝试入门一下,在 Studio 可在线体验 GraphQL,也可以到 [SWAPI GraphQL API](<https://swapi-graphql.netlify.app/?query={
person(personID: 1) {
name
}
}> ‘SWAPI GraphQL API (swapi-graphql.netlify.app)’) 中体验。

下面以 apollographql 为例,并查询 People 对象。

query

查询所有 People 并且只获取 namegenderheight 字段

查询 personID 为 1 的 Person 并且只获取 namegenderheight 字段

查询 personID 为 2 的 Person 并且只获取 nameeyeColorskinColorhairColor 字段

从上面查询案例中其实就可以发现,我只需要在 person 中写上想要获取的字段,GraphQL 便会返回带有该字段的数据。避免了返回结果中不必要的数据字段。

{
	person{ 
		# 写上想获取的字段 
	}
}

如果你不想要 person 数据或者想要其他其他的数据,不用像 Restful API 那样请求多条接口,依旧请求/graphql,如

:::info

无论你想要什么数据,一次请求便可满足。

:::

mutation

GraphQL 的大部分讨论集中在数据获取(也是它的强项),但是任何完整的数据平台也都需要一个改变服务端数据的方法。即 CRUD。

GraphQL 提供了 变更(Mutations) 用于改变服务端数据,不过 apollographql 在线示例中并没有如 createPeople 字段支持 。这个片段在线体验中就无法体验到,后在后文中展示到。这里你只需要知道 GraphQL 能够执行基本的 CRUD 即可。

fragmen 和 subscribtion

此外还有 fragment subscription 就不做介绍。

小结

尝试完上面这些操作后,可以非常明显的感受到 GraphQL 的优势与便利,本来是需要请求不同的 url,现在只需要请求 /graphql,对调用方(前端)来说非常友好,香是真的香。

可目前只是使用了别人配置好的 GraphQL 服务,让前端开发用了特别友好的 API。但是,对于后端开发而言,想要提供 GraphQL 服务可就不那么友善了。因为它不像传统的 restful 请求,需要专门配置 GraphQL 服务,而整个过程是需要花费一定的工作量(定义 Schema,Mutations 等等),前面也提到想要一条请求就能得到客户端想要的数据字段,那服务端必然需要额外的工作量。

不仅需要在后端中配置 GraphQL 服务,用于接收 GraphQL 查询并验证和执行,此外前端通常需要 GraphQL 客户端,来方便使用 GraphQL 获取数据,目前实用比较多的是Apollo Graph,不过本文侧重搭建GraphQL 服务,因此前端暂不演示如何使用 GraphQL。

你可能听过一句话是,graphq​l 大部分时间在折磨后端,并且要求比较严格的数据字段,但是好处都是前端。把工作量基本都丢给了后端,所以在遇到使用这门技术的公司,尤其是后端岗位就需要考虑有没有加班的可能了。

以下便会开始实际搭建 GraphQL 服务,这里会用 Nest.js 与 Strapi 分别实践演示。

Nest.js

官方文档:GraphQL + TypeScript | NestJS

模块:nestjs/graphql

仓库本文实例代码仓库: kuizuo/nest-graphql-demo

创建项目

nest new nest-graphql-demo

安装依赖

npm i @nestjs/graphql @nestjs/apollo graphql apollo-server-express

修改 app.module.ts

import {Module} from '@nestjs/common';
import {GraphQLModule} from '@nestjs/graphql';
import {ApolloDriver, ApolloDriverConfig} from '@nestjs/apollo';

@Module({
  imports: [
    GraphQLModule.forRoot<ApolloDriverConfig>({
      driver: ApolloDriver,
      autoSchemaFile: true,
    }),
  ],
})
export class AppModule {}

resolver

设置了autoSchemaFile: true ,nest.js 将会自动搜索整个项目所有以 .resolver.ts 为后缀的文件,将其解析为 schema.gql 比如说创建app.resolver.ts

import {Resolver, Query} from '@nestjs/graphql';

@Resolver()
export class AppResolver {
  @Query(() => String) // 定义一个查询,并且返回字符类型
  hello() {
    return 'hello world';
  }
}

graphqlresolver 叫解析器,与 service 类似(也需要在 @Module 中通过 providers 导入)。resolver主要包括query(查询数据)、mutation(增、删、改数据)、subscription(订阅,有点类型 socket),在 graphql 项目中我们用 resolver 替换了之前的控制器。

这时候打开http://127.0.0.1:3000/graphql,可以在右侧中看到自动生成的 Schema,这个 Schema 非常关键,决定了你客户端能够请求到什么数据。

尝试输入 GraphQL 的 query 查询(可以按 Ctrl + i 触发代码建议(Trigger Suggest),与 vscode 同理)

此时点击执行,可以得到右侧结果,即app.resolver.tshello 函数所定义的返回体。

Code first 与 Schema first

在 nestjs 中有 Code first 与 Schema first 两种方式来生成上面的 Schema,从名字上来看,前者是优先定义代码会自动生成 Schema,而后者是传统方式先定义Schema。

在上面一开始的例子中是 Code First 方式,通常使用该方式即可,无需关心 Schema 是如何生成的。下文也会以 Code First 方式来编写 GraphQL 服务。

也可到官方示例仓库中 nest/sample/31-graphql-federation-code-first 和 nest/sample/32-graphql-federation-schema-first 查看两者代码上的区别。

快速生成 GraphQL 模块

nest 提供 cli 的方式来快速生成 GraphQL 模块

nest g resource <name>

比如创建一个 blog 模块

nest g resource blog --no-spec
? What transport layer do you use? GraphQL (code first)
? Would you like to generate CRUD entry points? Yes
CREATE src/blog/blog.module.ts (217 bytes)
CREATE src/blog/blog.resolver.ts (1098 bytes)
CREATE src/blog/blog.resolver.spec.ts (515 bytes)
CREATE src/blog/blog.service.ts (623 bytes)
CREATE src/blog/blog.service.spec.ts (446 bytes)
CREATE src/blog/dto/create-blog.input.ts (196 bytes)
CREATE src/blog/dto/update-blog.input.ts (243 bytes)
CREATE src/blog/entities/blog.entity.ts (187 bytes)
UPDATE src/app.module.ts (643 bytes)

便会生成如下文件

import {Resolver, Query, Mutation, Args, Int} from '@nestjs/graphql';
import {BlogService} from './blog.service';
import {Blog} from './entities/blog.entity';
import {CreateBlogInput} from './dto/create-blog.input';
import {UpdateBlogInput} from './dto/update-blog.input';

@Resolver(() => Blog)
export class BlogResolver {
  constructor(private readonly blogService: BlogService) {}

  @Mutation(() => Blog)
  createBlog(@Args('createBlogInput') createBlogInput: CreateBlogInput) {
    return this.blogService.create(createBlogInput);
  }

  @Query(() => [Blog], {name: 'blogs'})
  findAll() {
    return this.blogService.findAll();
  }

  @Query(() => Blog, {name: 'blog'})
  findOne(@Args('id', {type: () => Int}) id: number) {
    return this.blogService.findOne(id);
  }

  @Mutation(() => Blog)
  updateBlog(@Args('updateBlogInput') updateBlogInput: UpdateBlogInput) {
    return this.blogService.update(updateBlogInput.id, updateBlogInput);
  }

  @Mutation(() => Blog)
  removeBlog(@Args('id', {type: () => Int}) id: number) {
    return this.blogService.remove(id);
  }
}

此时 Schema 如下

不过nest cli创建的blog.service.ts 只是示例代码,并没有实际业务的代码。

此外blog.entity.ts也不为数据库实体类,因此这里引入typeorm,并使用sqlite3

集成 Typeorm

安装依赖

pnpm install @nestjs/typeorm typeorm sqlite3
import {Module} from '@nestjs/common';
import {AppController} from './app.controller';
import {AppService} from './app.service';
import {GraphQLModule} from '@nestjs/graphql';
import {ApolloDriver, ApolloDriverConfig} from '@nestjs/apollo';
import {AppResolver} from './app.resolver';
import {BlogModule} from './blog/blog.module';
import {TypeOrmModule} from '@nestjs/typeorm';

@Module({
  imports: [
    TypeOrmModule.forRoot({
      type: 'sqlite',
      database: 'db.sqlite3',
      entities: [__dirname + '/**/*.entity{.ts,.js}'],
      synchronize: true,
    }),
    GraphQLModule.forRoot<ApolloDriverConfig>({
      driver: ApolloDriver,
      autoSchemaFile: true,
      playground: true,
    }),
    AppModule,
    BlogModule,
  ],
  controllers: [AppController],
  providers: [AppService, AppResolver],
})
export class AppModule {}

blog.entity.ts 改成实体类,代码为

import {ObjectType, Field} from '@nestjs/graphql';
import {
  Column,
  Entity,
  PrimaryGeneratedColumn,
  CreateDateColumn,
  UpdateDateColumn,
} from 'typeorm';

@ObjectType()
@Entity()
export class Blog {
  @Field(() => Int)
  @PrimaryGeneratedColumn()
  id: number;

  @Field()
  @Column()
  title: string;

  @Field()
  @Column({type: 'text'})
  content: string;

  @Field()
  @CreateDateColumn({name: 'created_at', comment: '创建时间'})
  createdAt: Date;

  @Field()
  @UpdateDateColumn({name: 'updated_at', comment: '更新时间'})
  updatedAt: Date;
}

其中 @ObjectType() 装饰器让 @nestjs/graphql 自动让其视为一个 type Blog

@Field() 则是作为可展示的字段,比如 password 字段无需返回,就不必要加该装饰器。

:::caution

@nestjs/graphql 会将 typescript 的 number 类型视为 Float,所以需要转成 Int 类型,即 @Field(() => Int)

:::

为 BlogService 编写 CRUD 数据库业务代码,并在 dto 编写参数效验代码,这里简单暂时部分代码。

import {Injectable} from '@nestjs/common';
import {InjectRepository} from '@nestjs/typeorm';
import {Repository} from 'typeorm';
import {CreateBlogInput} from './dto/create-blog.input';
import {UpdateBlogInput} from './dto/update-blog.input';
import {Blog} from './entities/blog.entity';

@Injectable()
export class BlogService {
  constructor(
    @InjectRepository(Blog)
    private blogRepository: Repository<Blog>,
  ) {}

  create(createBlogInput: CreateBlogInput) {
    return this.blogRepository.save(createBlogInput);
  }

  findAll() {
    return this.blogRepository.find();
  }

  findOne(id: number) {
    return this.blogRepository.findOneBy({id});
  }

  async update(id: number, updateBlogInput: UpdateBlogInput) {
    const blog = await this.blogRepository.findOneBy({id});
    const item = {...blog, ...updateBlogInput};
    return this.blogRepository.save(item);
  }

  remove(id: number) {
    return this.blogRepository.delete(id);
  }
}
import {InputType, Field} from '@nestjs/graphql';

@InputType()
export class CreateBlogInput {
  @Field()
  title: string;

  @Field()
  content: string;
}

此时

CRUD

下面将演示 graphql 的 Mutation。

新增

修改

删除

Query 就不在演示。

小结

至此,在 Nest.js 中配置 GraphQL 服务的就演示到此,从这里来看,Nest.js 配置 GraphQL 服务还算比较轻松,但是做了比较多的工作量,创建 resolver,创建 modal(或在已有实体添加装饰器),不过本文案例中只演示了基本的 CRUD 操作,实际业务中还需要涉及鉴权,限流等等。

Strapi

Strapi 官方提供 GraphQL 插件 免去了配置的繁琐。更具体的配置参见 GraphQL - Strapi Developer Documentation

这里我就选用 kuizuo/vitesse-nuxt-strapi 作为演示,并为其提供 graphQL 支持。

strapi 安装

npm install @strapi/plugin-graphql

接着启动 strapi 项目,并在浏览器打开 graphql 控制台 http://localhost:1337/graphql,以下将演示几个应用场景。

例子

查询所有 todo

查询 id 为 2 的 todo

查询 id 为 2 的 todo 并只返回 value 属性

新增 todo

更新 todo

删除 todo

由于 Nuxt Strapi 提供 useStrapiGraphQL 可以非常方便是在客户端调用 GraphQL 服务。

<script setup lang="ts">
  const route = useRoute();
  const graphql = useStrapiGraphQL();

  // Option 1: use inline query
  const restaurant = await graphql(`
  query {
    restaurant(id: ${route.params.id}) {
      data {
        id
        attributes {
          name
        }
      }
    }
  }
`);

  // Option 2: use imported query
  const restaurant = await graphql(query, {id: route.params.id});
</script>

小结

对于 Strapi 来说,搭建 GraphQL 服务基本没有配置的负担,安装一个插件,即可配合 Strapi 的 content-type 来提供 GraphQL 服务。

总结

GraphQL 翻译过来为 图表 Query Language,我所理解的理念是通过 json 数据格式的方式去写 SQL,而且有种前端人员在写 sql 语句。在我看来 GraphQL 更多是业务数据特别复制的情况下使用,往往能够事半功倍。但对于本文中示例的代码而言,GraphQL 反倒有点过于先进了。

如今看来,GraphQL 还处于不温不火的状态,目前更多的站点主流还是使用 Restful API 架构。我不过我猜测,主要还是大多数业务没有 API 架构的升级的需求,原有的 Restful API 虽说不够优雅,但是也能够满足业务的需求,反而 GraphQL 是一个新项目 API 架构的选择,但不是一个必须的选择。

至于如何选择,可以参阅官方 GraphQL 最佳实践,至于说有没有必要学 GraphQL,这篇文章 都快 2022 年了 GraphQL 还值得学吗 能给你答案。我的建议是了解即可,新项目可以考虑使用,就别想着用 GraphQL 来重构原有的 API 接口,工作量将会十分巨大,并且还可能是费力不讨好的事。反正我认为这门技术不像 Git 这种属于必学的技能,我的五星评分是⭐⭐

但多了解一门技术,就是工作面试的资本。回想我为何尝试 GraphQL,就是因为我无意间看到了一份 ts 全栈的远程面试招聘,在这份招聘单中写到 【会 graphql 编写是加分项】。所以抱着这样的态度去尝试了一番,说不准未来就是因为 graphql 让我拿到该 offer。当然也是因为很早之前就听闻 GraphQL,想亲手目睹下是否有所谓的那么神奇。

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

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

相关文章

《SystemVerilog Assertion 应用指南》学习01

文章目录0、基于断言的 验证1、SVA 介绍1.1.、什么是断言1.2、为什么使用 SystemVerilog 断言&#xff08;SVA&#xff09;1.3、SystemVerilog 的调度1.4、SVA 术语1.4.1、并发断言1.4.2、即时断言1.5、建立 SVA 块1.6、一个简单的序列1.7、边沿定义的序列1.8、逻辑关系的序列1…

云原生安全:4C~

4C是啥&#xff1f; cloudclustercontainercode 4个C是层的关系&#xff0c;外圈不安全&#xff0c;不能指望里面太安全。。。 目录 Cloud cloud Provider Security 基础架构安全 Cluster cluster的组件 cluster中的组件&#xff08;应用中的&#xff09; Container …

第二章:Pythonocc官方demo 案例44(几何板条)

源代码&#xff1a; ##Copyright 2009-2016 Jelle Feringa (jelleferingagmail.com) ## ##This file is part of pythonOCC. ## ##pythonOCC is free software: you can redistribute it and/or modify ##it under the terms of the GNU Lesser General Public License as pub…

数据库 Apache Doris 展开了为期两个月的调研测试

2022 年 3 月开始&#xff0c;我们对符合以上特点的数据库 Apache Doris 展开了为期两个月的调研测试。以下是 Apache Doris 1.1.2 在各个方面的满足情况。 基于上述情况&#xff0c;我们决定采用 Apache Doris&#xff0c;除了可以满足上文提到的几个特点&#xff0c;我们还考…

[信息系统项目管理师-2023备考]信息化与信息系统(一)

1.信息的质量特性 精确性&#xff1a;对事物状态描述的精准程度完整性&#xff1a;对事物状态描述的全面程度&#xff0c;完整信息应该包括所有重要事实可靠性&#xff1a;信息的来源、采集方法、传输过程是可以信任的&#xff0c;符合预期及时性&#xff1a;获取信息的时刻与…

(八)RabbitMQ发布确认

发布确认1、发布确认原理2、发布确认策略2.1、开启发布确认的方法2.2、单个确认发布2.3、批量确认发布2.4、异步确认发布2.5、处理异步未确认消息1、发布确认原理 书面文&#xff1a;生产者将信道设置成 confirm 模式&#xff0c;一旦信道进入 confirm 模式&#xff0c;所有在…

Python集合类型详解(一)——集合定义与集合操作符

今天继续给大家介绍Python相关知识&#xff0c;本文主要内容是Python集合类型定义与集合操作符。 一、集合类型定义 在Python中&#xff0c;集合是一种非常重要的组合数据类型。Python中的集合与数学中的集合非常相似&#xff0c;集合中的数据没有顺序&#xff0c;并且每个元…

第二章:Pythonocc官方demo 案例45(几何轴向曲线偏置)

源代码&#xff1a; #!/usr/bin/env python##Copyright 2009-2016 Jelle Feringa (jelleferingagmail.com) ## ##This file is part of pythonOCC. ## ##pythonOCC is free software: you can redistribute it and/or modify ##it under the terms of the GNU Lesser General …

【优化调度】遗传算法求解工件的并行调度组合优化问题【含Matlab源码 2234期】

⛄ 一、 遗传算法简介 1 问题描述 假定一个加工系统有m台机器和n件工件&#xff0c;每个工件包含一道或多道工序,工件的加工顺序是确定的,但每个工件可能有几条可行的加工路线,即每道工序可在多台不同的机床上加工,工序的加工时间和加工费用随机床的性能不同而变化。作业调度的…

并查集解析

文章目录&#x1f6a9;并查集的理解&#x1f6a9;并查集的结构与原理&#x1f6a9;并查集的实现&#x1f341;整体框架&#x1f341;路径压缩&#x1f6a9;总结&#x1f6a9;并查集的理解 并查集是基于数组操作的一个特殊数据结构&#xff0c;和以前学习[数组的堆排序]时有点相…

分析设备树文件

1.设备树是干嘛的 硬件资源有很多&#xff0c;想要实现分类管理&#xff0c;方便驱动去控制它&#xff0c;则需要设备树来管理硬件信息。 所以&#xff0c;设备树主要存放了一些设备节点信息&#xff0c;键值对&#xff0c;和属性&#xff1b;节点中也可以包含子节点。 2.设…

安全架构中的前端安全防护研究

国家互联网应急中心发布的被篡改网站数据让很多人触目惊心&#xff0c;近年来各种Web网站攻击事件频频发生&#xff0c;网站SQL注入&#xff0c;网页被篡改、信息失窃、甚至被利用成传播木马的载体Web安全形势日益严峻&#xff0c;越来越受到人们的关注。 Gartner 对安全架构的…

创建计划协议、维护创建计划、收货

创建计划协议事务码&#xff1a;ME31L创建计划协议 &#xff08;ME32L 修改计划协议 ME33L查询计划协议 ME2L查询采购订单&#xff09; 输入&#xff1a;供应商、协议类型、协议日期、采购组织、采购组、工厂、存储地点等信息后回车。 然后输入有效截至日期&#xff0c; 再点击…

计算机毕业设计java+springboot宠物商城系统

一、项目运行 环境配置&#xff1a; Jdk1.8 Tomcat8.5 Mysql HBuilderX&#xff08;Webstorm也行&#xff09; Eclispe&#xff08;IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持&#xff09;。 项目技术&#xff1a; Springboot Maven mybatis Vue 等等组成&#xff0c;B…

【JVM】垃圾回收:垃圾收集器

一、语境中的并行与并发 并行 并行描述的时多条垃圾收集器线程之间的关系&#xff0c;说明同一时间有多条这样的线程在协同工作&#xff0c;通常默认此时用户线程是处于等待状态。 并发 并发描述的是垃圾收集器线程与用户线程之间的关系&#xff0c;说明同一时间垃圾收集器线程…

简单实现一个虚拟形象系统

前言 上周启动居家开会的时候&#xff0c;看到有人通过「虚拟形象」功能&#xff0c;给自己带上了口罩、眼镜之类&#xff0c;于是想到了是不是也可以搞一个简单的虚拟形象系统。 大致想来&#xff0c;分为以下几个部分&#xff1a; 卷积神经网络(CNN) 下面讲解一下三层CN…

视频格式转换器哪个好用?万兴优转-好用的视频格式转换器

视频格式转换器是用于转换视频格式的软件&#xff0c;是指用于视频转换、音频转换、CD轨抓取、音视频混合转换、音视频剪切、连接转换、视频水印叠加、滚动字幕、个性化文字、图片叠加、视频相框叠加的音视频转换工具。 也就是说&#xff0c;视频有非常多的格式如AVI、VCD、SVC…

【JavaWeb从零到一】会话技术CookieSessionJSP

&#x1f680;【JavaWeb从零到一】系列文章目录 &#x1f6a9;【JavaWeb从零到一】前置知识 &#x1f6a9;【JavaWeb从零到一】Mysql基础总结 &#x1f6a9;【JavaWeb从零到一】JDBC详解 &#x1f6a9;【JavaWeb从零到一】JDBC连接池&JDBCTemplate Cookie&Session&…

王学岗音视频开发(二)—————OpenGLES开发实践

矩阵以及矩阵运算 上图就是m x a 的矩阵 1x30x22x1 :为左侧第一行乘以右侧第一列。 1x10x12x0 :为左侧第一行乘以右侧第二列。 -1x33x21x1:为左侧第二行乘以右侧第一列。 -1x13x11x0:为左侧第二行乘以右侧第二列。 矩阵的行列式 伴随矩阵 A*表示伴随矩阵 OpenGL 教程----屏…

Grails SpringBoot国际化不生效

问题描述&#xff1a; grails项目使用了国际化&#xff0c;按照官方文档的说法&#xff1a; 会根据用户浏览器访问时使用的Accept-Language头自动选择合适的语言。 但无论浏览器了配置什么语言甚至配置了Tomcat启动参数 -Duser.languagexxx -Duser.regionxxx页面显示依旧是英…