CoreDNS解析异常记录

CoreDNS解析异常记录 

异常情况:集群是用kubespray部署的4个worknode,coredns默认部署2个deployment。今天发现部署了coredns的node上的pod正常解析内部域名,而另外2个未运行coredns的node却无法解析。

 配置文件:

 下图中我们看到coredns2个pod分别在node1与node2上,只要分配到这2节点上的deployment都可以正常解析。

其他节点无法解析:

 处理过程:

正常来说所有的pod都是通过coredns来进行集群内域名解析的,我也搞不清楚为啥其他两个node没有跑coredns则就无法解析后面再研究。所以我临时的解决方法是扩容coredns让每个node都跑。

1、修改 ConfigMap 中的 dns-autoscaler(coredns自动扩容保证高可用)

kubectl edit configmap dns-autoscaler --namespace=kube-system

2、修改key:linear

  • coresPerReplica: 按照核心数目来计算副本集(replicas = cores / coresPerReplica)
  • nodesPerReplica:按照节点数目来计算副本集(replicas = nodes / nodesPerReplica)
  • min:最小副本数(默认为2,我先有4个节点改为4)
  • max:最大副本数
  • preventSinglePointFailure:防止单点故障

公式

replicas = max( ceil( cores × 1/coresPerReplica ) , ceil( nodes × 1/nodesPerReplica ) )

 

 

 

[Abp vNext 入坑分享] – 2.简化项目结构

一、简要说明

本篇文章根据我自己的需要对项目结果进行简化,让项目结构更符合我自己的要求,同时让项目跑起来。仅供参考

二、具体步骤

2.1卸载掉对我来说目前使用不上的项目,identityserver,mongodb,httpapi.client,以及对应的test项目

 

 

 

 

2.2删除掉下图红框中的包与类文件,由于我后期会创建一个独立的项目来做migrations,所以不需要在HttpApi.Host里面直接使用EF相关的操作,而且个人认为这样会模糊掉abpvnext的层级,混乱了层级的职责。

 

 

 

上图的报错都是由于我删除了相关的包引起的,因此跳转到相关文件中,把所有的报错行,全部删除。同时由于我没有启用redis的服务,所以要把下图的redis服务也先行注释掉。

2.3将启动模式修改成:如下图,同时把原来输出日志到文件的模式修改成:console(),以便在控制台时能很直观的看到Log。调试项目,则可以启动成功了。

 

 

 

 

 

2.4在src下面增加DbMigrations类库,注意:此处只能选择.netcore类型的类库,不能是只属于netstandard这样会导致无法使用。创建后的样子如下,这样就可以使用此项目进行migration操作了,且不会影响主线代码。

 

 

 

1.关于DbM_LearnDbContext这个类,如果你能保证整个项目的所有开发人员都只能使用codefirst进行开发的话,可以直接继承主的LearnDbContext,这样LearnDbContext的所有DbSet都会得到继承,则不需要再重复写dbset。

2.若是codefirstdbfirst混用的情况,则不要继承

3.单纯dbfirst的话,此项目可以去掉

Unity ML-agents 一、初次尝试

前言

曾在高二寒假的时候,跟表哥在外面玩,当时他问我有没有想过以后要做什么,我愣了一下,回答不上来。是的,从没想过以后要做什么,只是一直在完成学校、老师安排的任务,于是那之后半年,我一直在思考,大学要学什么。在大二下期中之后,我觉得自己还是对游戏更感兴趣,便想到以后想做游戏。于是,高考后填志愿,填的都是计算机专业。在大一的时候,自学了一段时间的 Unity,到大二在实验室接触强化学习之后,就想着用 RL 来做游戏 AI,但后来一直在做数据挖掘相关的内容,基本上以参加比赛为主。直到去年参加上海的谷歌开发者节,了解到 ML-agents 之后,就十分的想尝试。

然后由于疫情,直到现在还在家里咸鱼,前段时间一边咸鱼一边投简历,奈何自己水平太低又偏偏想投算法岗,直到现在也没有几次面试机会 orz。最近就想继续当年未完成的 Unity 的学习,顺便学习 ML-agents,回到原点,重新出发。

初试 ML-agents

环境配置

既然要尝试,肯定免不了环境配置
目前我的环境为:

  1. Win10
  2. Tensorflow 2.0
  3. ML-agents 0.15.0
  4. Unity 2019.3.1f1
    关于 Tensorflow 2.0 的安装,参考我之前的博客,Unity 的安装,推荐先下载 Unity Hub,通过 Unity Hub 可以管理不同版本的 Unity,下载戳这里

ML-agents 的安装有两种方式
一是直接 pip

pip install mlagents

二是从官方 github 中 clone 整个项目,然后 cd 到目录中

pip install -e ./ml-agents-envs
pip install -e ./ml-agents

到这里,环境配置就算完成了

跑个 Demo 先吧

在 Unity Hub 中导入刚刚从 github 上 clone 的项目

选择 Unity 版本之后打开

然后打开 3DBall 这个场景

点击运行的话可以直接看到效果

接着,开始尝试自己训练,打开命令行,进入到之前 clone 下来的项目目录中,并在目录中创建一个名为 summaries 的文件夹

然后输入

mlagents-learn config/trainer_config.yaml --run-id=test --train

在出现图标之后,切换到 Unity 运行项目,就可以看见开始训练了

可以看出来一开的效果是很差的,完全控制不好,这次训练大概训练 40w+ 步,到后面就很稳了

注:项目目录中尽量不要出现中文,下午在尝试的时候一直报错,后来更改目录之后发现成功了,不清楚是不是路径中有中文的原因
报错内容

 "The Unity environment took too long to respond. Make sure that :\n"
mlagents_envs.exception.UnityTimeOutException: The Unity environment took too long to respond. Make sure that :
         The environment does not need user interaction to launch
         The Agents are linked to the appropriate Brains
         The environment and the Python interface have compatible versions.

训练完成后,我们可以在 models 目录下看到刚刚训练好的模型,重命名一下,然后把模型拖到 TFModel 目录中

接着打开 prefabs 中的 3DBall,点击其中的 agent,然后将 Behavior 改成我们刚刚拖到 TFModel 目录中的模型

点击运行,发现控制的十分稳定,跟一开始差不多。至此,初试完毕。

小节

这里我们安装配置了 ML-agnets 的相关环境,并运行了个 Demo 来熟悉了遍流程,后面将开始尝试自己搭建环境,训练 AI,也不知道能不能捣鼓出来,23333。

kafka消息分区机制原理

背景

kafka如何支撑海量消息的集中写入?

答案就是消息分区。

核心思想是:负载均衡,采用合适的分区策略把消息写到不同的broker上的分区中;

其它的产品中有类似的思想。

比如monogodb, es 里面叫做 shard;   hbase叫region,  cassdra叫vnode;

消息的三层结构

如下图:

即  topic -> partition -> message ;

topic是逻辑上的消息容器;

partition实际承载消息,分布在不同的kafka的broke上;

message即具体的消息。

分区策略

round-robin轮询

消息按照分区挨个的写。

randomness随机分区

随机的找一个分区写入,代码如下:

List<PartitionInfo> partitions = cluster.partitionsForTopic(topic);
return ThreadLocalRandom.current().nextInt(partitions.size());

key

相同的key的消息写到固定的分区中

自定义分区

必须完成两步:

1,自定义分区实现类,需要实现org.apache.kafka.clients.producer.Partitioner接口。

主要是实现下面的方法:

int partition(String topic, Object key, byte[] keyBytes, 
              Object value, byte[] valueBytes, Cluster cluster);

比如按照区域分区。

List<PartitionInfo> partitions = cluster.partitionsForTopic(topic);
return partitions.stream().filter(p -> isSouth(p.leader().host()))
    .map(PartitionInfo::partition).findAny().get();

2,显示配置生产者端的参数partitioner.class为具体的类

系统默认:如果消息有key,按照key分区策略,否则按照轮询策略。

小结

kafka的分区实现消息的高吞吐量的主要依托,主要是实现了写的负载均衡。可以指定各种负载均衡算法。
负载均衡算法非常重要,需要极力避免消息分区不均的情况,可能给消费者带来性能瓶颈。

小结如下:

原创不易,点赞关注支持一下吧!转载请注明出处,让我们互通有无,共同进步,欢迎沟通交流。
我会持续分享Java软件编程知识和程序员发展职业之路,欢迎关注,我整理了这些年编程学习的各种资源,关注公众号‘李福春持续输出’,发送’学习资料’分享给你!

每个优秀程序员都应遵循的代码原则和规范

本文由葡萄城技术团队原创并首发

转载请注明出处:葡萄城官网,葡萄城为开发者提供专业的开发工具、解决方案和服务,赋能开发者。

 

什么是优秀的程序员?

首先我们会先提出这个问题,如果你向10个人问这个问题,尽管可能答案不同,但是少有一点应该是一致的。而对我个人而言,一个优秀的程序员应该是一个能够充分理解需求,并能提出可行性解决方案通过团队协作向最终用户展示成果。而说到团队协作,就涉及到代码的可维护性,那么你该如何管理庞大的代码库?如果放任团队成员提交随意的代码,那么在项目中无论在bug修复还是新增功能,都将很难完成。

如果想要实现可维护这个目标,那么团队中的每个成员都应该保证提交整洁且可维护的代码。那么您应该让你的团队成员遵守一定的编码原则。遵守这些原则,可以使你和其他人的协作变得更容易。所以团队成员应该遵循什么样的规则呢?

童子军原则

童子军是美国社会针对未成年人的一种教育实践制度,加入童子军的小朋友都要学习并遵守一些规则,然后获得各种各样的勋章。其中一条规则是离开宿营地前进行清扫活动的原则,简洁明了:

Leave the campground cleaner than you found it.

假设某个小朋友来到某个宿营地,不幸发现地上有两处垃圾,然后他自己在接下来的日常活动中也制造了一处垃圾。那么当他离开时,不仅要清理掉自己的垃圾,还有处理早先小朋友留下的两处垃圾。而不是去寻找是谁丢的。应把注意力放在为下一个露营者创造更好的环境。

这个原则放到软件生产中则意味着让check in比check out时更整洁,至少不要让代码变得更糟糕。

(图片来源于网络)

避免重复

尽量在项目的开发过程中减少产出重复的代码、方法和类,多数的设计模式根本目的是为了减少代码重复,尽可能将重复使用的代码抽象封装,是提高代码的可重用性和可维护性的最佳方式之一。

功能独立

这里功能独立的意思是指,函数或方法尽可能简单,功能尽可能独立。

换句话来讲就是,一个方法最好只做一件事,如果你觉得你的代码过于复杂,该怎么做?抽方法。

初级程序员最常犯的错误就是在一个方法中包含了很多种要做的工作,这可能会在软件的生命周期带来灾难。

简单易懂的代码比“聪明“的代码更好

程序员作为社会中最聪明的群体之一,往往在写代码时也会产出一些炫技的代码块,这部分代码块过段时间再去看,就像谜一样存在于程序中,虽然很简洁,但并不易读。

例如:有些人在程序中喜欢使用三目运算而非if-else,虽然本身使用三目运算符没有问题,但存在嵌套情况时,那么对于后面的维护者就是一场噩梦,例如如下代码:

(A>B?(A>C?A:C):(B>C?B:C))

其实上面的代码等同于,显然下面的格式更易懂

if(A>B){
    (A>C?A:C)
}else{
    (B>C?B:C)
}

迪米特法则

迪米特法则是1987年秋天由lan holland在美国东北大学一个叫做迪米特的项目设计提出的,它要求一个对象应该对其他对象有最少的了解,所以迪米特法则又叫做最少知识原则。它的意义旨在降低类和类之间的耦合,避免发生由于耦合度过大造成的因为一个类发生变化,而对另一个类造成影响。

YAGNI

YAGNI原则是指在开发时只需要将应用程序必需的功能包含进来,而不要试图添加任何其他你认为可能需要的功能。开发过程中为了应对将来可能的提出的需求,提前开发一些功能进去,我们通常会花不少时间成本在这些过度设计的功能开发上,但可能未来的两三年内这个设计根本没有用到。应把更多的精力花在更重要的功能开发上,适度假设未来需求的规划,加速后期功能迭代和代码维护。

总结

虽然上面提了很多原则和规范,但这些规范需要在长期在工作中的实践才能有更深的理解的。希望您能从本文中了解一些基础,最后,希望大家都能写出优美、规范的代码。

 

上周热点回顾(4.13-4.19)

热点随笔:

· 进大厂也就这回事,工作后2到3年进大厂操作指南 (hsm_computer)
· .Net微服务实战之技术架构分层篇 (陈珙)
· HTML5+tracking.js实现刷脸支付 (nd)
· 大专学历以后就职会不会有瓶颈 (沉默王二)
· 技术从业者的未来 (lex-wu)
· 震撼!全网第一张源码分析全景图揭秘Nginx (轩辕之风)
· 谁说.NET不适合搞大数据,机器学习、人工智能 (芝麻麻雀)
· Blazor WebAssembly 3.2.0 Preview 4 如期发布 (张善友)
· Docker 常用命令(.NET Core示例) (滴答的雨)
· 一个关于HttpClient的轮子 (丶Pz)
· 面试官再问你 HashMap 底层原理,就把这篇文章甩给他看 (烟雨星空)
· DevEco Toolkit使用指南–平行视界 (华为开发者论坛)

热点新闻:

· 谁杀死了免费工具?
· 清华校友预测美国疫情发展,准确率达96%|网友:病毒都听你的,绝了
· 为什么诈骗损失难以追回
· 永久免费 国产OS系统深度Deepin v20 beta正式发布
· GitHub宣布向所有团队免费开放核心功能
· 中国最赚钱高铁 京沪高铁发布上市首份年报:每天净赚3200万
· 疯狂的《动森》:游戏已下架,“黑产”依旧火
· 光追版《我的世界》开放抢先体验:简直不是同一款游戏
· 物美价廉的品牌,为什么活不过半年?
· 苹果收购的天气App有多特别?预测10分钟后会不会下雨
· GitHub 已完成对 npm 的收购
· 百度网盘破解版开发者被抓,百度回应:会提升体验

实例讲解Springboot整合MongoDB进行CRUD操作的两种方式

1 简介

Springboot是最简单的使用Spring的方式,而MongoDB是最流行的NoSQL数据库。两者在分布式、微服务架构中使用率极高,本文将用实例介绍如何在Springboot中整合MongoDB的两种方法:MongoRepositoryMongoTemplate

代码结构如下:

2 项目准备

2.1 启动MongoDB实例

为了方便,使用Docker来启动MongoDB,详细指导文档请参考:用Docker安装一个MongoDB最新版玩玩 ,这里不再赘述。

2.2 引入相关依赖

主要的依赖为WebMongoDBstarter,把下面代码加入到pom.xml中即可:

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-data-mongodb</artifactId>
</dependency>
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-web</artifactId>
</dependency>

2.3 配置数据库连接信息

与配置MySQLOracle一样,MongoDB也需要配置连接信息,配置在application.properties中如下:

server.port=8080

spring.data.mongodb.authentication-database=admin
spring.data.mongodb.database=testdb
spring.data.mongodb.username=user
spring.data.mongodb.password=123456
spring.data.mongodb.host=localhost
spring.data.mongodb.port=27017

2.4 创建数据模型实体

创建User类如下:

package com.pkslow.mongo.model;

import org.springframework.data.annotation.Id;
import java.util.Date;

public class User {
    @Id
    private String userId;
    private String name;
    private Integer age;
    private Date createTime = new Date();

    public String getUserId() {
        return userId;
    }

    public void setUserId(String userId) {
        this.userId = userId;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Integer getAge() {
        return age;
    }

    public void setAge(Integer age) {
        this.age = age;
    }

    public Date getCreateTime() {
        return createTime;
    }

    public void setCreateTime(Date createTime) {
        this.createTime = createTime;
    }
}

不需要在MongoDB中创建对应的Collections(表),当通过Web应用新增时会自动创建。

3 方式1:MongoRepository

3.1 定义数据访问层UserRepository

使用过Spring Jpa的都清楚,Repository实际就是用于操作数据库的类。在非关系型数据库MongoDB的整合中,也是一样的。Spring会帮我们实现好对应接口的方法,开发人员连SQL都不用写,非常省心。代码如下:

package com.pkslow.mongo.dal;

import com.pkslow.mongo.model.User;
import org.springframework.data.mongodb.repository.MongoRepository;
import org.springframework.stereotype.Repository;

@Repository
public interface UserRepository extends MongoRepository<User, String> {
}

注意MongoRepository后面接的泛型<User, String>第一个为实体类,第二个为主键ID

3.2 实现Controller

Controller比较基础,就不讲解了,常用的注解是必须要掌握的,直接上代码吧:

package com.pkslow.mongo.contrlloer;

import com.pkslow.mongo.dal.UserRepository;
import com.pkslow.mongo.model.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

import java.util.List;

@RestController
@RequestMapping("/user")
public class UserController {
    @Autowired
    private final UserRepository userRepository;

    public UserController(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    @GetMapping("")
    public List<User> getAllUsers() {
        return userRepository.findAll();
    }

    @GetMapping("/{userId}")
    public User getByUserId(@PathVariable String userId) {
        return userRepository.findById(userId).orElse(new User());
    }

    @PostMapping("")
    public User addNewUser(@RequestBody User user) {
        return userRepository.save(user);
    }

    @DeleteMapping("/{userId}")
    public String delete(@PathVariable String userId) {
        User user = new User();
        user.setUserId(userId);
        userRepository.deleteById(userId);
        return "deleted: " + userId;
    }

    @PutMapping("")
    public User update(@RequestBody User user) {
        return userRepository.save(user);
    }
}

注意代码没有做异常情况的判断和处理,这里为了快速演示,就先不管了。

3.3 测试

Postman测试后,每个接口均调用成功。就不一一截图了。

4 方式2:MongoTemplate

4.1 定义数据访问层UserDAL

先定义接口为:

package com.pkslow.mongo.dal;

import com.pkslow.mongo.model.User;
import java.util.List;

public interface UserDAL {
    List<User> findAll();

    User findById(String userId);

    User save(User user);

    void deleteById(String userId);
}

然后实现该接口如下:

package com.pkslow.mongo.dal;

import com.pkslow.mongo.model.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.data.mongodb.core.query.Criteria;
import org.springframework.data.mongodb.core.query.Query;
import org.springframework.stereotype.Repository;
import java.util.List;

@Repository
public class UserDALImpl implements UserDAL {

    @Autowired
    private MongoTemplate template;

    @Override
    public List<User> findAll() {
        return template.findAll(User.class);
    }

    @Override
    public User findById(String userId) {
        return template.findById(userId,User.class);
    }

    @Override
    public User save(User user) {
        return template.save(user);
    }

    @Override
    public void deleteById(String userId) {
        Query query = new Query();
        query.addCriteria(Criteria.where("userId").is(userId));
        template.remove(query, User.class);
    }
}

4.2 实现另一个Controller

这个Controller的代码与之前的基本一样,只是数据访问类不一样,代码如下:

package com.pkslow.mongo.contrlloer;

import com.pkslow.mongo.dal.UserDAL;
import com.pkslow.mongo.model.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

import java.util.List;

@RestController
@RequestMapping("/userTemplate")
public class UserTemplateController {

    @Autowired
    private final UserDAL userDAL;

    public UserTemplateController(UserDAL userDAL) {
        this.userDAL = userDAL;
    }


    @GetMapping("")
    public List<User> getAllUsers() {
        return userDAL.findAll();
    }

    @GetMapping("/{userId}")
    public User getByUserId(@PathVariable String userId) {
        return userDAL.findById(userId);
    }

    @PostMapping("")
    public User addNewUser(@RequestBody User user) {
        return userDAL.save(user);
    }

    @DeleteMapping("/{userId}")
    public String delete(@PathVariable String userId) {
        User user = new User();
        user.setUserId(userId);
        userDAL.deleteById(userId);
        return "deleted: " + userId;
    }

    @PutMapping("")
    public User update(@RequestBody User user) {
        return userDAL.save(user);
    }
}

4.3 测试

测试一样也是全部通过:

5 总结

本文通过实例讲解了如何整合SpringbootMongoDB,主要有两种方法:MongoRepositoryMongoTemplate。代码基本在文章中已经贴出来了,如果还不清楚,可以在南瓜慢说公众号回复<SpringbootMongoDB>获取代码。

欢迎访问南瓜慢说 www.pkslow.com 获取更多精彩文章!

欢迎关注微信公众号<南瓜慢说>,将持续为你更新…

多读书,多分享;多写作,多整理。

ASP.NET Core中的Action的返回值类型

在Asp.net Core之前所有的Action返回值都是ActionResult,Json(),File()等方法返回的都是ActionResult的子类。并且Core把MVC跟WebApi合并之后Action的返回值体系也有了很大的变化。

ActionResult类

ActionResult类是最常用的返回值类型。基本沿用了之前Asp.net MVC的那套东西,使用它大部分情况都没问题。比如用它来返回视图,返回json,返回文件等等。如果是异步则使用Task 。

    public class TestController : Controller
    {
        public ActionResult Index()
        {
            return View();
        }

        public ActionResult MyFile()
        {
            return File(new byte[] { }, "image/jpg");
        }

        public ActionResult MyJson()
        {
            return Json(new { name = "json" });
        }

        public ActionResult Ok()
        {
            return Ok();
        }
    }

IActionResult接口

ActionResult类实现了IActionResult接口所以能用ActionResult的地方都可以使用IActionResult来替换。同样异步的话使用Task包起来做为返回值。

   public class ITestController : Controller
    {
        public IActionResult Index()
        {
            return View();
        }

        public IActionResult MyFile()
        {
            return File(new byte[] { }, "image/jpg");
        }

        public IActionResult MyJson()
        {
            return Json(new { name = "json" });
        }

        public IActionResult HttpOk()
        {
            return Ok();
        }

        public async Task<IActionResult> AsyncCall()
        {
            await Task.Delay(1000);

            return Content("ok");
        }
    }

直接返回POCO类

Asp.net Core的Controller的Action可以把POCO类型(其实不一定是POCO类,可以是任意类型,但是使用的时候一般都返回viwemodel等POCO类)当做返回值,不一定非要是ActionResult或者IActionResult。Asp.net Core框架会帮我们自动序列化返回给前端,默认使用json序列化。同样异步的话使用Task包起来做为返回值。

   public class Person
    {
        public string Name { get; set; }

        public string Sex { get; set; }
    }

    public class ITestController : Controller
    {

          public Person GetPerson()
        {
            return new Person { Name = "abc", Sex = "f" };
        }

        public async Task<List<Person>> GetPersons()
        {
            await Task.Delay(1000);

            return new List<Person> {
            new Person { Name = "abc", Sex = "f" },
            new Person { Name = "efg", Sex = "m" }
            };
        }
    }

ActionResult< T >泛型类

当我们设计restful webapi系统的时候习惯使用POCO做为返回值。比如我们设计一个获取Person的api。通过 /person/001 url获取001号person。

    [Route("[controller]")]
    public class PersonController : Controller
    {
        IPersonRepository _repository;
        PersonController(IPersonRepository repository) 
        {
            _repository = repository;
        }

        [HttpGet("{id}")]
       public Person Get(string id)
        {
            return _repository.Get(id);
        }
    }

这个方法看起来好像没什么问题,但其实有个小问题。如果repository.Get方法没有根据id查找到数据,那么将会返回null。如果null做为Action的返回值,最后框架会转换为204的http status code。

204表示No Content 。做为restful api,204的语义在这里会有问题,这里比较适合的status code是404 NOT FOUND 。那么我们来改一下:

        [HttpGet("{id}")]
       public Person Get(string id)
        {
            var person = _repository.Get(id);
            if (person == null)
            {
                Response.StatusCode = 404;
            }

            return person;
        }

现在如果查找不到person数据,则系统会返回404 Not Found 。

但是这看起来显然不够优雅,因为ControllerBase内置了NotFoundResult NotFound() 方法。这使用这个方法代码看起来更加清晰明了。继续改:

        [HttpGet("{id}")]
       public Person Get(string id)
        {
            var person = _repository.Get(id);
            if (person == null)
            {
                return NotFound();
            }
            return person;
        }

很不幸,这段代码VS会提示错误。因为返回值类型不一致。方法签名的返回值是Person,但是方法内部一会返回NotFoundResult,一会返回Person。

解决这个问题就该ActionResult< T >出场了。我们继续改一下:

        [HttpGet("{id}")]
       public ActionResult<Person> Get(string id)
        {
            var person = _repository.Get(id);
            if (person == null)
            {
                return NotFound();
            }

            return person;
        }

现在VS已经不会报错了,运行一下也可以正常工作。但仔细想想也很奇怪,为什么返回值类型改成了ActionResult< Person >就不报错了呢?明明返回值类型跟方法签名还是不一致啊?

深入ActionResult< T >

接上面的问题,让我们看一下ActionResult的内部:

看到这里就明白了原来ActionResult< T >里面内置了2个implicit operator方法。implicit operator用于声明隐式类型转换。

public static implicit operator ActionResult<TValue>(ActionResult result); 

表示ActionResult类型可以转换为ActionResult< TValue >类型。

public static implicit operator ActionResult<TValue>(TValue value)

表示TValue类型可以转换为ActionResult< TValue >类型。

因为有了这2个方法,当ActionResult或者TValue类型往ActionResult< T >赋值的时候会进行一次自动的类型转换。所以VS这里不会报错。

总结

  1. 大部分时候Action的返回值可以使用ActionResult/IActionResult
  2. 设计restful api的时候可以直接使用POCO类作为返回值
  3. 如果要设计既支持POCO类返回值或者ActionResult类为返回值的action可以使用ActionResult< T >作为返回值
  4. ActionResult< T >之所以能够支持两种类型的返回值类型,是因为使用了implicit operator内置了2个隐式转换的方法

IdentityServer4 QuckStart 授权与自定义Claims

最近在折腾IdentityServer4,为了简单,直接使用了官方给的QuickStart示例项目作为基础进行搭建。有一说一,为了保护一个API,感觉花费的时间比写一个API还要多。

本文基于ASP.NET CORE 3.1, IdentityServer4 3.1.3。代码皆为关键代码,贴全了太多了。

好不容易跑起来了,最终的任务要落实到授权的工作上来。在API中使用Authorize用来限制用户的访问。

[Route("api/[controller]")]
[Authorize(Roles = "Administrator")]
[ApiController]
public class UserInfoController : ControllerBase
{
    /// <summary>
    /// 无参GET请求
    /// </summary>
    /// <returns></returns>
    [HttpGet()]
    [ProducesResponseType(typeof(ReturnData<IEnumerable<UserInfo>>), Status200OK)]
    public async Task<ActionResult> Get()
    {
        var info = new Info<UserInfo>();
        return Ok(new ReturnData<IEnumerable<UserInfo>>(await info.Get()));
    }

然而在使用的时候,虽然正确取得授权,但是却无法正常访问API,一直提示401没有授权错误。仔细检查,发现IdentityServer4返回的内容并没有返回role的JwtClaimTypes,没有它,Authorize无法正常工作。

{
    "nbf": 1587301921,
    "exp": 1587305521,
    "iss": "http://localhost:5000",
    "aud": "MonitoringSystemApi",
    "client_id": "webClient",
    "sub": "c6c18d4d-c28e-4de5-86dd-779121216204",
    "auth_time": 1587301921,
    "idp": "local",
    "scope": [
        "roles",
        "MonitoringSystemApi",
        "offline_access"
    ],
    "amr": [
        "pwd"
    ]
}

实现

查看Config.cs,IdentityServer4默认只返回两种IdentityResource:openid和profile。按照官方的说法,这个东西定义的内容会返回到用户的token。参考。那么就果断给它安排。

public static IEnumerable<IdentityResource> Ids =>
new List<IdentityResource>
{
    new IdentityResources.OpenId(),
    new IdentityResources.Profile(),
    new IdentityResource ("roles", new List<string> { JwtClaimTypes.Role }){ Required = true}
};

public static IEnumerable<Client> Clients =>
    new List<Client>
    {
        new Client
        {
            ClientId = "webClient",
            ClientSecrets = { new Secret("secret".Sha256()) },
            AllowOfflineAccess = true,
            AllowedGrantTypes = GrantTypes.ResourceOwnerPassword,
            // scopes that client has access to
            AllowedScopes = {
                "roles",

                "MonitoringSystemApi" }
        },

执行之前,需要确保数据库中的用户数据,已经包含role的Claim。

//添加用户代码
bob = new ApplicationUser
{
    UserName = "bob"
};
var result = userMgr.CreateAsync(bob, "Pass123$").Result;
if (!result.Succeeded)
{
    throw new Exception(result.Errors.First().Description);
}
result = userMgr.AddClaimsAsync(bob, new Claim[]{
new Claim(JwtClaimTypes.Role, "Administrator"),
new Claim(JwtClaimTypes.Name, "Bob Smith"),

运行程序,返回值依旧没有任何变化,很挫败,只能继续折腾。
研究通过实现IProfileService达到自定义Cliams。文章写的很详细,我这就不重复了,我实际试验过,可以成功。

但是文章末尾的注意,很重要。

“那么, 通过profileservice颁发的claims, 任意clients都能拿到”

说明这个优先级是非常高的,可以覆盖所有的行为,当然我们可以在IProfileService的实现上对权限进行进一步的设置,不过还是挺麻烦的。参考实现参考官方

作为懒人,必然不想再费劲去折腾权限的问题,那么是否有简单点的办法呢?

网上有一些问答说到了可以通过设置Scopes来达到目的。不过过于久远,IdentityServer4已经没有这个独立的类了,说是已经被ApiResource取代了。

直觉上这个东西应该是指示要保护的API的相关内容的,好像和这个没啥关系,不过也只能死马当活马医了。修改config.cs,最终如下内容:

public static IEnumerable<ApiResource> Apis =>
new List<ApiResource>
{
    new ApiResource("pls", new[]{ "role"}),
};

public static IEnumerable<Client> Clients =>
new List<Client>
{
new Client
{
    ClientId = "webClient",
    ClientSecrets = { new Secret("secret".Sha256()) },
    AllowOfflineAccess = true,
    AllowedGrantTypes = GrantTypes.ResourceOwnerPassword,
    // scopes that client has access to
    AllowedScopes = {
        "pls"
        }
},

返回结果如下:

{
    "nbf": 1587301799,
    "exp": 1587305399,
    "iss": "http://localhost:5000",
    "aud": "pls",
    "client_id": "webClient",
    "sub": "c6c18d4d-c28e-4de5-86dd-779121216204",
    "auth_time": 1587301799,
    "idp": "local",
    "role": "Administrator",
    "scope": [
        "pls",
        "offline_access"
    ],
    "amr": [
        "pwd"
    ]
}

终于看见心心念念的自定义Claim(role),可以去访问API了。

注意,在Client中也有个Claims,添加了role并且设置AlwaysSendClientClaimsAlwaysIncludeUserClaimsInIdToken之后,会在token中添加client_roie字段,这个是没办法用与授权的,可以理解为IdentityServer4直接指定了Client角色,并不是Identity中的角色概念。

后记

回过头来仔细看官方的文档,ApiResource中的UserClaims就是用来干这个的,折腾了半天,不如当时仔细看看文档了。

模板参数的“右值引用”是转发引用

在C++11中,&&不再只有逻辑与的含义,还可能是右值引用:

void f(int&& i);

但也不尽然,&&还可能是转发引用:

template<typename T>
void g(T&& obj);

“转发引用”(forwarding reference)旧称“通用引用”(universal reference),它的“通用”之处在于你可以拿一个左值绑定给转发引用,但不能给右值引用:

void f(int&& i) { }

template<typename T>
void g(T&& obj) { }

int main()
{
    int n = 2;
    f(1);
//  f(n); // error
    g(1);
    g(n);
}

一个函数的参数要想成为转发引用,必须满足:

  • 参数类型为T&&,没有constvolatile

  • T必须是该函数的模板参数。

换言之,以下函数的参数都不是转发引用:

template<typename T>
void f(const T&&);
template<typename T>
void g(typename std::remove_reference<T>&&);
template<typename T>
class A
{
    template<typename U>
    void h(T&&, const U&);
};

另一种情况是auto&&变量也可以成为转发引用:

auto&& vec = foo();

所以写范围for循环的最好方法是用auto&&

std::vector<int> vec;
for (auto&& i : vec)
{
    // ...
}

有一个例外,当auto&&右边是初始化列表,如auto&& l = {1, 2, 3};时,该变量为std::initializer_list<int>&&类型。

转发引用,是用来转发的。只有当你的意图是转发参数时,才写转发引用T&&,否则最好把const T&T&&写成重载(如果需要的话还可以写T&,还有不常用的const T&&;其中T是具体类型而非模板参数)。

转发一个转发引用需要用std::forward,定义在<utility>中:

#include <utility>

template<typename... Args>
void f(Args&&... args) { }

template<typename T>
void g(T&& obj)
{
    f(std::forward<T>(obj));
}

template<typename... Args>
void h(Args&&... args)
{
    f(std::forward<Args>(args)...);
}

调用g有几种可能的参数:

  • int i = 1; g(i);Tint&,调用g(int&)

  • const int j = 2; g(j);Tconst int&,调用g(const int&)

  • int k = 3; g(std::move(k));g(4);Tint(不是int&&哦!),调用g(int&&)

你也许会疑惑,为什么std::move不需要<T>std::forward需要呢?这得从std::forward的签名说起:

template<typename T>
constexpr T&& forward(std::remove_reference_t<T>&) noexcept;
template<typename T>
constexpr T&& forward(std::remove_reference_t<T>&&) noexcept;

调用std::forward时,编译器无法根据std::remove_reference_t<T>反推出T,从而实例化函数模板,因此<T>需要手动指明。

但是这并没有从根本上回答问题,或者可以进一步引出新的问题——为什么std::forward的参数不定义成T&&呢?

原因很简单,T&&会把T&const T&T&&const T&&(以及对应的volatile)都吃掉,有了T&&以后,再写T&也没用。

且慢,T&&参数在传入函数是会匹配到T&&吗?

#include <iostream>
#include <utility>

void foo(int&)
{
    std::cout << "int&" << std::endl;
}

void foo(const int&)
{
    std::cout << "const int&" << std::endl;
}

void foo(int&&)
{
    std::cout << "int&&" << std::endl;
}

void bar(int&& i)
{
    foo(i);
}

int main()
{
    int i;
    bar(std::move(i));
}

不会!程序输出int&。在函数bar中,i是一个左值,其类型为int的右值引用。更直接一点,它有名字,所以它是左值。

因此,如果std::forward没有手动指定的模板参数,它将不能区分T&T&&——那将是“糟糕转发”,而不是“完美转发”了。

最后分析一下std::forward的实现,以下代码来自libstdc++:

template<typename _Tp>
  constexpr _Tp&&
  forward(typename std::remove_reference<_Tp>::type& __t) noexcept
  { return static_cast<_Tp&&>(__t); }

template<typename _Tp>
  constexpr _Tp&&
  forward(typename std::remove_reference<_Tp>::type&& __t) noexcept
  {
    static_assert(!std::is_lvalue_reference<_Tp>::value, "template argument"
                  " substituting _Tp is an lvalue reference type");
    return static_cast<_Tp&&>(__t);
  }
  • 当转发引用T&& obj绑定左值int&时,匹配第一个重载,_TpTint&,返回类型_Tp&&int&(引用折叠:& && &&&& &都折叠为&,只有&& &&折叠为&&);

  • const int&同理;

  • 当转发引用绑定右值int&&时,匹配第二个重载,_Tpint,返回类型为int&&

  • const int&&同理。

综上,std::forward能完美转发。

程序员总是要在Stack Overflow上撞撞墙才能学会一点东西。