Akka(7): FSM:通过状态变化来转换运算行为
在上篇讨论里我们提到了become/unbecome。由于它们本质上是堆栈操作,所以只能在较少的状态切换下才能保证堆栈操作的协调及维持程序的清晰逻辑。对于比较复杂的程序流程,Akka提供了FSM:一种通过状态变化进行功能切换的Actor。FSM模式的状态转变特别适合对应现实情况中的程序流程,我们可以用每一种状态来代表一个程序流程。FSM是个trait,定义如下:
trait FSM[S, D] extends Actor with Listeners with ActorLogging {...}
我们看到:FSM就是一个特殊的Actor。带着两个类型参数:S代表状态类型,D代表状态数据类型。实际上S和D结合起来就是FSM的内部状态,即:SomeState+DataX 和 SomeState+DataY分别代表不同的Actor内部状态,这点从State定义可以得到信息:
/**
* This captures all of the managed state of the [[akka.actor.FSM]]: the state
* name, the state data, possibly custom timeout, stop reason and replies
* accumulated while processing the last message.
*/
case class State[S, D](stateName: S, stateData: D, timeout: Option[FiniteDuration] = None, stopReason: Option[Reason] = None, replies: List[Any] = Nil) {...}
我们可以用下面的表达式来代表FSM功能:
State(SA) x Event(E) -> Actions (A), State(SB)
意思是:假如在状态SA发生了事件E,那么FSM应该实施操作A并把状态转换到SB。这里面操作Action代表某项功能,事件Event是个新的类型,定义如下:
/**
* All messages sent to the [[akka.actor.FSM]] will be wrapped inside an
* `Event`, which allows pattern matching to extract both state and data.
*/
final case class Event[D](event: Any, stateData: D) extends NoSerializationVerificationNeeded
Event[D]是个包嵌消息和数据的类型。理论上FSM是通过接收Event来确定运行功能和下一个状态转换,就像普通的Actor接收Message原理一样。我们用上一篇里的FillSeasons作为这次FSM示范的例子。首先定义State,Data:
trait Seasons //States
case object Spring extends Seasons
case object Summer extends Seasons
case object Fall extends Seasons
case object Winter extends Seasons class SeasonInfo(talks: Int, month: Int) //Data
case object BeginSeason extends SeasonInfo(,)
四个状态分别是:春夏秋冬。SeasonInfo代表数据类型,含被问候次数talks,季中月份month两个参数,每季含1,2,3三个月份。目前我们只支持两种功能消息:
object FillSeasons {
sealed trait Messages //功能消息
case object HowYouFeel extends Messages
case object NextMonth extends Messages
}
这种普通Actor的消息类型对应到FSM的Event类型中的event:Any。也就是说FSM在收到功能消息后需要构建一个Event类型实例并把消息包嵌在里面。因为FSM继承了Actor,所以它必须实现receive函数。下面是FSM.receive的源代码:
/*
* *******************************************
* Main actor receive() method
* *******************************************
*/
override def receive: Receive = {
case TimeoutMarker(gen) ⇒
if (generation == gen) {
processMsg(StateTimeout, "state timeout")
}
case t @ Timer(name, msg, repeat, gen) ⇒
if ((timers contains name) && (timers(name).generation == gen)) {
if (timeoutFuture.isDefined) {
timeoutFuture.get.cancel()
timeoutFuture = None
}
generation +=
if (!repeat) {
timers -= name
}
processMsg(msg, t)
}
case SubscribeTransitionCallBack(actorRef) ⇒
// TODO Use context.watch(actor) and receive Terminated(actor) to clean up list
listeners.add(actorRef)
// send current state back as reference point
actorRef ! CurrentState(self, currentState.stateName)
case Listen(actorRef) ⇒
// TODO Use context.watch(actor) and receive Terminated(actor) to clean up list
listeners.add(actorRef)
// send current state back as reference point
actorRef ! CurrentState(self, currentState.stateName)
case UnsubscribeTransitionCallBack(actorRef) ⇒
listeners.remove(actorRef)
case Deafen(actorRef) ⇒
listeners.remove(actorRef)
case value ⇒ {
if (timeoutFuture.isDefined) {
timeoutFuture.get.cancel()
timeoutFuture = None
}
generation +=
processMsg(value, sender())
}
除timer,subscription等特殊功能外,case value => ... 就是处理自定义消息的地方了。我们看到FSM是用processMsg(value, sender())来处理消息的。processMsg又调用了processEvent:
private def processMsg(value: Any, source: AnyRef): Unit = {
val event = Event(value, currentState.stateData)
processEvent(event, source)
} private[akka] def processEvent(event: Event, source: AnyRef): Unit = {
val stateFunc = stateFunctions(currentState.stateName)
val nextState = if (stateFunc isDefinedAt event) {
stateFunc(event)
} else {
// handleEventDefault ensures that this is always defined
handleEvent(event)
}
applyState(nextState)
}
在processEvent里的stateFunction是个Map,以stateName为主键存放StateFunction:
/*
* State definitions
*/
private val stateFunctions = mutable.Map[S, StateFunction]()
而StateFuction是:
type StateFunction = scala.PartialFunction[Event, State]
FSM的receive函数在收到消息后把消息包嵌入新构建的Event然后在processEvent里通过stateName取出相应的StateFunction后传入Event产生新的状态State。用户提供的StateFunction是通过FSM的when函数压进stateFunction Map里的:
/**
* Insert a new StateFunction at the end of the processing chain for the
* given state. If the stateTimeout parameter is set, entering this state
* without a differing explicit timeout setting will trigger a StateTimeout
* event; the same is true when using #stay.
*
* @param stateName designator for the state
* @param stateTimeout default state timeout for this state
* @param stateFunction partial function describing response to input
*/
final def when(stateName: S, stateTimeout: FiniteDuration = null)(stateFunction: StateFunction): Unit =
register(stateName, stateFunction, Option(stateTimeout)) private def register(name: S, function: StateFunction, timeout: Timeout): Unit = {
if (stateFunctions contains name) {
stateFunctions(name) = stateFunctions(name) orElse function
stateTimeouts(name) = timeout orElse stateTimeouts(name)
} else {
stateFunctions(name) = function
stateTimeouts(name) = timeout
}
}
我们看到when调用了register在stateFunction Map中按stateName放置StateFunction。FSM的这个stateFunction Map解决了become/unbecome产生的堆栈问题。FSM有个比较规范的结构,拿上面例子的FeelingSeasons结构做个示范:
class FillSeasons extends FSM[Seasons,SeasonInfo] with ActorLogging {
import FillSeasons._
startWith(Spring,SeasonInfo(,)) //起始状态
when(Spring) { //状态在春季
case Event(HowYouFeel,seasonInfo) => ...
}
when(Summer) { //夏季状态
case Event(HowYouFeel,_) =>
}
when(Fall) { //秋季状态
case Event(HowYouFeel,SeasonInfo(tks,mnth)) =>
}
when(Winter) { //冬季状态
case Event(HowYouFeel,si@ SeasonInfo(tks,_)) =>
}
whenUnhandled { //所有状态未处理的Event
case Event(NextMonth,seasonInfo) =>
}
onTransition {
case Spring -> Summer => log.info("Season changed from Spring to Summer month 1")
case Summer -> Fall => log.info("Season changed from Summer to Fall month 1")
case Fall -> Winter => log.info("Season changed from Fall to Winter month 1")
case Winter -> Spring => log.info("Season changed from Winter to Spring month 1")
} initialize() //设定起始状态
}
基本上是按照各状态定义事件处理函数StateFunction的。也可以包括状态转换处理函数TransitionHandler:
type TransitionHandler = PartialFunction[(S, S), Unit]
最后,initialize()确定起始状态是否安排正确:
/**
* Verify existence of initial state and setup timers. This should be the
* last call within the constructor, or [[akka.actor.Actor#preStart]] and
* [[akka.actor.Actor#postRestart]]
*
* An initial `currentState -> currentState` notification will be triggered by calling this method.
*
* @see [[#startWith]]
*/
final def initialize(): Unit =
if (currentState != null) makeTransition(currentState)
else throw new IllegalStateException("You must call `startWith` before calling `initialize`")
完整的FSM FeelingSeasons定义如下:
class FillSeasons extends FSM[Seasons,SeasonInfo] with ActorLogging {
import FillSeasons._
startWith(Spring,SeasonInfo(,)) //起始状态
when(Spring) { //状态在春季
case Event(HowYouFeel,seasonInfo) =>
val numtalks = seasonInfo.talks +
log.info(s"It's ${stateName.toString}, feel so gooood! You've asked me ${numtalks}times.")
stay using seasonInfo.copy(talks = numtalks)
}
when(Summer) { //夏季状态
case Event(HowYouFeel,_) =>
val numtalks = stateData.talks +
log.info(s"It's ${stateName.toString}, it's so hot! You've asked me ${numtalks}times")
stay().using(stateData.copy(talks = numtalks))
}
when(Fall) { //秋季状态
case Event(HowYouFeel,SeasonInfo(tks,mnth)) =>
val numtalks = tks +
log.info(s"It's ${stateName.toString}, it's no so bad. You've asked me ${numtalks}times.")
stay using SeasonInfo(numtalks,mnth)
}
when(Winter) { //冬季状态
case Event(HowYouFeel,si@ SeasonInfo(tks,_)) =>
val numtalks = tks +
log.info(s"It's ${stateName.toString}, it's freezing cold! You've asked me ${numtalks}times.")
stay using si.copy(talks = numtalks)
}
whenUnhandled { //所有状态未处理的Event
case Event(NextMonth,seasonInfo) =>
val mth = seasonInfo.month
if (mth <= ) {
log.info(s"It's month ${mth+1} of ${stateName.toString}")
stay using seasonInfo.copy(month = mth + )
}
else {
goto(nextSeason(stateName)) using SeasonInfo(,)
}
}
onTransition {
case Spring -> Summer => log.info(s"Season changed from Spring to Summer month ${nextStateData.month}")
case Summer -> Fall => log.info(s"Season changed from Summer to Fall month ${nextStateData.month}")
case Fall -> Winter => log.info(s"Season changed from Fall to Winter month ${nextStateData.month}")
case Winter -> Spring => log.info(s"Season changed from Winter to Spring month ${nextStateData.month}")
} initialize() //设定起始状态
log.info(s"It's month 1 of ${stateName.toString}") //季节转换顺序
def nextSeason(season: Seasons): Seasons =
season match {
case Spring => Summer
case Summer => Fall
case Fall => Winter
case Winter => Spring
}
}
首先注意StateFunction中SeasonInfo的各种意思同等的表达方式及nextStateData。FSM状态数据用不可变对象(immutable object)最安全,所以在更新时必须用case class 的copy或直接构建新的SeasonInfo实例。
我们再来看看processEvent的作业流程:
private[akka] def processEvent(event: Event, source: AnyRef): Unit = {
val stateFunc = stateFunctions(currentState.stateName)
val nextState = if (stateFunc isDefinedAt event) {
stateFunc(event)
} else {
// handleEventDefault ensures that this is always defined
handleEvent(event)
}
applyState(nextState)
}
先运算用户定义的StateFunction处理事件Event获取新的状态State。然后调用applyState运算makeTransition处理状态转换(currentState = nextState):
private[akka] def applyState(nextState: State): Unit = {
nextState.stopReason match {
case None ⇒ makeTransition(nextState)
case _ ⇒
nextState.replies.reverse foreach { r ⇒ sender() ! r }
terminate(nextState)
context.stop(self)
}
} private[akka] def makeTransition(nextState: State): Unit = {
if (!stateFunctions.contains(nextState.stateName)) {
terminate(stay withStopReason Failure("Next state %s does not exist".format(nextState.stateName)))
} else {
nextState.replies.reverse foreach { r ⇒ sender() ! r }
if (currentState.stateName != nextState.stateName || nextState.notifies) {
this.nextState = nextState
handleTransition(currentState.stateName, nextState.stateName)
gossip(Transition(self, currentState.stateName, nextState.stateName))
this.nextState = null
}
currentState = nextState def scheduleTimeout(d: FiniteDuration): Some[Cancellable] = {
import context.dispatcher
Some(context.system.scheduler.scheduleOnce(d, self, TimeoutMarker(generation)))
} currentState.timeout match {
case SomeMaxFiniteDuration ⇒ // effectively disable stateTimeout
case Some(d: FiniteDuration) if d.length >= ⇒ timeoutFuture = scheduleTimeout(d)
case _ ⇒
val timeout = stateTimeouts(currentState.stateName)
if (timeout.isDefined) timeoutFuture = scheduleTimeout(timeout.get)
}
}
}
我们用FSM DSL的stay, goto,using来取得新的FSM状态和数据:
/**
* Produce transition to other state.
* Return this from a state function in order to effect the transition.
*
* This method always triggers transition events, even for `A -> A` transitions.
* If you want to stay in the same state without triggering an state transition event use [[#stay]] instead.
*
* @param nextStateName state designator for the next state
* @return state transition descriptor
*/
final def goto(nextStateName: S): State = FSM.State(nextStateName, currentState.stateData) /**
* Produce "empty" transition descriptor.
* Return this from a state function when no state change is to be effected.
*
* No transition event will be triggered by [[#stay]].
* If you want to trigger an event like `S -> S` for `onTransition` to handle use `goto` instead.
*
* @return descriptor for staying in current state
*/
final def stay(): State = goto(currentState.stateName).withNotification(false) // cannot directly use currentState because of the timeout field
stay,goto返回结果都是State[S,D]类型。using是State类型的一个方法:
/**
* Modify state transition descriptor with new state data. The data will be
* set when transitioning to the new state.
*/
def using(@deprecatedName('nextStateDate) nextStateData: D): State[S, D] = {
copy(stateData = nextStateData)
}
我们看到using的主要作用是把当前状态数据替换成新状态的数据。
Akka的FSM是一个功能强大的Actor类型,所以配备了一套完整的DSL来方便FSM编程。FSM的DSL语句包括:
final def startWith(stateName: S, stateData: D, timeout: Timeout = None): Unit =
currentState = FSM.State(stateName, stateData, timeout) final def goto(nextStateName: S): State = FSM.State(nextStateName, currentState.stateData) final def stay(): State = goto(currentState.stateName).withNotification(false) final def stop(): State = stop(Normal) final def transform(func: StateFunction): TransformHelper = new TransformHelper(func) ...
State[S,D]也提供了一些比较实用的方法函数:
case class State[S, D](stateName: S, stateData: D, timeout: Option[FiniteDuration] = None, stopReason: Option[Reason] = None, replies: List[Any] = Nil) {
... // defined here to be able to override it in SilentState
def copy(stateName: S = stateName, stateData: D = stateData, timeout: Option[FiniteDuration] = timeout, stopReason: Option[Reason] = stopReason, replies: List[Any] = replies): State[S, D] = {
new State(stateName, stateData, timeout, stopReason, replies)
} /**
* Modify state transition descriptor to include a state timeout for the
* next state. This timeout overrides any default timeout set for the next
* state.
*
* Use Duration.Inf to deactivate an existing timeout.
*/
def forMax(timeout: Duration): State[S, D] = timeout match {
case f: FiniteDuration ⇒ copy(timeout = Some(f))
case Duration.Inf ⇒ copy(timeout = SomeMaxFiniteDuration) // we map the Infinite duration to a special marker,
case _ ⇒ copy(timeout = None) // that means "cancel stateTimeout". This marker is needed
} // so we do not have to break source/binary compat.
// TODO: Can be removed once we can break State#timeout signature to `Option[Duration]` /**
* Send reply to sender of the current message, if available.
*
* @return this state transition descriptor
*/
def replying(replyValue: Any): State[S, D] = {
copy(replies = replyValue :: replies)
} /**
* Modify state transition descriptor with new state data. The data will be
* set when transitioning to the new state.
*/
def using(@deprecatedName('nextStateDate) nextStateData: D): State[S, D] = {
copy(stateData = nextStateData)
} ...
}
FSM DSL中的transform是这样定义的:
final class TransformHelper(func: StateFunction) {
def using(andThen: PartialFunction[State, State]): StateFunction =
func andThen (andThen orElse { case x ⇒ x })
} final def transform(func: StateFunction): TransformHelper = new TransformHelper(func)
我们看到TransformHelper用using对入参func:StateFunction施用用户提供的andThen: PartialFunction[State,State]后返回新的状态State。这个using与State.using是不同的。下面是一个transform用法例子:
when(Running) {
transform {
case Event(m, Target(Actor.noSender)) =>
goto(Uninitialised) using NoConfig case Event(m, Target(ref)) =>
ref ! m
stay
} using targetTransformer
} def targetTransformer: PartialFunction[State, State] = {
case s @ State(stateName, Target(ref), _, _, _) if ref.path.name.startsWith("testActor") =>
log.debug("Setting target to dead letters")
s.using(Target(Actor.noSender))
}
transform{...}产生的State传给了targetTransformer然后经过模式匹配拆分出properties后用s.using更新stateData。
与become/unbecome相同,我们也可以在FSM里使用stashing。下面是一个用例:
when(Uninitialised) {
case Event(Config(ref), _) =>
goto(Running) using Target(ref) case Event(_, _) =>
stash
stay
} when(Running) {
case Event(m, Target(ref)) =>
ref ! m
stay
} onTransition {
case Uninitialised -> Running => unstashAll()
}
当然,还有如stop,setTimer,replying,forMax,onTermination等方法和函数,这里就不一一详述了,有兴趣可以直接查询Akka/actor/FSM.scala。
下面是本次讨论的示范源码:
import akka.actor._ sealed trait Seasons //States
case object Spring extends Seasons
case object Summer extends Seasons
case object Fall extends Seasons
case object Winter extends Seasons //sealed trait SeasonData //Data
case class SeasonInfo(talks: Int, month: Int) object FillSeasons {
sealed trait Messages //功能消息
case object HowYouFeel extends Messages
case object NextMonth extends Messages def props = Props(new FillSeasons)
} class FillSeasons extends FSM[Seasons,SeasonInfo] with ActorLogging {
import FillSeasons._
startWith(Spring,SeasonInfo(,)) //起始状态
when(Spring) { //状态在春季
case Event(HowYouFeel,seasonInfo) =>
val numtalks = seasonInfo.talks +
log.info(s"It's ${stateName.toString}, feel so gooood! You've asked me ${numtalks} times.")
stay using seasonInfo.copy(talks = numtalks)
}
when(Summer) { //夏季状态
case Event(HowYouFeel,_) =>
val numtalks = stateData.talks +
log.info(s"It's ${stateName.toString}, it's so hot! You've asked me ${numtalks} times")
stay().using(stateData.copy(talks = numtalks))
}
when(Fall) { //秋季状态
case Event(HowYouFeel,SeasonInfo(tks,mnth)) =>
val numtalks = tks +
log.info(s"It's ${stateName.toString}, it's no so bad. You've asked me ${numtalks} times.")
stay using SeasonInfo(numtalks,mnth)
}
when(Winter) { //冬季状态
case Event(HowYouFeel,si@ SeasonInfo(tks,_)) =>
val numtalks = tks +
log.info(s"It's ${stateName.toString}, it's freezing cold! You've asked me ${numtalks} times.")
stay using si.copy(talks = numtalks)
}
whenUnhandled { //所有状态未处理的Event
case Event(NextMonth,seasonInfo) =>
val mth = seasonInfo.month
if (mth <= ) {
log.info(s"It's month ${mth+1} of ${stateName.toString}")
stay using seasonInfo.copy(month = mth + )
}
else {
goto(nextSeason(stateName)) using SeasonInfo(,)
}
}
onTransition {
case Spring -> Summer => log.info(s"Season changed from Spring to Summer month ${nextStateData.month}")
case Summer -> Fall => log.info(s"Season changed from Summer to Fall month ${nextStateData.month}")
case Fall -> Winter => log.info(s"Season changed from Fall to Winter month ${nextStateData.month}")
case Winter -> Spring => log.info(s"Season changed from Winter to Spring month ${nextStateData.month}")
} initialize() //设定起始状态
log.info(s"It's month 1 of ${stateName.toString}") //季节转换顺序
def nextSeason(season: Seasons): Seasons =
season match {
case Spring => Summer
case Summer => Fall
case Fall => Winter
case Winter => Spring
}
} object FSMDemo extends App {
import scala.util.Random
val fsmSystem = ActorSystem("fsmSystem")
val fsmActor = fsmSystem.actorOf(FillSeasons.props,"fsmActor") ( to ).foreach { _ =>
( to Random.nextInt()).foreach{ _ =>
fsmActor ! FillSeasons.HowYouFeel
}
fsmActor ! FillSeasons.NextMonth
} scala.io.StdIn.readLine()
fsmSystem.terminate()
}
Akka(7): FSM:通过状态变化来转换运算行为的更多相关文章
- robotframework中的用evaluate关键字进行运算(随机数+转换+运算)
当我们在写rf测试用例时,可能需要随机产生一些数据,可能需要将已有的数据进行转换,做简单的运算等:此时我们可以用万能的evaluate来实现 ,后面一般均适用python表达式来进行实现. 接下来详细 ...
- Akka FSM 源代码分析
Akka FSM 源代码分析 萧猛 <simonxiao@qq.com> 啰嗦几句 有限状态机本身不是啥新奇东西,在GoF的设计模式一书中就有状态模式, 也给出了实现的建议.各种语言对状态 ...
- Akka(6): become/unbecome:运算行为切换
通过一段时间的学习了解,加深了一些对Akka的认识,特别是对于Akka在实际编程中的用途方面.我的想法,或者我希望利用Akka来达到的目的是这样的:作为传统方式编程的老兵,我们已经习惯了直线流程方式一 ...
- Akka源码分析-FSM
akka还有一个不常使用.但我觉得比较方便的一个模块,那就是FSM(有限状态机).我们知道了akka中Actor模型的具体实现之后,就会发现,Akka的actor可以非常方便的实现FSM.其实在akk ...
- Akka(16): 持久化模式:PersistentFSM-可以自动修复的状态机器
前面我们讨论过FSM,一种专门为维护内部状态而设计的Actor,它的特点是一套特殊的DSL能很方便地进行状态转换.FSM的状态转换模式特别适合对应现实中的业务流程,因为它那套DSL可以更形象的描述业务 ...
- [翻译]AKKA笔记 - 有限状态机 -1
原文地址:http://rerun.me/2016/05/21/akka-notes-finite-state-machines-1/ 我最近有个机会在工作上使用了Akka FSM,是个非常有趣的例子 ...
- Akka Typed 官方文档之随手记
️ 引言 近两年,一直在折腾用FP与OO共存的编程语言Scala,采取以函数式编程为主的方式,结合TDD和BDD的手段,采用Domain Driven Design的方法学,去构造DDDD应用(Dom ...
- SQL Server 进制转换函数
一.背景 前段时间群里的朋友问了一个问题:“在查询时增加一个递增序列,如:0x00000001,即每一个都是36进位(0—9,A--Z),0x0000000Z后面将是0x00000010,生成一个像下 ...
- .NET LINQ 转换数据类型
转换数据类型 转换方法更改输入对象的类型. LINQ 查询中的转换运算可用于各种应用程序.下面是一些示例: Enumerable.AsEnumerable<TSource&g ...
随机推荐
- Memcached安装使用教程及常见问题
一.Windows下安装memcahed 1.下载memcache的windows稳定版,解压放某个盘下面,比如在c:/memcached2.在终端(也即cmd命令界面)下输入"c:/mem ...
- Python全栈之路-Day33
1 time模块 #!/usr/bin/env python # __Author__: "wanyongzhen" # Date: 2017/4/7 import time # ...
- 用vuejs仿网易云音乐(实现听歌以及搜索功能)
前言 前端时间学了vue,一开始看了vue1.0,后来实在觉得技术总得实践,就直接上手vue2.0.然后花了将近一周时间做了一个网易云音乐的小项目.一开始觉得项目比较小,没必要用vuex所以就没有使用 ...
- Javascript中变量作用域(2)
多层函数调用取变量时,无论在哪里调用,要到创建此函数的作用域中取值,如果找不到再往上一级,直到全局变量. 外面定义了很多的全局的变量,下面我们来一个个理一下. 定义三个变量a,b,c;将A1函数赋值给 ...
- Lock使用实例
using System; using System.Collections.Generic; using System.Data.SqlClient; using System.Linq; usin ...
- maven命令更新子父项目的pom版本
Q:一个maven项目,有多个子module,在顶级父pom.xml里设置<version>1.0.0-SHAPSHOT</version>,在子pom.xml里都用了 < ...
- jdbc(2)
create table account ( id int primary key auto_increment, name varchar(20), money double);insert int ...
- redis-key相关命令
本篇主要介绍和redis的key进行操作的相关命令. 命令 描述 复杂度 返回值 DEL key [key ...] 删除给定的一个或多个 key .不存在的 key 会被忽略.删除单个列表.集合.有 ...
- Python—课时ONE
说实话第一次接触Python还是在我刚上班的时候,听说很多人这个开发语言很吊,应用的领域很多.but这些东西还不能够吸引我,真正的原因是因为这个开发语言很是简单,比较适合我这种2B学的.但是越往后越觉 ...
- ASP.NET Core 菜鸟之路:从Startup.cs说起
1.前言 本文主要是以Visual Studio 2017 默认的 WebApi 模板作为基架,基于Asp .Net Core 1.0,本文面向的是初学者,如果你有 ASP.NET Core 相关实践 ...