经过一段时间的开发与测试,终于发布了Lms框架的第一个正式版本(1.0.0版本),并给出了lms框架的样例项目lms.samples。本文通过对lms.samples的介绍,简述如何通过lms框架快速的构建一个微服务的业务框架,并进行应用开发。

lms.samples项目基本介绍

lms.sample项目由三个独立的微服务应用模块组成:account、stock、order和一个网关项目gateway构成。

业务应用模块

每个独立的微服务应用采用模块化设计,主要由如下几部分组成:

  1. 主机(Host): 主要用于托管微服务应用本身,主机通过引用应用服务项目(应用接口的实现),托管微服务应用,通过托管应用服务,在主机启动的过程中,向服务注册中心注册服务路由。

  2. 应用接口层(Application.Contracts): 用于定义应用服务接口,通过应用接口,该微服务模块与其他微服务模块或是网关进行rpc通信的能力。在该项目中,除了定义应用服务接口之前,一般还定义与该应用接口相关的DTO对象。应用接口除了被该微服务应用项目引用,并实现应用服务之前,还可以被网关或是其他微服务模块引用。网关或是其他微服务项目通过应用接口生成的代理与该微服务模块通过rpc进行通信。

  3. 应用服务层(Application): 应用服务是该微服务定义的应用接口的实现。应用服务与DDD传统分层架构的应用层的概念一致。主要负责外部通信与领域层之间的协调。一般地,应用服务进行业务流程控制,但是不包含业务逻辑的实现。

  4. 领域层(Domain): 负责表达业务概念,业务状态信息以及业务规则,是该微服务模块的业务核心。一般地,在该层可以定义聚合根、实体、领域服务等对象。

  5. 领域共享层(Domain.Shared): 该层用于定义与领域对象相关的模型、实体等相关类型。不包含任何业务实现,可以被其他微服务引用。

  6. 数据访问(DataAccess)层: 该层一般用于封装数据访问相关的对象。例如:仓库对象、 SqlHelper、或是ORM相关的类型等。在lms.samples中,通过efcore实现数据的读写操作。

()

服务聚合与网关

lms框架不允许服务外部与微服务主机直接通信,应用请求必须通过http请求到达网关,网关通过lms提供的中间件解析到服务条目,并通过rpc与集群内部的微服务进行通信。所以,如果服务需要与集群外部进行通信,那么,开发者定义的网关必须要引用各个微服务模块的应用接口层;以及必须要使用lms相关的中间件。

开发环境

  1. .net版本: 5.0.101

  2. lms版本: 1.0.0

  3. IDE: (1) visual studio 最新版 (2) Rider(推荐)

主机与应用托管

主机的创建步骤

通过lms框架创建一个业务模块非常方便,只需要通过如下4个步骤,就可以轻松的创建一个lms应用业务模块。

  1. 创建项目

创建控制台应用(Console Application)项目,并且引用Silky.Lms.NormHost包。

dotnet add package Silky.Lms.NormHost --version 1.0.0
  1. 应用程序入口与主机构建

main方法中,通用.net的主机Host构建并注册lms微服务。在注册lms微服务时,需要指定lms启动的依赖模块。

一般地,如果开发者不需要额外依赖其他模块,也无需在应用启动或停止时执行方法,那么您可以直接指定NormHostModule模块。

 public class Program
{
public static async Task Main(string[] args)
{
await CreateHostBuilder(args).Build().RunAsync();
} private static IHostBuilder CreateHostBuilder(string[] args)
{
return Host.CreateDefaultBuilder(args)
.RegisterLmsServices<NormHostModule>()
;
}
}
  1. 配置文件

lms框架支持yml或是json格式作为配置文件。通过appsettings.yml对lms框架进行统一配置,通过appsettings.${Environment}.yml对不同环境变量下的配置项进行设置。

开发者如果直接通过项目的方式启动应用,那么可以通过Properties/launchSettings.jsonenvironmentVariables.DOTNET_ENVIRONMENT环境变量。如果通过docker-compose的方式启动应用,那么可以通过.env设置DOTNET_ENVIRONMENT环境变量。

为保证配置文件有效,开发者需要显式的将配置文件拷贝到项目生成目录下。

  1. 引用应用服务层和数据访问层

一般地,主机项目需要引用该微服务模块的应用服务层和数据访问层。只有主机引用应用服务层,主机在启动时,才会生成服务条目的路由,并且将服务路由注册到服务注册中心。

一个典型的主机项目文件如下所示:

<Project Sdk="Microsoft.NET.Sdk">

    <PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net5.0</TargetFramework>
</PropertyGroup> <ItemGroup>
<PackageReference Include="Silky.Lms.NormHost" Version="$(LmsVersion)" />
</ItemGroup> <ItemGroup>
<None Update="appsettings.yml">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="appsettings.Production.yml">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="appsettings.Development.yml">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
</ItemGroup> <ItemGroup>
<ProjectReference Include="..\Lms.Account.Application\Lms.Account.Application.csproj" />
<ProjectReference Include="..\Lms.Account.EntityFrameworkCore\Lms.Account.EntityFrameworkCore.csproj" />
</ItemGroup>
</Project>

配置

一般地,一个微服务模块的主机必须要配置:服务注册中心、分布式锁链接、分布式缓存地址、集群rpc通信token、数据库链接地址等。

如果使用docker-compose来启动和调试应用的话,那么,rpc配置节点下的的host和port可以缺省,因为生成的每个容器的都有自己的地址和端口号。

如果直接通过项目的方式启动和调试应用的话,那么,必须要配置rpc节点下的port,每个微服务模块的主机应用有自己的端口号。

lms框架的必要配置如下所示:

rpc:
host: 0.0.0.0
rpcPort: 2201
token: ypjdYOzNd4FwENJiEARMLWwK0v7QUHPW
registrycenter:
connectionStrings: 127.0.0.1:2181,127.0.0.1:2182,127.0.0.1:2183;127.0.0.1:2184,127.0.0.1:2185,127.0.0.1:2186 # 使用分号;来区分不同的服务注册中心
registryCenterType: Zookeeper
distributedCache:
redis:
isEnabled: true
configuration: 127.0.0.1:6379,defaultDatabase=0
lock:
lockRedisConnection: 127.0.0.1:6379,defaultDatabase=1
connectionStrings:
default: server=127.0.0.1;port=3306;database=account;uid=root;pwd=qwe!P4ss;

应用接口

应用接口定义

一般地,在应用接口层开发者需要安装Silky.Lms.Rpc包。如果该微服务模块还涉及到分布式事务,那么还需要安装Silky.Lms.Transaction.Tcc,当然,您也可以选择在应用接口层安装Silky.Lms.Transaction包,在应用服务层安装Silky.Lms.Transaction.Tcc包。

  1. 开发者只需要在应用接口通过ServiceRouteAttribute特性对应用接口进行直接即可。

  2. Lms约定应用接口应当以IXxxAppService命名,这样,服务条目生成的路由则会以api/xxx形式生成。当然这并不是强制的。

  3. 每个应用接口的方法都对应着一个服务条目,服务条目的Id为: 方法的完全限定名 + 参数名

  4. 您可以在应用接口层对方法的缓存、路由、服务治理、分布式事务进行相关配置。该部分内容请参考官方文档

  5. 网关或是其他模块的微服务项目需要引用服务应用接口项目或是通过nuget的方式安装服务应用接口生成的包。

  6. [Governance(ProhibitExtranet = true)]可以标识一个方法禁止与集群外部进行通信,通过网关也不会生成swagger文档。

  7. 应用接口方法生成的WebApi支持restful API风格。Lms支持通过方法的约定命名生成对应http方法请求的WebApi。您当然开发者也可以通过HttpMethodAttribute特性对某个方法进行注解。

一个典型的应用接口的定义

    /// <summary>
/// 账号服务
/// </summary>
[ServiceRoute]
public interface IAccountAppService
{
/// <summary>
/// 新增账号
/// </summary>
/// <param name="input">账号信息</param>
/// <returns></returns>
Task<GetAccountOutput> Create(CreateAccountInput input); /// <summary>
/// 通过账号名称获取账号
/// </summary>
/// <param name="name">账号名称</param>
/// <returns></returns>
[GetCachingIntercept("Account:Name:{0}")]
[HttpGet("{name:string}")]
Task<GetAccountOutput> GetAccountByName([CacheKey(0)] string name); /// <summary>
/// 通过Id获取账号信息
/// </summary>
/// <param name="id">账号Id</param>
/// <returns></returns>
[GetCachingIntercept("Account:Id:{0}")]
[HttpGet("{id:long}")]
Task<GetAccountOutput> GetAccountById([CacheKey(0)] long id); /// <summary>
/// 更新账号信息
/// </summary>
/// <param name="input"></param>
/// <returns></returns>
[UpdateCachingIntercept( "Account:Id:{0}")]
Task<GetAccountOutput> Update(UpdateAccountInput input); /// <summary>
/// 删除账号信息
/// </summary>
/// <param name="id">账号Id</param>
/// <returns></returns>
[RemoveCachingIntercept("GetAccountOutput","Account:Id:{0}")]
[HttpDelete("{id:long}")]
Task Delete([CacheKey(0)]long id); /// <summary>
/// 订单扣款
/// </summary>
/// <param name="input"></param>
/// <returns></returns>
[Governance(ProhibitExtranet = true)]
[RemoveCachingIntercept("GetAccountOutput","Account:Id:{0}")]
[Transaction]
Task<long?> DeductBalance(DeductBalanceInput input);
}

应用服务--应用接口的实现

  1. 应用服务层只需要引用应用服务接口层以及领域服务层,并实现应用接口相关的方法。

  2. 确保该微服务模块的主机引用了该模块的应用服务层,这样主机才能够托管该应用本身。

  3. 应用服务层可以通过引用其他微服务模块的应用接口层项目(或是安装nuget包,取决于开发团队的项目管理方法),与其他微服务模块进行rpc通信。

  4. 应用服务层需要依赖领域服务,通过调用领域服务的相关接口,实现该模块的核心业务逻辑。

  5. DTO到实体对象或是实体对DTO对象的映射关系可以在该层指定映射关系。

一个典型的应用服务的实现如下所示:

public class AccountAppService : IAccountAppService
{
private readonly IAccountDomainService _accountDomainService; public AccountAppService(IAccountDomainService accountDomainService)
{
_accountDomainService = accountDomainService;
} public async Task<GetAccountOutput> Create(CreateAccountInput input)
{
var account = input.MapTo<Domain.Accounts.Account>();
account = await _accountDomainService.Create(account);
return account.MapTo<GetAccountOutput>();
} public async Task<GetAccountOutput> GetAccountByName(string name)
{
var account = await _accountDomainService.GetAccountByName(name);
return account.MapTo<GetAccountOutput>();
} public async Task<GetAccountOutput> GetAccountById(long id)
{
var account = await _accountDomainService.GetAccountById(id);
return account.MapTo<GetAccountOutput>();
} public async Task<GetAccountOutput> Update(UpdateAccountInput input)
{
var account = await _accountDomainService.Update(input);
return account.MapTo<GetAccountOutput>();
} public Task Delete(long id)
{
return _accountDomainService.Delete(id);
} [TccTransaction(ConfirmMethod = "DeductBalanceConfirm", CancelMethod = "DeductBalanceCancel")]
public async Task<long?> DeductBalance(DeductBalanceInput input)
{
var account = await _accountDomainService.GetAccountById(input.AccountId);
if (input.OrderBalance > account.Balance)
{
throw new BusinessException("账号余额不足");
}
return await _accountDomainService.DeductBalance(input, TccMethodType.Try);
} public Task DeductBalanceConfirm(DeductBalanceInput input)
{
return _accountDomainService.DeductBalance(input, TccMethodType.Confirm);
} public Task DeductBalanceCancel(DeductBalanceInput input)
{
return _accountDomainService.DeductBalance(input, TccMethodType.Cancel);
}
}

领域层--微服务的核心业务实现

  1. 领域层是该微服务模块核心业务处理的模块,一般用于定于聚合根、实体、领域服务、仓储等业务对象。

  2. 领域层引用该微服务模块的应用接口层,方便使用dto对象。

  3. 领域层可以通过引用其他微服务模块的应用接口层项目(或是安装nuget包,取决于开发团队的项目管理方法),与其他微服务模块进行rpc通信。

  4. 领域服务必须要直接或间接继承ITransientDependency接口,这样,该领域服务才会被注入到ioc容器。

  5. lms.samples 项目使用TanvirArjel.EFCore.GenericRepository包实现数据的读写操作。

一个典型的领域服务的实现如下所示:

  public class AccountDomainService : IAccountDomainService
{
private readonly IRepository _repository;
private readonly IDistributedCache<GetAccountOutput, string> _accountCache; public AccountDomainService(IRepository repository,
IDistributedCache<GetAccountOutput, string> accountCache)
{
_repository = repository;
_accountCache = accountCache;
} public async Task<Account> Create(Account account)
{
var exsitAccountCount = await _repository.GetCountAsync<Account>(p => p.Name == account.Name);
if (exsitAccountCount > 0)
{
throw new BusinessException($"已经存在{account.Name}名称的账号");
} exsitAccountCount = await _repository.GetCountAsync<Account>(p => p.Email == account.Email);
if (exsitAccountCount > 0)
{
throw new BusinessException($"已经存在{account.Email}Email的账号");
} await _repository.InsertAsync<Account>(account);
return account;
} public async Task<Account> GetAccountByName(string name)
{
var accountEntry = _repository.GetQueryable<Account>().FirstOrDefault(p => p.Name == name);
if (accountEntry == null)
{
throw new BusinessException($"不存在名称为{name}的账号");
} return accountEntry;
} public async Task<Account> GetAccountById(long id)
{
var accountEntry = _repository.GetQueryable<Account>().FirstOrDefault(p => p.Id == id);
if (accountEntry == null)
{
throw new BusinessException($"不存在Id为{id}的账号");
} return accountEntry;
} public async Task<Account> Update(UpdateAccountInput input)
{
var account = await GetAccountById(input.Id);
if (!account.Email.Equals(input.Email))
{
var exsitAccountCount = await _repository.GetCountAsync<Account>(p => p.Email == input.Email);
if (exsitAccountCount > 0)
{
throw new BusinessException($"系统中已经存在Email为{input.Email}的账号");
}
} if (!account.Name.Equals(input.Name))
{
var exsitAccountCount = await _repository.GetCountAsync<Account>(p => p.Name == input.Name);
if (exsitAccountCount > 0)
{
throw new BusinessException($"系统中已经存在Name为{input.Name}的账号");
}
} await _accountCache.RemoveAsync($"Account:Name:{account.Name}");
account = input.MapTo(account);
await _repository.UpdateAsync(account);
return account;
} public async Task Delete(long id)
{
var account = await GetAccountById(id);
await _accountCache.RemoveAsync($"Account:Name:{account.Name}");
await _repository.DeleteAsync(account);
} public async Task<long?> DeductBalance(DeductBalanceInput input, TccMethodType tccMethodType)
{
var account = await GetAccountById(input.AccountId);
var trans = await _repository.BeginTransactionAsync();
BalanceRecord balanceRecord = null;
switch (tccMethodType)
{
case TccMethodType.Try:
account.Balance -= input.OrderBalance;
account.LockBalance += input.OrderBalance;
balanceRecord = new BalanceRecord()
{
OrderBalance = input.OrderBalance,
OrderId = input.OrderId,
PayStatus = PayStatus.NoPay
};
await _repository.InsertAsync(balanceRecord);
RpcContext.GetContext().SetAttachment("balanceRecordId",balanceRecord.Id);
break;
case TccMethodType.Confirm:
account.LockBalance -= input.OrderBalance;
var balanceRecordId1 = RpcContext.GetContext().GetAttachment("orderBalanceId")?.To<long>();
if (balanceRecordId1.HasValue)
{
balanceRecord = await _repository.GetByIdAsync<BalanceRecord>(balanceRecordId1.Value);
balanceRecord.PayStatus = PayStatus.Payed;
await _repository.UpdateAsync(balanceRecord);
}
break;
case TccMethodType.Cancel:
account.Balance += input.OrderBalance;
account.LockBalance -= input.OrderBalance;
var balanceRecordId2 = RpcContext.GetContext().GetAttachment("orderBalanceId")?.To<long>();
if (balanceRecordId2.HasValue)
{
balanceRecord = await _repository.GetByIdAsync<BalanceRecord>(balanceRecordId2.Value);
balanceRecord.PayStatus = PayStatus.Cancel;
await _repository.UpdateAsync(balanceRecord);
}
break;
} await _repository.UpdateAsync(account);
await trans.CommitAsync();
await _accountCache.RemoveAsync($"Account:Name:{account.Name}");
return balanceRecord?.Id;
}
}

数据访问(EntityFrameworkCore)--通过efcore实现数据读写

  1. lms.samples项目使用orm框架efcore进行数据读写。

  2. lms提供了IConfigureService,通过继承该接口即可使用IServiceCollection的实例指定数据上下文对象和注册仓库服务。

  public class EfCoreConfigureService : IConfigureService
{
public void ConfigureServices(IServiceCollection services, IConfiguration configuration)
{
services.AddDbContext<OrderDbContext>(opt =>
opt.UseMySql(configuration.GetConnectionString("Default"),
ServerVersion.AutoDetect(configuration.GetConnectionString("Default"))))
.AddGenericRepository<OrderDbContext>(ServiceLifetime.Transient)
;
} public int Order { get; } = 1;
}
  1. 主机项目需要显式的引用该项目,只有这样,该项目的ConfigureServices才会被调用。

  2. 数据迁移,请参考

应用启动与调试

获取源码

  1. 使用git 克隆lms项目源代码,lms.samples存放在samples目录下
# github
git clone https://github.com/liuhll/lms.git # gitee
git clone https://gitee.com/liuhll2/lms.git

必要的前提

  1. 服务注册中心zookeeper

  2. 缓存服务redis

  3. mysql数据库

如果您电脑已经安装了docker以及docker-compose命令,那么您只需要进入samples\docker-compose\infrastr目录下,打开命令行工作,执行如下命令就可以自动安装zookeeperredismysql等服务:

docker-compose -f .\docker-compose.mysql.yml -f .\docker-compose.redis.yml -f .\docker-compose.zookeeper.yml up -d

数据库迁移

需要分别进入到各个微服务模块下的EntityFrameworkCore项目(例如:),执行如下命令:

dotnet ef database update

例如: 需要迁移account模块的数据库如下所示:

order模块和stock模块与account模块一致,在服务运行前都需要通过数据库迁移命令生成相关数据库。

  1. 数据库迁移指定数据库连接地址默认指定的是appsettings.Development.yml中配置的,您可以通过修改该配置文件中的connectionStrings.default配置项来指定自己的数据库服务地址。

  2. 如果没有dotnet ef命令,则需要通过dotnet tool install --global dotnet-ef安装ef工具,请[参考] (https://docs.microsoft.com/zh-cn/ef/core/get-started/overview/install)

以项目的方式启动和调试

使用visual studio作为开发工具

进入到samples目录下,使用visual studio打开lms.samples.sln解决方案,将项目设置为多启动项目,并将网关和各个模块的微服务主机设置为启动项目,如下图:

()

设置完成后直接启动即可。

使用rider作为开发工具

  1. 进入到samples目录下,使用rider打开lms.samples.sln解决方案,打开各个微服务模块下的Properties/launchSettings.json,点击图中绿色的箭头即可启动项目。

()

  1. 启动网关项目后,可以看到应用接口的服务条目生成的swagger api文档 http://localhost:5000/swagger

()

  1. 默认的环境变量为: Development,如果需要修改环境变量的话,可以通过Properties/launchSettings.json下的environmentVariables节点修改相关环境变量,请参考在 ASP.NET Core 中使用多个环境

  2. 数据库连接、服务注册中心地址、以及redis缓存地址和分布式锁连接等配置项可以通过修改appsettings.Development.yml配置项自定义指定。

以docker-compose的方式启动和调试

  1. 进入到samples目录下,使用visual studio打开lms.samples.dockercompose.sln解决方案,将docker-compose设置为启动项目,即可启动和调式。

  2. 应用启动成功后,打开: http://127.0.0.1/swagger,即可看到swagger api文档

()

  1. 以docker-compose的方式启动和调试,则指定的环境变量为:ContainerDev

  2. 数据库连接、服务注册中心地址、以及redis缓存地址和分布式锁连接等配置项可以通过修改appsettings.ContainerDev.yml配置项自定义指定,配置的服务连接地址不允许为: 127.0.0.1或是localhost

测试和调式

服务启动成功后,您可以通过写入/api/account-post接口和/api/product-post新增账号和产品,然后通过/api/order-post接口进行测试和调式。

开源地址

github: https://github.com/liuhll/lms

gitee: https://gitee.com/liuhll2/lms

通过lms.samples熟悉lms微服务框架的使用的更多相关文章

  1. silky微服务框架服务注册中心介绍

    目录 服务注册中心简介 服务元数据 主机名称(hostName) 服务列表(services) 终结点 时间戳 使用Zookeeper作为服务注册中心 使用Nacos作为服务注册中心 使用Consul ...

  2. MicroService.Core简易微服务框架《一、简介》

    MicroService.Core MicroService.Core 的初衷是为了方便的创建一个微服务, 可作为 Windows Service 或者控制台模式启动. 它底层使用了 OWin 自托管 ...

  3. 微服务框架Lagom介绍之一

    背景 Lagom是JAVA系下响应式 微服务框架,在阅读本文之前请先阅读微服务架构设计,Lagom与其他微服务框架相比,与众不同的特性包括: 目前,大多数已有的微服务框架关注于简化单个微服务的构建-- ...

  4. php微服务框架 PHP-MSF 的容器部署和使用

    评论:1 · 阅读:8412· 喜欢:1 一.需求 PHP-msf 是 Carema360 开发的 PHP 微服务框架,目前我没有实际用过,但是市面上的微服务框架要么在推崇 Spring 系,要么是  ...

  5. Java微服务框架一览

    引言:本文首先简单介绍了微服务的概念以及使用微服务所能带来的优势,然后结合实例介绍了几个常见的Java微服务框架. 微服务在开发领域的应用越来越广泛,因为开发人员致力于创建更大.更复杂的应用程序,而这 ...

  6. 日调度万亿次,微服务框架TSF大规模应用——云+未来峰会开发者专场回顾

    欢迎大家前往腾讯云+社区,获取更多腾讯海量技术实践干货哦~ 演讲者:张浩 腾讯云中间件产品负责人 背景:众多开发者中,一定经历类似的甜蜜烦恼,就是当线上业务规模越来越大,系统分支发展越来越多的时候,初 ...

  7. 十分钟搭建微服务框架(SpringBoot +Dubbo+Docker+Jenkins源码)

    本文将以原理+实战的方式,首先对“微服务”相关的概念进行知识点扫盲,然后开始手把手教你搭建这一整套的微服务系统. 这套微服务框架能干啥? 这套系统搭建完之后,那可就厉害了: 微服务架构 你的整个应用程 ...

  8. 手把手0基础项目实战(一)——教你搭建一套可自动化构建的微服务框架(SpringBoot+Dubbo+Docker+Jenkins)...

    原文:手把手0基础项目实战(一)--教你搭建一套可自动化构建的微服务框架(SpringBoot+Dubbo+Docker+Jenkins)... 本文你将学到什么? 本文将以原理+实战的方式,首先对& ...

  9. 开箱即用的微服务框架 Go-zero(进阶篇)

    之前我们简单介绍过 Go-zero 详见<Go-zero:开箱即用的微服务框架>.这次我们从动手实现一个 Blog 项目的用户模块出发,详细讲述 Go-zero 的使用. 特别说明本文涉及 ...

随机推荐

  1. node.js & Unbuntu Linux & nvm & npm

    node.js & Unbuntu Linux & nvm & npm https://websiteforstudents.com/install-the-latest-no ...

  2. Power Query 导入多源数据

    导入方法: 导入数据库文件: 修改加载方式: 其他类型数据处理方式类似

  3. Python序列之列表(一)

    在Python中,列表是一种常用的序列,接下来我来讲一下关于Python中列表的知识. 列表的创建 Python中有多种创建列表的方式 1.使用赋值运算符直接赋值创建列表 在创建列表时,我们直接使用赋 ...

  4. WPF 之绘画(十一)

    一.WPF 绘画 WPF 可以绘制线段(Line).矩形(Rectange).椭圆(Ellipse).路径(Path).具体使用如下所示: <!--(1)线段:Line--> <Li ...

  5. oracle check datapump jobs

    reference: https://asktom.oracle.com/pls/apex/asktom.search?tag=getting-ora-31626-job-does-not-exist ...

  6. MarkDown简单语法回顾

    写在前面: 本文是我的第一篇博客文章,希望与大家共同交流.分享我们热爱的技术,努力成为一名优秀的软件工程师! 进入正文 使用MarkDown记笔记已经有些时候了,编辑器是使用的sublime text ...

  7. JAVA基础(一)—— 基础类型与面向对象

    JAVA基础(一)--基础类型与面向对象 1 数据类型 基本类型 byte short int long float double boolean char n 8 16 32 64 32 64 tr ...

  8. Git使用的常用场景

    场景一 小张作为一个开发人员,刚进团队,发现团队是使用git进行代码管理的,现在需要去初始化团队的代码仓库以及新增提交自己修改的一部分代码 1.克隆远程仓库 git clone <ssh> ...

  9. 【资源下载】Linux下的Hi3861一站式鸿蒙开发烧录(附工具)

    下载附件 2021春节前夕,华为发布了 HUAWEI DevEco Device Tool 2.0 Beta1,整体提供了异常强大的功能.得知消息后,我在第一时间带着无比兴奋的心情下载尝鲜,但结果却是 ...

  10. Slenium详解

    Slenium介绍 Selenium 是一个 Web 应用的自动化框架. 通过它,我们可以写出自动化程序,像人一样在浏览器里操作web界面. 比如点击界面按钮,在文本框中输入文字 等操作. 而且还能从 ...