阿里云「飞天发布时刻」2024来啦!新产品、新特性、新能力、新方案,等你来探~ 了解详情
写点什么

Micronaut 教程(二):分布式跟踪、JWT 安全和 AWS Lambda 部署

  • 2018-12-15
  • 本文字数:10489 字

    阅读完需:约 34 分钟

Micronaut教程(二):分布式跟踪、JWT安全和AWS Lambda部署

关键要点

  • Micronaut 提供了与 Zipkin 和 Jaeger 等多种分布式跟踪解决方案的无缝集成。

  • 框架提供了几种“开箱即用”的安全解决方案,例如基于 JWT 的认证。

  • Micronaut 提供了“令牌传播”之类的功能,用以简化微服务之间的安全通信。

  • 因为内存占用少,Micronaut 能够运行在功能即服务(FaaS)无服务器环境中。


在本系列的第一篇文章中,我们使用基于 JVM 的Micronaut框架开发并部署了三个微服务。在第二篇文章中,我们将为应用程序添加几个功能:分布式跟踪、JWT 安全性和无服务器功能。此外,我们也将介绍 Micronaut 提供的用户输入验证功能。

分布式跟踪

将系统分解为更小、更细粒度的微服务可以带来多种好处,但也会给生产环境的监控系统增加复杂性。


你应该假设你的网络将会受到恶意实体的骚扰,它们时刻准备着随心所欲地释放它们的愤怒。

——Sam Newman,《构建微服务》


Micronaut 与 Jaeger 和 Zipkin 原生集成——它们都是顶级的开源分布式跟踪解决方案。


Zipkin 是一种分布式跟踪系统,用于收集时序数据,这些数据可用于解决微服务架构中的延迟问题。它负责收集和查找这些数据。


启动 Zipkin 的简单方法是通过 Docker:


$ docker run -d -p 9411:9411 openzipkin/zipkin
复制代码


这个应用程序由三个微服务组成,也就是我们在第一篇文章中开发的三个微服务(gateway、inventory、books)。


我们需要对这三个微服务做出修改。


修改 build.gradle,加入跟踪依赖项:


build.gradle
compile "io.micronaut:micronaut-tracing"
复制代码


将以下依赖项添加到 build.gradle 中,这样就可以将跟踪数据发送到 Zipkin。


build.gradle
runtime 'io.zipkin.brave:brave-instrumentation-http' runtime 'io.zipkin.reporter2:zipkin-reporter' compile 'io.opentracing.brave:brave-opentracing'
复制代码


配置跟踪选项:


src/main/resources/application.yml
tracing: zipkin: http: url: http://localhost:9411 enabled: true sampler: probability: 1
复制代码


设置 tracing.zipkin.sample.probability = 1,意思是我们要跟踪所有的请求。在生产环境中,你可能希望设置较低的百分比。


在测试时禁用跟踪:


src/test/resources/application-test.yml
tracing: zipkin: enabled: false
复制代码


只需要很少的配置更改,就可以将分布式跟踪集成到 Micronaut 中。

运行应用程序

现在让我们运行应用程序,看看分布式跟踪集成是否能够正常运行。在第一篇文章中,我们集成了 Consul,用于实现服务发现。因此,在启动微服务之前需要先启动 Zipkin 和 Consul。在微服务启动好以后,它们将在 Consul 服务发现中进行注册。当我们发出请求时,它们会向 Zipkin 发送数据。


Gradle 提供了一个 flag(-parallel)用来启动微服务:


./gradlew -parallel run
复制代码


你可以通过 cURL 命令向三个微服务发起请求:


$ curl http://localhost:8080/api/books[{"isbn":"1680502395","name":"Release It!","stock":3},{"isbn":"1491950358","name":"Building Microservices","stock":2}]
复制代码


然后,你可以通过http://localhost:9411来访问Zipkin UI。

JWT 安全性

Micronaut 提供了多种开箱即用的安全选项,你可以使用基本的身份验证、基于会话的身份验证、JWT 身份验证、Ldap 身份验证,等等。JSON Web Token(JWT)是一种开放的行业标准(RFC 7519)用于在参与方之间声明安全。


Micronaut 提供了开箱即用的用于生成、签名、加密和验证 JWT 令牌的功能。


我们将把 JWT 身份验证集成到我们的应用程序中。

修改 gateway 微服务,让它支持 JWT

gateway 微服务将负责生成和传播 JWT 令牌。


修改 build.gradle,为每个微服务(gateway、inventory 和 books)添加 micronaut-security-jwt 依赖项:


gateway/build.gradle
compile "io.micronaut:micronaut-security-jwt" annotationProcessor "io.micronaut:micronaut-security"
复制代码


修改 application.yml:


gateway/src/main/resources/application.ymlmicronaut:    application:        name: gateway    server:        port: 8080    security:        enabled: true        endpoints:            login:                enabled: true            oauth:                enabled: true        token:            jwt:                enabled: true               signatures:                   secret:                       generator:                           secret: pleaseChangeThisSecretForANewOne            writer:                header:                   enabled: true            propagation:                enabled: true                service-id-regex: "books|inventory"
复制代码


我们做了几个重要的配置变更:


  • micronaut.security.enable = true 启用了安全,并默认为每个端点提供安全保护。

  • micronaut.security.endpoints.login.enable = true 启用了/login 端点,我们将用它进行身份验证。

  • micronaut.security.endpoints.oauth.enable = true 启用了/oauth/access_tokenendpoint 端点,在令牌过期时,我们可以使用它来获取新的 JWT 访问令牌。

  • micronaut.security.jwt.enable = true 启用了 JWT 功能。

  • 我们让应用程序启用签名的 JWT。更多的签名和加密选项,请参阅 JWT 令牌生成文档。

  • micronaut.security.token.propagation.enabled = true 表示启用了令牌传播。这是一种在微服务架构中简化 JWT 或其他令牌安全机制的功能。

  • micronaut.security.writer.header.enabled = ture 启用了一个令牌写入器,它将为开发人员在 HTTP 标头中写入 JWT 令牌。

  • micronaut.security.token.propagation.service-id-regex 设置了一个正则表达式,用于匹配需要进行令牌传播的服务。我们匹配了应用程序中的其他两个服务。


你可以使用 @Secured 注解来配置 Controller 或 Controller Action 级别的访问。


使用 @Secured(“isAuthenticated()”)注解 BookController.java,只允许经过身份验证的用户访问。同时记得使用 @Secured(“isAuthenticated()”)注解 inventory 和 books 微服务的 BookController 类。


/login 端点被调用时,会尝试通过任何可用的 AuthenticationProvider 对用户进行身份验证。为了简单起见,我们将允许两个用户访问,他们是福尔摩斯和华生。创建 SampleAuthenticationProvider:


gateway/src/main/java/example/micronaut/SampleAuthenticationProvider.java
package example.micronaut;
import io.micronaut.context.annotation.Requires; import io.micronaut.context.env.Environment; import io.micronaut.security.authentication.AuthenticationFailed; import io.micronaut.security.authentication.AuthenticationProvider; import io.micronaut.security.authentication.AuthenticationRequest; import io.micronaut.security.authentication.AuthenticationResponse; import io.micronaut.security.authentication.UserDetails; import io.reactivex.Flowable; import org.reactivestreams.Publisher;
import javax.inject.Singleton; import java.util.ArrayList; import java.util.Arrays;
@Requires(notEnv = Environment.TEST) @Singleton public class SampleAuthenticationProvider implements AuthenticationProvider {
@Override public Publisher<AuthenticationResponse> authenticate(AuthenticationRequest authenticationRequest) { if (authenticationRequest.getIdentity() == null) { return Flowable.just(new AuthenticationFailed()); } if (authenticationRequest.getSecret() == null) { return Flowable.just(new AuthenticationFailed()); } if (Arrays.asList("sherlock", "watson").contains(authenticationRequest.getIdentity().toString()) && authenticationRequest.getSecret().equals("elementary")) { return Flowable.just(new UserDetails(authenticationRequest.getIdentity().toString(), new ArrayList<>())); } return Flowable.just(new AuthenticationFailed()); } }
复制代码

修改 inventory 和 books,让它们支持 JWT

对于 inventory 和 books,除了添加 micronaut-security-jwt 依赖项并使用 @Secured 注解控制器之外,我们还需要修改 application.yml,以便能够验证在 gateway 中生成和签名的 JWT 令牌。


修改 application.yml:


inventory/src/main/resources/application.yml
micronaut: application: name: inventory server: port: 8081 security: enabled: true token: jwt: enabled: true signatures: secret: validation: secret: pleaseChangeThisSecretForANewOne
复制代码


请注意,我们使用与 gateway 配置中相同的秘钥,这样就可以验证由 gateway 微服务签名的 JWT 令牌。

运行安全的应用程序

在启动了 Zipkin 和 Consul 之后,你就可以同时启动这三个微服务。Gradle 提供了一个方便的 flag(-parallel):


./gradlew -parallel run
复制代码


你可以运行 cURL 命令,然后会收到 401 错误,表示未授权!


$ curl -I http://localhost:8080/api/books HTTP/1.1 401 UnauthorizedDate: Mon, 1 Oct 2018 18:44:54 GMT transfer-encoding: chunked connection: close
复制代码


我们需要先登录,并获得一个有效的 JWT 访问令牌:


$ curl -X "POST" "http://localhost:8080/login" \-H 'Content-Type: application/json; charset=utf-8' \-d $'{ "username": "sherlock", "password": "password" }' {"username":"sherlock","access_token":"eyJhbGciOiJIUzI1NiJ9.eyJzdWI iOiJzaGVybG9jayIsIm5iZiI6MTUzODQxMjQwOSwicm9sZXMiOltdLCJpc3MiOiJnYX Rld2F5IiwiZXhwIjoxNTM4NDE2MDA5LCJpYXQiOjE1Mzg0MTI0MDl9.1W4CXbN1bJgM CQlCDKJtm7zHWzyZeIr1rHpTuDy6h0","refresh_token":"eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJ zaGVybG9jayIsIm5iZiI6MTUzODQxMjQwOSwicm9sZXMiOltdLCJpc3MiOiJnYXRld2 F5IiwiaWF0IjoxNTM4NDEyNDA5fQ.l72msZKwHmYeLs7T0vKtRxu7_DZr62rPCILNmC 7UEZ4","expires_in":3600,"token_type":"Bearer"}
复制代码


Micronaut 提供了开箱即用的 RFC 6750 Bearer Token 规范支持。我们可以使用从/login 响应标头中获得的 JWT 来调用/api/books 端点。


curl "http://localhost:8080/api/books" \ -H 'Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJzaGVybG9jayIsIm5iZiI6MTUzODQxMjQwOS wicm9sZXMiOltdLCJpc3MiOiJnYXRld2F5IiwiZXhwIjoxNTM4NDE2MDA5LCJpYXQiO jE1Mzg0MTI0MDl9.1W4CXbN1bJgMCQlCDKJtm7zHWz-yZeIr1rHpTuDy6h0'[{"isbn":"1680502395","name":"Release It!","stock":3}, {"isbn":"1491950358","name":"Building Microservices","stock":2}]
复制代码

Serverless

我们将添加一个部署到 AWS Lambda 的功能来验证 books 的 ISBN。


mn create-function example.micronaut.isbn-validator
复制代码


注意:我们使用了 Micronaut CLI 提供的 create-function 命令。

验证

我们将创建一个单例来处理 ISBN 10 验证。


创建一个封装操作的接口:


package example.micronaut;
import javax.validation.constraints.Pattern;
public interface IsbnValidator { boolean isValid(@Pattern(regexp = "\\d{10}") String isbn);}
复制代码


Micronaut 的验证基于标准框架JSR 380,也称为 Bean Validation 2.0。


Hibernate Validator是这个标准的参考实现。


将以下代码段添加到 build.gradle 中:


isbn-validator/build.gradle
compile "io.micronaut.configuration:micronaut-hibernatevalidator"
复制代码


创建一个实现了 IsbnValidator 的单例。


isbn-validator/src/main/java/example/micronaut/DefaultIsbnValidator.java
package example.micronaut;
import io.micronaut.validation.Validated; import javax.inject.Singleton; import javax.validation.constraints.Pattern;
@Singleton @Validated public class DefaultIsbnValidator implements IsbnValidator {
/** * must range from 0 to 10 (the symbol X is used for 10), and must be such that the sum of all the ten digits, each multiplied by its (integer) weight, descending from 10 to 1, is a multiple of 11. * @param isbn 10 Digit ISBN * @return whether the ISBN is valid or not. */ @Override public boolean isValid(@Pattern(regexp = "\\d{10}") String isbn) { char[] digits = isbn.toCharArray(); int accumulator = 0; int multiplier = 10; for (int i = 0; i < digits.length; i++) { char c = digits[i]; accumulator += Character.getNumericValue(c) * multiplier; multiplier--; } return (accumulator % 11 == 0); }}
复制代码


与之前的代码清单一样,你要为需要验证的类添加 @Validated 注解。


创建单元测试:


isbn-validator/src/test/java/example/micronaut/IsbnValidatorTest.java
package example.micronaut;
import io.micronaut.context.ApplicationContext; import io.micronaut.context.DefaultApplicationContext; import io.micronaut.context.env.Environment; import org.junit.AfterClass; import org.junit.BeforeClass; import org.junit.Rule; import org.junit.Test; import org.junit.rules.ExpectedException;import javax.validation.ConstraintViolationException;import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue;
public class IsbnValidatorTest {
private static ApplicationContext applicationContext;
@BeforeClass public static void setupContext() { applicationContext = new DefaultApplicationContext(Environment.TEST).start(); } @AfterClass public static void stopContext() { if (applicationContext!=null) { applicationContext.stop(); } }
@Rule public ExpectedException thrown = ExpectedException.none();
@Test public void testTenDigitValidation() { thrown.expect(ConstraintViolationException.class); IsbnValidator isbnValidator = applicationContext.getBean(IsbnValidator.class); isbnValidator.isValid("01234567891"); }
@Test public void testControlDigitValidationWorks() { IsbnValidator isbnValidator = applicationContext.getBean(IsbnValidator.class); assertTrue(isbnValidator.isValid("1491950358")); assertTrue(isbnValidator.isValid("1680502395")); assertFalse(isbnValidator.isValid("0000502395")); }}
复制代码


如果我们尝试使用十一位数字字符串调用该方法,就会抛出 javax.validation.ConstraintViolationException。

函数的输入和输出

这个函数将接受单个参数(ValidationRequest,它是一个封装了 ISBN 的 POJO)。


isbn-validator/src/main/java/example/micronaut/IsbnValidationRequest.java
package example.micronaut;
public class IsbnValidationRequest { private String isbn; public IsbnValidationRequest() { } public IsbnValidationRequest(String isbn) { this.isbn = isbn; } public String getIsbn() { return isbn; }
public void setIsbn(String isbn) { this.isbn = isbn; }}
复制代码


并返回单个结果(ValidationResponse,一个封装了 ISBN 和一个指示 ISBN 是否有效的布尔值的 POJO)。


isbn-validator/src/main/java/example/micronaut/IsbnValidationResponse.java
package example.micronaut;
public class IsbnValidationResponse { private String isbn; private Boolean valid; public IsbnValidationResponse() { }
public IsbnValidationResponse(String isbn, boolean valid) { this.isbn = isbn; this.valid = valid; } public String getIsbn() { return isbn; } public void setIsbn(String isbn) { this.isbn = isbn; } public Boolean getValid() { return valid; } public void setValid(Boolean valid) { this.valid = valid; }}
复制代码

函数测试

当我们运行 create-function 命令时,Micronaut 会在 src/main/java/example/micronaut 目录创建一个 IsbnValidatorFunction 类。修改它,让它实现 java.util.Function 接口。


isbn-validator/src/main/java/example/micronaut/IsbnValidatorFunction.java
package example.micronaut;
import io.micronaut.function.FunctionBean;import java.util.function.Function; import javax.validation.ConstraintViolationException;
@FunctionBean("isbn-validator") public class IsbnValidatorFunction implements Function<IsbnValidationRequest, IsbnValidationResponse> {
private final IsbnValidator isbnValidator;
public IsbnValidatorFunction(IsbnValidator isbnValidator) { this.isbnValidator = isbnValidator; }
@Override public IsbnValidationResponse apply(IsbnValidationRequest req) { try { return new IsbnValidationResponse(req.getIsbn(), isbnValidator.isValid(req.getIsbn())); } catch(ConstraintViolationException e) { return new IsbnValidationResponse(req.getIsbn(),false); } }}
复制代码


上面的代码做了几件事:


  • 使用 @FunctionBean 注解了一个返回函数的方法。

  • 你可以在函数中使用 Micronaut 的编译时依赖注入。我们通过构造函数注入了 IsbnValidator。


函数也可以作为 Micronaut 应用程序上下文的一部分运行,这样方便进行测试。应用程序已经在类路径中包含了用于测试的 function-web 和 HTTP 服务器依赖项:


isbn-validator/build.gradle
testRuntime "io.micronaut:micronaut-http-server-netty" testRuntime "io.micronaut:micronaut-function-web"
复制代码


要在测试中调用函数,需要修改 IsbnValidatorClient.java


isbn-validator/src/test/java/example/micronaut/IsbnValidatorClient.java
package example.micronaut;
import io.micronaut.function.client.FunctionClient; import io.micronaut.http.annotation.Body; import io.reactivex.Single;import javax.inject.Named;
@FunctionClient public interface IsbnValidatorClient { @Named("isbn-validator") Single<IsbnValidationResponse> isValid(@Body IsbnValidationRequest isbn);}
复制代码


同时修改 IsbnValidatorFunctionTest.java。我们需要测试不同的场景(有效的 ISBN、无效的 ISBN、超过 10 位的 ISBN 和少于 10 位的 ISBN)。


isbn-validator/src/test/java/example/micronaut/IsbnValidatorFunctionTest.java
package example.micronaut;
import io.micronaut.context.ApplicationContext; import io.micronaut.runtime.server.EmbeddedServer; import org.junit.Test; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue;
public class IsbnValidatorFunctionTest {
@Test public void testFunction() { EmbeddedServer server = ApplicationContext.run(EmbeddedServer.class);
IsbnValidatorClient client = server.getApplicationContext().getBean(IsbnValidatorClient.class);
assertTrue(client.isValid(new IsbnValidationRequest("1491950358")).blockingGet().getValid()); assertTrue(client.isValid(new IsbnValidationRequest("1680502395")).blockingGet().getValid()); assertFalse(client.isValid(new IsbnValidationRequest("0000502395")).blockingGet().getValid()); assertFalse(client.isValid(new IsbnValidationRequest("01234567891")).blockingGet().getValid()); assertFalse(client.isValid(new IsbnValidationRequest("012345678")).blockingGet().getValid()); server.close();}
}
复制代码

部署到 AWS Lambda

假设你拥有 Amazon Web Services(AWS)帐户,那么就可以转到 AWS Lambda 并创建一个新功能。


选择 Java 8 运行时。名称为 isbn-validator,并创建一个新的角色表单模板。角色名称为 lambda_basic_execution。



运行./gradlew shadowJar 生成一个 Jar 包。


shadowJar 是 Gradle ShadowJar 插件提供的一个任务。


$ du -h isbn-validator/build/libs/isbn-validator-0.1-all.jar 11M isbn-validator/build/libs/isbn-validator-0.1-all.jar
复制代码


上传 JAR,并指定 Handler。


io.micronaut.function.aws.MicronautRequestStreamHandler 
复制代码


我只分配了 256Mb 内存,超时时间为 25 秒。


从另一个微服务中调用函数

我们将在 gateway 微服务中使用这个 lambda。修改 gateway 微服务中的 build.gradle,添加 micronaut-function-client:


com.amazonaws:aws-java-sdk-lambda dependencies:
build.gradle
compile "io.micronaut:micronaut-function-client" runtime 'com.amazonaws:aws-java-sdk-lambda:1.11.285'
复制代码


修改 src/main/resources/application.yml:


src/main/resources/application.yml
aws: lambda: functions: vat: functionName: isbn-validator qualifer: isbn region: eu-west-3 # Paris Region
复制代码


创建一个接口:


src/main/java/example/micronaut/IsbnValidator.java
package example.micronaut;
import io.micronaut.http.annotation.Body;import io.reactivex.Single;
public interface IsbnValidator { Single<IsbnValidationResponse> validateIsbn(@Body IsbnValidationRequest req); }
复制代码


创建一个 @FunctionClient:


src/main/java/example/micronaut/FunctionIsbnValidator.java
package example.micronaut;
import io.micronaut.context.annotation.Requires; import io.micronaut.context.env.Environment; import io.micronaut.function.client.FunctionClient; import io.micronaut.http.annotation.Body; import io.reactivex.Single;import javax.inject.Named;
@FunctionClient @Requires(notEnv = Environment.TEST) public interface FunctionIsbnValidator extends IsbnValidator { @Override @Named("isbn-validator") Single<IsbnValidationResponse> validateIsbn(@Body IsbnValidationRequest req);}
复制代码


关于上面这些代码有几点值得注意:


  • FunctionClient 注解可以在接口上应用引入通知(introduction advice),这样接口定义的方法就会成为远程函数的调用者。

  • 使用函数名 isbn-validator,与 application.yml 定义的一样。


最后一步是修改 gateway 的 BookController,让它调用函数。


src/main/java/example/micronaut/BooksController.java
package example.micronaut;
import io.micronaut.http.annotation.Controller; import io.micronaut.http.annotation.Get; import io.micronaut.security.annotation.Secured; import io.reactivex.Flowable;
import java.util.List;
@Secured("isAuthenticated()")@Controller("/api") public class BooksController { private final BooksFetcher booksFetcher; private final InventoryFetcher inventoryFetcher; private final IsbnValidator isbnValidator; public BooksController(BooksFetcher booksFetcher, InventoryFetcher inventoryFetcher, IsbnValidator isbnValidator) { this.booksFetcher = booksFetcher; this.inventoryFetcher = inventoryFetcher; this.isbnValidator = isbnValidator; }
@Get("/books") Flowable<Book> findAll() { return booksFetcher.fetchBooks() .flatMapMaybe(b -> isbnValidator.validateIsbn(new IsbnValidationRequest(b.getIsbn())) .filter(IsbnValidationResponse::getValid) .map(isbnValidationResponse -> b) ) .flatMapMaybe(b -> inventoryFetcher.inventory(b.getIsbn()) .filter(stock -> stock > 0) .map(stock -> { b.setStock(stock); return b; }) ); }}
复制代码


我们通过构造函数注入了 IsbnValidator。调用远程函数对程序员来说是透明的。

结论

下面的图片说明了我们在这一系列文章中开发的应用程序:


  • 我们有三个微服务(一个 Java 服务、一个 Groovy 服务和一个 Kotlin 服务)。

  • 这些微服务使用 Consul 进行服务发现。

  • 这些微服务使用 Zipkin 作为分布式跟踪服务。

  • 我们添加了第四个微服务,一个部署到 AWS Lambda 的功能。

  • 微服务之间的通信是安全的。每个请求在 Authorization Http 标头中包含一个 JWT 令牌就可以通过网络。JWT 令牌通过内部请求自动传播。


关于 Micronaut 的更多内容,请访问官方网站

关于作者


Sergio del AmoCaballero 是一名手机应用程序(iOS、Android,后端由 Grails/Micronaut 驱动)开发者。自 2015 年起,Sergio del Amo 为 Groovy 生态系统和微服务维护着一个新闻源Groovy Calamari


查看英文原文Micronaut Tutorial: Part 2: Easy Distributed Tracing, JWT Security and AWS Lambda Deployment


2018-12-15 09:001912
用户头像

发布了 731 篇内容, 共 433.9 次阅读, 收获喜欢 1997 次。

关注

评论 1 条评论

发布
暂无评论
发现更多内容

Final Cut Pro 中文基础教程:多机位剪辑

Rose

用云手机打造tiktok账号需要注意些什么?

Ogcloud

云手机 海外云手机 tiktok云手机 云手机海外版

AE如何导入LUTS呢 ?After Effects导入lut使用详细教程

Rose

XMind for mac XMind思维导图 v24.01中文版

Rose

一种高效解决12306第三方抢票不公平乱收费体验差的新技术

巨公摇号创始人钱庆照

12306 第三方付费抢票 随机信标 巨公摇号 抢火车票

海外云手机:入局海外市场的最佳利器

Ogcloud

云手机 海外云手机 云手机海外版 国外云手机

microsoft 365永久激活密钥

Rose

开发者集结令丨Farris-Vue前端组件创新挑战赛开赛啦!

inBuilder低代码平台

前端 低代码 开发

Dock栏快速启动程序uDock for Mac 激活版

影影绰绰一往直前

Easy Data Transform for Mac v1.46.3激活版下载 强大数据转换工具

影影绰绰一往直前

全世界 LoRA 训练脚本,联合起来!

不在线第一只蜗牛

Java 架构 前端开发

好“云”来!盘点春节与云计算息息相关的那些事儿

Finovy Cloud

云计算 云时代

前端开发CSS实用的技巧有哪些

小魏写代码

从技术到管理:如何避免失去专业指导能力的陷阱?

码哥字节

程序员 架构师 职业发展

Tiktok云手机是什么,做tiktok养号有什么优势?

Ogcloud

云手机 海外云手机 tiktok云手机 云手机海外版

EndNote 21 for Mac(文献管理软件) v21.2激活版

iMac小白

将Excel转换为HTML:Easy Data Transform for mac

Rose

数据库管理软件 DBeaverUE for Mac v23.3.4旗舰激活版

影影绰绰一往直前

photoshop2024硬件要求

Rose

云计算 - 弹性计算技术全解与实践

快乐非自愿限量之名

云计算 物联网 弹性计算

十八张图带你入门实时监控系统HertzBeat

EquatorCoco

监控系统

macOS Big Sur 11安装包(macOS11系统下载) v11.7.10正式版

Rose

服务器操作卡,出现蓝屏、死机,该怎么解决

德迅云安全杨德俊

为什么VPS比传统虚拟空间更受欢迎?深度解析!

一只扑棱蛾子

VPS

运营商数智化缩影:一部哑资源的资源管理史

鲸品堂

网络 资源 运营商 企业号 2 月 PK 榜

VMware Fusion Pro 13(VM虚拟机)中文破解版安装教程

Rose

可视化代码编辑器Blocs for mac v5.2.1激活版下载

影影绰绰一往直前

ai全称是什么?好用的AI软件有哪些?这14款一定要知道。

彭宏豪95

AI 在线白板 AIGC AI绘画 效率软件

告别 GPU 焦虑,玩转极致性价比的 CPU 文生图

阿里巴巴云原生

阿里云 Kubernetes 云原生

博客生成编辑器MWeb Pro for Mac v4.5.6中文激活版

影影绰绰一往直前

解密 ARMS 持续剖析:如何用一个全新视角洞察应用的性能瓶颈?

阿里巴巴云原生

阿里云 云原生

Micronaut教程(二):分布式跟踪、JWT安全和AWS Lambda部署_语言 & 开发_Sergio del Amo Caballero_InfoQ精选文章