環境資訊中心綜合外電;姜唯 編譯;林大利 審校
本站聲明:網站內容來源環境資訊中心https://e-info.org.tw/,如有侵權,請聯繫我們,我們將及時處理
【其他文章推薦】
※網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!
※網頁設計公司推薦不同的風格,搶佔消費者視覺第一線
※想知道購買電動車哪裡補助最多?台中電動車補助資訊懶人包彙整
※南投搬家公司費用,距離,噸數怎麼算?達人教你簡易估價知識!
※教你寫出一流的銷售文案?
※超省錢租車方案
居家、公司行號垃圾清運、廢棄物處理、大型家具回收,服務快速,包月及計重或計桶供客戶選擇,合法登記的清潔公司、廢棄物清除許可,專業技術人員及專業廢棄物清運車輛
摘錄自2019年11月18日ETtoday綜合報導
今年入秋以來,美國阿拉斯加州氣候異常溫暖,當地最大城市安克拉治居民,近日竟在同一天內,先後經歷了創紀錄高溫與破紀錄降雪兩種截然不同的極端天氣型態!
位於安克拉治國際機場南側的美國國家氣象局(NWS)安克拉治辦公室,當天觀測到超過8.3英吋積雪,打破當地1958年以來的降雪新紀錄。奇怪的是,安克拉治市當天不止出現破紀錄降雪,同一天凌晨3時,當地還出現華氏45度(攝氏7.2度)氣溫,與1967創下的的同日最高溫紀錄持平。
NWS預報員德魯茲(Eric Drewitz)解釋,16日凌晨,一股東南風從海上吹來,為城市帶來溫暖空氣,雖著風勢減弱,氣溫也隨之下滑,並且降下硬幣大小的雪花。報導提到,阿拉斯加州今年秋天以來異常溫暖,10月下旬也曾出現破紀錄高溫,溫暖天氣一直延續到11月。在16日降下大雪以前,當地今年入冬第一場,也是唯一一場降雪出現在10月16日,但降雪量僅5分之1英吋(約0.5公分)。
本站聲明:網站內容來源環境資訊中心https://e-info.org.tw/,如有侵權,請聯繫我們,我們將及時處理
【其他文章推薦】
※網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!
※網頁設計公司推薦不同的風格,搶佔消費者視覺第一線
※Google地圖已可更新顯示潭子電動車充電站設置地點!!
※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益
※別再煩惱如何寫文案,掌握八大原則!
摘錄自2019年11月18日中央通訊社報導
西爪哇的西大魯河曾被稱為世界上最髒的河流,印尼從2年前整治至今,部分上游河水已可供居民飲用。印尼官員說,乾淨印尼是全國性運動,將持續動員全民達到2025年的目標。
印尼總統佐科威(Joko Widodo)在2017年宣示,2025年將達成讓印尼乾淨的目標(Gerakan Indonesia Bersih),包括垃圾從源頭減量3成、並處理7成的垃圾,以避免垃圾直接放置於堆積場或流入海洋。
這是印尼首度、也是唯一動員軍隊清理的河川。負責清理的少將蘇山托(Susanto)之前表示,上游整治後,經養魚測試水質6個月前設置第一台淨水機器,已開始供居民使用。
印尼人口2億6700萬,因垃圾清運不普及、缺乏處理機制,也沒有回收概念,大量垃圾不是丟河裡,就是掩埋或燒掉。許多都市貧民社區及鄉下民眾取用浸泡垃圾的河水作日常使用,暴露在遭垃圾污染的環境及空氣中,衛生條件極差。
輿論普遍分析,印尼乾淨運動是遠大、有難度的目標。印尼海洋事務統籌部海洋科學與科技主任納尼說,政府已列出67條優先治理的河川,以減少海洋垃圾;密集與地方政府合作推動減量,目前已有峇里島等12個地方政府制定限制使用一次性塑膠產品;也研擬嚴懲與觀光有關的行為造成的垃圾污染;並尋求新科技的運用。
本站聲明:網站內容來源環境資訊中心https://e-info.org.tw/,如有侵權,請聯繫我們,我們將及時處理
【其他文章推薦】
※網頁設計公司推薦不同的風格,搶佔消費者視覺第一線
※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益
※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面
※南投搬家公司費用需注意的眉眉角角,別等搬了再說!
※新北清潔公司,居家、辦公、裝潢細清專業服務
摘錄自2019年11月18日NOWnews報導
為了達到環保的目的,2020年起泰國許多連鎖超商或大賣場將不再供應塑膠袋。根據路透社的報導,泰國國家資源和環境部部長瓦拉伍特(Varawut Silpa-archa)在臉書宣佈,許多商店與政府合作,暫停提供一次用塑膠袋,措施將於2020年1月起生效。
「方便性會下降,不過這是為了延續環境的生態循環。」瓦拉伍特受訪時說,他建議民眾可以使用可重複利用的大布袋取代塑膠袋。
不再提供塑膠袋的商店包括在泰國管理上千家7-11的CP All Plc公司和泰國購物商場集團。其實,這並不是泰國第一次嘗試縮減塑膠袋使用的政策。早在2018年,CAN便報導過泰國旅遊局曾發佈「國家淨化聲明」,希望在2021年時,能減少一半以上現行使用的塑膠袋總用量。
本站聲明:網站內容來源環境資訊中心https://e-info.org.tw/,如有侵權,請聯繫我們,我們將及時處理
【其他文章推薦】
※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面
※網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!
※想知道最厲害的網頁設計公司"嚨底家"!
※幫你省時又省力,新北清潔一流服務好口碑
※別再煩惱如何寫文案,掌握八大原則!
Angular 入坑記錄的筆記第七篇,介紹 Angular 中的模塊的相關概念,了解相關的使用場景,以及知曉如何通過特性模塊來組織我們的 Angular 應用
對應官方文檔地址:
前端模塊化是指將程序中一組相關的功能按照一定的規則組織在一塊,整個模塊內部的數據和功能實現是私有的,通過 export 暴露其中的一些接口(方法)與系統中的別的模塊進行通信
在 Angular 應用中,至少會存在一個 NgModule,也就是應用的根模塊(AppModule),通過引導這個根模塊就可以啟動整個項目
像開發中使用到 FormsModule、HttpClientModule 這種 Angular 內置的庫也都是一個個的 NgModule,在開發中通過將組件、指令、管道、服務或其它的代碼文件聚合成一個內聚的功能塊,專註於系統的某個功能模塊
| 模塊名稱 | 模塊所在文件 | 功能點 |
|---|---|---|
| BrowserModule | @angular/platform-browser | 用於啟動和運行瀏覽器應用的的基本服務 |
| CommonModule | @angular/common | 使用 NgIf、NgFor 之類的內置指令 |
| FormsModule | @angular/forms | 使用 NgModel 構建模板驅動表單 |
| ReactiveFormsModule | @angular/forms | 構建響應式表單 |
| RouterModule | @angular/router | 使用前端路由 |
| HttpClientModule | @angular/common/http | 發起 http 請求 |
在 JavaScript 中,每一個 js 文件就是一個模塊,文件中定義的所有對象都從屬於那個模塊。 通過 export 關鍵字,模塊可以把其中的某些對象聲明為公共的,從而其它 JavaScript 模塊可以使用 import 語句來訪問這些公共對象
例如下面的示例代碼中,別的 javascript 模塊可以通過導入這個 js 文件來直接使用暴露的 getRoles 和 getUserInfo 方法
function getRoles() {
// ...
}
function getUserInfo() {
// ...
}
export {
getRoles,
getUserInfo
}
NgModule 是一個帶有 @NgModule 裝飾器的類,通過函數的參數來描述這個模塊,例如在上節筆記中創建的 CrisisModule,定義了我們在該特性模塊中創建的組件,以及需要使用到的其它模塊
在使用 @NgModule 裝飾器時,通常會使用到下面的屬性來定義一個模塊
declarations:當前模塊中的組件、指令、管道
imports:當前模塊所需的其它 NgModule 模塊
exports:其它模塊中可以使用到當前模塊可聲明的對象
providers:當前模塊向當前應用中其它應用模塊暴露的服務
bootstrap:用來定義整個應用的根組件,是應用中所有其它視圖的宿主,只有根模塊中才會存在
根模塊是用來啟動此 Angular 應用的模塊, 按照慣例,它通常命名為 AppModule
通過 Angular CLI 新建一個應用后,默認的根模塊代碼如下,通過使用 @NgModule 裝飾器裝飾 AppModule 類,定義了這個模塊的一些屬性特徵,從而告訴 Angular 如何編譯和啟動本應用
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
@NgModule({
declarations: [
AppComponent
],
imports: [
BrowserModule,
AppRoutingModule
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }
declarations 數組告訴 Angular 哪些組件屬於當前模塊。 當創建新的組件時,需要將它們添加到 declarations 數組中。每個組件都只能聲明在一個 NgModule 類中,同時,如果你使用了未聲明過的組件,Angular 將會報錯
同樣的,對於當前模塊使用到的自定義指令、自定義管道,也需要在 declarations 數組中進行聲明
imports 數組表明當前模塊正常工作時需要引入哪些的模塊,例如這裏使用到的 BrowserModule、AppRoutingModule 或者是我們使用雙向數據綁定時使用到的 FormsModule,它表現出當前模塊的一個依賴關係
providers 數組定義了當前模塊可以提供給當前應用其它模塊的各項服務,例如一個用戶模塊,提供了獲取當前登錄用戶信息的服務,因為應用中的其它地方也會存在調用的可能,因此,可以通過添加到 providers 數組中,提供給別的模塊使用
Angular 應用通過引導根模塊來啟動的,因為會涉及到構建組件樹,形成實際的 DOM,因此需要在 bootstrap 數組中添加根組件用來作為組件樹的根
特性模塊是用來將特定的功能或具有相關特性的代碼從其它代碼中分離出來,聚焦於特定應用需求。特性模塊通過它提供的服務以及共享出的組件、指令和管道來與根模塊和其它模塊合作
在上一章中,定義了一個 CrisisModule 用來包括包含與危機有關的功能模塊,創建特性模塊時可以通過 Angular CLI 命令行進行創建
-- 創建名為 xxx 的特性模塊
ng new component xxx
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { CrisisRoutingModule } from './crisis-routing.module';
import { FormsModule } from '@angular/forms';
import { CrisisListComponent } from './crisis-list/crisis-list.component';
import { CrisisDetailComponent } from './crisis-detail/crisis-detail.component';
@NgModule({
declarations: [
CrisisListComponent,
CrisisDetailComponent
],
imports: [
CommonModule,
FormsModule,
CrisisRoutingModule
]
})
export class CrisisModule { }
當創建完成后,為了將該特性模塊包含到應用中,需要和 BrowserModule、AppRoutingModule 一樣,在根模塊中 imports 引入
默認情況下,NgModule 都是急性加載的,也就是說它會在應用加載時儘快加載,所有模塊都是如此,無論是否立即要用。對於帶有很多路由的大型應用,考慮使用惰性加載的模式。惰性加載可以減小初始包的尺寸,從而減少程序首次的加載時間
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
// 添加自定義的模塊
import { CrisisModule } from './crisis/crisis.module';
@NgModule({
declarations: [
AppComponent
],
imports: [
BrowserModule,
FormsModule,
CrisisModule, // 引入自定義模塊
AppRoutingModule
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }
本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理
【【其他文章推薦】
※帶您來了解什麼是 USB CONNECTOR ?
※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面
※如何讓商品強力曝光呢? 網頁設計公司幫您建置最吸引人的網站,提高曝光率!
※綠能、環保無空污,成為電動車最新代名詞,目前市場使用率逐漸普及化
※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益
※教你寫出一流的銷售文案?
前两天,我們已經介紹了關於JdbcTemplate的多數據源配置以及Spring Data JPA的多數據源配置,接下來具體說說使用MyBatis時候的多數據源場景該如何配置。
先在Spring Boot的配置文件application.properties中設置兩個你要鏈接的數據庫配置,比如這樣:
spring.datasource.primary.jdbc-url=jdbc:mysql://localhost:3306/test1
spring.datasource.primary.username=root
spring.datasource.primary.password=123456
spring.datasource.primary.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.secondary.jdbc-url=jdbc:mysql://localhost:3306/test2
spring.datasource.secondary.username=root
spring.datasource.secondary.password=123456
spring.datasource.secondary.driver-class-name=com.mysql.cj.jdbc.Driver
說明與注意:
完成多數據源的配置信息之後,就來創建個配置類來加載這些配置信息,初始化數據源,以及初始化每個數據源要用的MyBatis配置。
這裏我們繼續將數據源與框架配置做拆分處理:
@Configuration
public class DataSourceConfiguration {
@Primary
@Bean
@ConfigurationProperties(prefix = "spring.datasource.primary")
public DataSource primaryDataSource() {
return DataSourceBuilder.create().build();
}
@Bean
@ConfigurationProperties(prefix = "spring.datasource.secondary")
public DataSource secondaryDataSource() {
return DataSourceBuilder.create().build();
}
}
可以看到內容跟JdbcTemplate、Spring Data JPA的時候是一模一樣的。通過@ConfigurationProperties可以知道這兩個數據源分別加載了spring.datasource.primary.*和spring.datasource.secondary.*的配置。@Primary註解指定了主數據源,就是當我們不特別指定哪個數據源的時候,就會使用這個Bean真正差異部分在下面的JPA配置上。
Primary數據源的JPA配置:
@Configuration
@MapperScan(
basePackages = "com.didispace.chapter39.p",
sqlSessionFactoryRef = "sqlSessionFactoryPrimary",
sqlSessionTemplateRef = "sqlSessionTemplatePrimary")
public class PrimaryConfig {
private DataSource primaryDataSource;
public PrimaryConfig(@Qualifier("primaryDataSource") DataSource primaryDataSource) {
this.primaryDataSource = primaryDataSource;
}
@Bean
public SqlSessionFactory sqlSessionFactoryPrimary() throws Exception {
SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
bean.setDataSource(primaryDataSource);
return bean.getObject();
}
@Bean
public SqlSessionTemplate sqlSessionTemplatePrimary() throws Exception {
return new SqlSessionTemplate(sqlSessionFactoryPrimary());
}
}
Secondary數據源的JPA配置:
@Configuration
@MapperScan(
basePackages = "com.didispace.chapter39.s",
sqlSessionFactoryRef = "sqlSessionFactorySecondary",
sqlSessionTemplateRef = "sqlSessionTemplateSecondary")
public class SecondaryConfig {
private DataSource secondaryDataSource;
public SecondaryConfig(@Qualifier("secondaryDataSource") DataSource secondaryDataSource) {
this.secondaryDataSource = secondaryDataSource;
}
@Bean
public SqlSessionFactory sqlSessionFactorySecondary() throws Exception {
SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
bean.setDataSource(secondaryDataSource);
return bean.getObject();
}
@Bean
public SqlSessionTemplate sqlSessionTemplateSecondary() throws Exception {
return new SqlSessionTemplate(sqlSessionFactorySecondary());
}
}
說明與注意:
@MapperScan註解來指定當前數據源下定義的Entity和Mapper的包路徑;另外需要指定sqlSessionFactory和sqlSessionTemplate,這兩個具體實現在該配置類中類中初始化。@Qualifier註解來指定具體要用哪個數據源,其名字對應在DataSourceConfiguration配置類中的數據源定義的函數名。上一篇介紹JPA的時候,因為之前介紹JPA的使用時候,說過實體和Repository定義的方法,所以省略了 User 和 Repository的定義代碼,但是還是有讀者問怎麼沒有這個,其實都有說明,倉庫代碼里也都是有的。未避免再問這樣的問題,所以這裏就貼一下吧。
根據上面Primary數據源的定義,在com.didispace.chapter39.p包下,定義Primary數據源要用的實體和數據訪問對象,比如下面這樣:
@Data
@NoArgsConstructor
public class UserPrimary {
private Long id;
private String name;
private Integer age;
public UserPrimary(String name, Integer age) {
this.name = name;
this.age = age;
}
}
public interface UserMapperPrimary {
@Select("SELECT * FROM USER WHERE NAME = #{name}")
UserPrimary findByName(@Param("name") String name);
@Insert("INSERT INTO USER(NAME, AGE) VALUES(#{name}, #{age})")
int insert(@Param("name") String name, @Param("age") Integer age);
@Delete("DELETE FROM USER")
int deleteAll();
}
根據上面Secondary數據源的定義,在com.didispace.chapter39.s包下,定義Secondary數據源要用的實體和數據訪問對象,比如下面這樣:
@Data
@NoArgsConstructor
public class UserSecondary {
private Long id;
private String name;
private Integer age;
public UserSecondary(String name, Integer age) {
this.name = name;
this.age = age;
}
}
public interface UserMapperSecondary {
@Select("SELECT * FROM USER WHERE NAME = #{name}")
UserSecondary findByName(@Param("name") String name);
@Insert("INSERT INTO USER(NAME, AGE) VALUES(#{name}, #{age})")
int insert(@Param("name") String name, @Param("age") Integer age);
@Delete("DELETE FROM USER")
int deleteAll();
}
完成了上面之後,我們就可以寫個測試類來嘗試一下上面的多數據源配置是否正確了,先來設計一下驗證思路:
具體實現如下:
@Slf4j
@RunWith(SpringRunner.class)
@SpringBootTest
@Transactional
public class Chapter39ApplicationTests {
@Autowired
private UserMapperPrimary userMapperPrimary;
@Autowired
private UserMapperSecondary userMapperSecondary;
@Before
public void setUp() {
// 清空測試表,保證每次結果一樣
userMapperPrimary.deleteAll();
userMapperSecondary.deleteAll();
}
@Test
public void test() throws Exception {
// 往Primary數據源插入一條數據
userMapperPrimary.insert("AAA", 20);
// 從Primary數據源查詢剛才插入的數據,配置正確就可以查詢到
UserPrimary userPrimary = userMapperPrimary.findByName("AAA");
Assert.assertEquals(20, userPrimary.getAge().intValue());
// 從Secondary數據源查詢剛才插入的數據,配置正確應該是查詢不到的
UserSecondary userSecondary = userMapperSecondary.findByName("AAA");
Assert.assertNull(userSecondary);
// 往Secondary數據源插入一條數據
userMapperSecondary.insert("BBB", 20);
// 從Primary數據源查詢剛才插入的數據,配置正確應該是查詢不到的
userPrimary = userMapperPrimary.findByName("BBB");
Assert.assertNull(userPrimary);
// 從Secondary數據源查詢剛才插入的數據,配置正確就可以查詢到
userSecondary = userMapperSecondary.findByName("BBB");
Assert.assertEquals(20, userSecondary.getAge().intValue());
}
}
本文的相關例子可以查看下面倉庫中的chapter3-9目錄:
如果您覺得本文不錯,歡迎Star支持,您的關注是我堅持的動力!
本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理
【其他文章推薦】
※為什麼 USB CONNECTOR 是電子產業重要的元件?
※網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!
※台北網頁設計公司全省服務真心推薦
※想知道最厲害的網頁設計公司"嚨底家"!
※新北清潔公司,居家、辦公、裝潢細清專業服務
※推薦評價好的iphone維修中心
在微服務架構中 API網關 非常重要,網關作為全局流量入口並不單單是一個反向路由,更多的是把各個邊緣服務(Web層)的各種共性需求抽取出來放在一個公共的“服務”(網關)中實現,例如安全認證、權限控制、限流熔斷、監控、跨域處理、聚合API文檔等公共功能。
在以 Dubbo 框架體系來構建的微服務架構下想要增加API網關,如果不想自研開發的情況下在目前的開源社區中幾乎沒有找到支持dubbo協議的主流網關,但是 Spring Cloud 體系下卻有兩個非常熱門的開源API網關可以選擇;本文主要介紹如何通過 Nacos 整合 Spring Cloud Gateway 與 Dubbo 服務。
dubbo屬於rpc調用,所以必須提供一個web層的服務作為http入口給客戶端調用,並在上面提供安全認證等基礎功能,而web層前面對接Nginx等反向代理用於統一入口和負載均衡。
web層一般是根據業務模塊來切分的,用於聚合某個業務模塊所依賴的各個service服務
PS:我們能否把上圖中的web層全部整合在一起成為一個API網關呢?(不建議這樣做)
因為這樣的web層並沒有實現 泛化調用 必須引入所有dubbo服務的api依賴,會使得網關變得非常不穩定,任何服務的接口變更都需要修改網關中的api依賴!
下面就開始聊聊直接拿熱門的 Srping Cloud Gateway 來作為dubbo架構體系的網關是否可行,首先該API網關是屬於 Spring Cloud 體系下的組件之一,要整合dubbo的話需要解決以下問題:
上面提到的第一個問題“打通註冊中心”其實已經不是問題了,目前dubbo支持
Zookeeper與Nacos兩個註冊中心,而 Spring Cloud 自從把@EnableEurekaClient改為@EnableDiscoveryClient之後已經基本上支持所有主流的註冊中心了,本文將使用Nacos作為註冊中心打通兩者
把傳統dubbo架構中的 Nginx 替換為 Spring Cloud Gateway ,並把 安全認證 等共性功能前移至網關處實現
由於web層服務本身提供的就是http接口,所以網關層無需作協議轉換,但是由於
安全認證前移至網關了需要通過網絡隔離的手段防止被繞過網關直接請求後面的web層
dubbo服務本身修改或添加 rest 傳輸協議的支持,這樣網關就可以通過http傳輸協議與dubbo服務通信了
rest傳輸協議:基於標準的Java REST API——JAX-RS 2.0(Java API for RESTful Web Services的簡寫)實現的REST調用支持
目前版本的dubbo已經支持dubbo、rest、rmi、hessian、http、webservice、thrift、redis等10種傳輸協議了,並且還支持同一個服務同時定義多種協議,例如配置 protocol = { “dubbo”, “rest” } 則該服務同時支持
dubbo與rest兩種傳輸協議
方式一 對比 方式二 多了一層web服務所以多了一次網絡調用開銷,但是優點是各自的職責明確單一,web層可以作為聚合層用於聚合多個service服務的結果經過融合加工一併返回給前端,所以這種架構下能大大減少服務的 循環依賴
在根目錄的 pom.xml 中定義全局的依賴版本
<properties>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<java.version>8</java.version>
<spring-boot-dependencies.version>2.2.8.RELEASE</spring-boot-dependencies.version>
<spring-cloud-dependencies.version>Hoxton.SR5</spring-cloud-dependencies.version>
<spring-cloud-alibaba-dependencies.version>2.2.1.RELEASE</spring-cloud-alibaba-dependencies.version>
<jaxrs.version>3.12.1.Final</jaxrs.version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>${spring-boot-dependencies.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud-dependencies.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>${spring-cloud-alibaba-dependencies.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
分別定義兩個api接口
DubboService 使用dubbo協議的服務
public interface DubboService {
String test(String param);
}
RestService 使用rest協議的服務
public interface RestService {
String test(String param);
}
使用 方式一 整合對接網關,這裏為了簡化在同一個服務下只使用邏輯分層定義controller層與service層,並沒有做服務拆分
定義 spring boot 配置
server:
port: 8081
spring:
application:
name: zlt-web-dubbo
main:
allow-bean-definition-overriding: true
cloud:
nacos:
server-addr: 192.168.28.130:8848
username: nacos
password: nacos
server.port:配置應用服務器暴露的端口
spring.cloud.nacos:配置 spring cloud 的註冊中心相關參數,nacos 的配置需要改為自己環境所對應
定義 dubbo 配置
dubbo:
scan:
base-packages: org.zlt.service
protocols:
dubbo:
name: dubbo
port: -1
registry:
address: spring-cloud://localhost
consumer:
timeout: 5000
check: false
retries: 0
cloud:
subscribed-services:
dubbo.scan.base-packages:指定 Dubbo 服務實現類的掃描基準包
dubbo.protocols:服務暴露的協議配置,其中子屬性name為協議名稱,port為協議端口( -1 表示自增端口,從 20880 開始)
dubbo.registry.address:Dubbo 服務註冊中心配置,其中子屬性address的值 “spring-cloud://localhost”,說明掛載到 Spring Cloud 註冊中心
通過 protocol = "dubbo" 指定使用 dubbo協議 定義服務
@Service(protocol = "dubbo")
public class DubboServiceImpl implements DubboService {
@Override
public String test(String param) {
return "dubbo service: " + param;
}
}
使用 Spring Boot 的 @RestController 註解定義web服務
@RestController
public class WebController {
@Autowired
private DubboService dubboService;
@GetMapping("/test/{p}")
public String test(@PathVariable("p") String param) {
return dubboService.test(param);
}
}
使用 方式二 整合對接網關,由於該服務是通過dubbo來創建rest服務,所以並不需要使用 Spring Boot 內置應用服務
定義 spring boot 配置
spring:
application:
name: zlt-rest-dubbo
main:
allow-bean-definition-overriding: true
cloud:
nacos:
server-addr: 192.168.28.130:8848
username: nacos
password: nacos
因為不使用 Spring Boot 內置的應用服務所以這裏並不需要指定
server.port
定義 dubbo 配置
dubbo:
scan:
base-packages: org.zlt.service
protocols:
dubbo:
name: dubbo
port: -1
rest:
name: rest
port: 8080
server: netty
registry:
address: spring-cloud://localhost
consumer:
timeout: 5000
check: false
retries: 0
cloud:
subscribed-services:
dubbo.protocols:配置兩種協議,其中rest協議定義 8080 端口並使用 netty 作為應用服務器
通過 protocol = "rest" 指定使用 rest協議 定義服務
@Service(protocol = "rest")
@Path("/")
public class RestServiceImpl implements RestService {
@Override
@Path("test/{p}")
@GET
public String test(@PathParam("p") String param) {
return "rest service: " + param;
}
}
定義 spring boot 配置
server:
port: 9900
spring:
application:
name: sc-gateway
main:
allow-bean-definition-overriding: true
cloud:
nacos:
server-addr: 192.168.28.130:8848
username: nacos
password: nacos
server.port:定義網關端口為 9090
定義網關配置
spring:
cloud:
gateway:
discovery:
locator:
lowerCaseServiceId: true
enabled: true
routes:
- id: web
uri: lb://zlt-web-dubbo
predicates:
- Path=/api-web/**
filters:
- StripPrefix=1
- id: rest
uri: lb://zlt-rest-dubbo
predicates:
- Path=/api-rest/**
filters:
- StripPrefix=1
分別定義兩個路由策略:
/api-web/ 為請求 web-dubbo 工程/api-rest/ 為請求 rest-dubbo 工程
分別啟動:Nacos、sc-gateway、web-dubbo、rest-dubbo 工程,通過網關的以下兩個接口分別測試兩種整合方式
web-dubbo 工程測試整合方式一rest-dubbo 工程測試整合方式二
ide需要安裝 lombok 插件
https://github.com/zlt2000/dubboSpringCloud
掃碼關注有驚喜!
本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理
【其他文章推薦】
※網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!
※網頁設計公司推薦不同的風格,搶佔消費者視覺第一線
※想知道購買電動車哪裡補助最多?台中電動車補助資訊懶人包彙整
※南投搬家公司費用,距離,噸數怎麼算?達人教你簡易估價知識!
※教你寫出一流的銷售文案?
※超省錢租車方案
halo,相信大家一定過了一個很開心的端午節吧,我看朋友圈裡各種曬旅遊,曬美食的,真是羡慕啊,不像我,感冒了只能在家擼文章。
當然,玩的多開心,節後上班就有多鬱悶,假日綜合征可不是說說而已。對此我想表達的是,沒事,不用鬱悶,來看我如何自爆家醜來讓你們開心下。
上周四午休時分,我正在工位上小憩,睡夢中彷彿看到了自己拿着李白在榮耀峽谷里大殺四方的情景,就在我剛拿完五殺準備帶領隊友推對面水晶的時候,一句慌亂急促的“糟了”把我從睡夢中驚醒。我眯開朦朧的雙眼,才發現剛才的發聲來源於我的組長庄哥,看到他在緊張的點開日誌系統查看日誌,我預感到有什麼不妙的事情發生,仔細一問才知道,原來就在我眯眼的期間,線上數據庫服務器的CPU被打滿,同時觸發了生產數據庫只讀延遲的限定時間並且發出告警,而且告警的過程持續了半個小時。
這讓我倒吸了一口涼氣,因為我們組做的系統很多都用的是同一個數據庫服務器,日用戶活躍量有好幾十萬,如果服務器崩潰了將會使所有的系統服務都不可用,於是我們趕緊通過sql日誌進行問題查找,最後排查出來是因為一張sql的高量查詢沒有走索引導致,日誌列表显示,這條sql語句的掃描行數達到了上百萬,基本就是全表掃描的情況,而且半個小時的時間查詢了達上萬次,每條sql查詢的耗時都在3000ms以上。我的天啊,難怪服務器會CPU打滿,這麼一條耗時的sql語句查詢量這麼大,數據庫的資源當然是直接就崩潰了,這是當時那條sql的查詢情況:
看了這條語句,我又倒吸一口涼氣,這不就是我寫的系統調用的sql語句嗎?完了,這回逃不掉了,真是人在睡夢裡,鍋從天上來。
當然,因為是我自己寫的sql,所以我一看就知道這條語句是有問題的。
根據我的代碼處理,這條sql的調用還少了個重要的參數user_fruit_id,這個參數沒有傳的話是不應該走這條sql查詢的,在我的設計里,該參數是數據表裡一個聯合索引的最左側字段,如果該字段沒有傳值的話,那麼索引就不會生效了。
KEY `idx_userfruitid_type` (`user_fruit_id`,`task_type`,`receive_start_time`,`receive_end_time`) USING BTREE
雖然定位到了sql語句,但是線上的問題刻不容緩,總不可能找出bug改完再上線吧,所以,我們只能做了一個臨時處理,就是在原來的表上多加了一個聯合索引,其實就是去掉了user_fruit_id 字段,讓這些高量的查詢都能走新的索引,就像下面這樣
KEY `idx_task_type_receive_start_time` (`task_type`,`receive_start_time`,`receive_end_time`,`created_time`) USING BTREE
加上索引后,sql的掃描行數就大幅度的降低了,重啟實例后就又能正常運行了。
那麼為什麼最左側的字段沒傳索引就不生效了,這是因為MySQL的聯合索引是基於“最左匹配原則”匹配的。
我們都知道,索引的底層是B+樹結構,聯合索引的結構也是B+樹,只不過鍵值數量不是一個,而是多個,構建一顆B+樹只能根據一個值來構建,因此數據庫依據聯合索引最左的字段來構建B+樹。
例如我們用兩個字段(name,age)這個聯合索引來分析,
圖片來源於林曉斌老師的《MySQL實戰45講》課程,
當我們在where條件中查找name為“張三”的所有記錄的時候,可以快速定位到ID4,並且查出所有包含“張三”的記錄,而如果要查找“張三,10”這一條特定的數據,就可以用 name = “張三” and age = 10 獲取,因為聯合索引的鍵值對是兩個,所以只要前面的name確定的情況下就可以進一步定位到具體的age記錄,但是如果你的查詢條件只有age的話,那麼索引就不會生效,因為沒有匹配最左邊的字段,後面所有的索引字段都不會生效,所以我之前寫的sql語句才會因為少了最左邊的user_fruit_id字段而走了全表掃描的查詢方式。
正常來說,假設一個聯合索引設計成(a,b)這樣的結構的話,那麼用a and b作為條件,或者a單獨作為查詢條件都會走索引,這種情況下我們就不要再為a字段單獨設計索引了。
但如果查詢條件裏面只有b的語句,是無法使用(a,b)這個聯合索引的,這時候你不得不維護另外一個索引,也就是說你需要同時維護(a,b)、(b) 這兩個索引。
雖然臨時做了處理,但問題並不算解決,很明顯是系統出現了bug才會有走這樣的查詢條件。因為是我自己寫的代碼,所以知道是哪條sql后我就馬上定位到了代碼里的具體方法,後來才發現是因為我對user_fruit_id字段的判空處理不生效所致。
因為該字段是從調用方傳過來的,所以我在方法參數里對該字段做了非空限制的註解,也就是javax包下的@NotNull,
public class GardenUserTaskListReq implements Serializable {
private static final long serialVersionUID = -9161295541482297498L;
@ApiModelProperty(notes = "水果id")
@NotNull(message = "水果id不能為空")
private Long userFruitId;
/**以下省略*/
.....................
}
雖然加上該註解來做非空校驗,但我卻沒有在參數加上另一個註解@Validated,該註解如果沒加上的話,那麼調用javax包下的校驗規則就都不生效,正確的寫法是在controller層方法的參數前面加上註解,
除此之外,因為user_fruit_id這個字段是另一張表的主鍵,我在代碼里也沒有對這張表是否存在這個id做查詢判斷,這樣一來,無論調用方傳什麼值過來都會直接觸發sql查詢,並且在不跑索引的情況下直接走全表掃描。
不得不說,這真是個低級錯誤,說真的,我對這個原因真是感到嘀笑皆非,再怎麼說也工作幾年了,怎麼還犯一些新手級別的錯誤呢,這臉打得真是讓我相當慚愧。
雖然是低級錯誤,但造成的後果也算挺嚴重了,這次事件也讓我更加的警醒,在以後的開發工作中必須要遵守該有的原則,大概有這麼幾點:
1、不能相信調用端。重要的參數都要先做驗證,即使是非空值也需要做驗證,不符合條件的就要直接返回或拋異常,不能參与業務sql的查詢,否則頻繁的訪問也會對服務造成負擔。
2、sql語句要先做性能查詢。對於數據量大的表,建好索引后,所有的sql查詢語句要用explain檢測性能,並且根據結果來進一步優化索引。
3、代碼必須要review。之前我沒有放太大的精力在代碼的review上,雖說跟迭代排期的緊湊也有關係,但不管怎麼說,bug確實是我的疏忽造成的,尤其是像空值這種細小的錯誤在Java里可以說家常便飯。千里之堤毀於蟻穴,有時一個小bug很容易就引發整個系統的崩盤,這一次的問題也讓我更加深刻的認識到了review代碼的重要性,不管業務開發的工作量有多麻煩,這一步操作絕對不能忽視。
知道了bug的原因,改完代碼當天就重新發布了,後來,庄哥告訴我說,為了以後讓組裡的其他人對此次問題有所警戒,讓我寫一篇問題記錄總結一下,我想了一下,這不是我的強項啊,但怎麼說也確實是自己的問題,還是老老實實的寫一下記錄好了。我本以為這樣就可以松一口氣了,可平哥 (組裡的一位大佬) 卻突然用詭異的眼神看着我,語重心長的說,上次xxx也因為線上出現問題寫了報告,你這一次估計也不能例外了,可能要一萬字以上。我瞬間就感覺一個雷劈到了我頭上,蒼天啊。。。。。。
本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理
【其他文章推薦】
※網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!
※網頁設計公司推薦不同的風格,搶佔消費者視覺第一線
※Google地圖已可更新顯示潭子電動車充電站設置地點!!
※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益
※別再煩惱如何寫文案,掌握八大原則!
相信很多朋友對於邏輯式編程語言,都有一種最熟悉的陌生人的感覺。一方面,平時在書籍、在資訊網站,偶爾能看到一些吹噓邏輯式編程的話語。但另一方面,也沒見過周圍有人真正用到它(除了SQL)。
遙記當時看《The Reasoned Schemer》(一本講邏輯式編程語言的小人書),被最後兩頁的解釋器實現驚艷到了。看似如此複雜的計算邏輯,其實現竟然這麼簡潔。不過礙於當時水平有限,也就囫圇吞棗般看了過去。後來有一天,不知何故腦子靈光一閃,把圖遍歷和流計算模式聯繫在一起,瞬間明白了《The Reasoned Schemer》中的做法。動手寫了寫代碼,果然如此,短短兩百來行代碼,就完成了解釋器的實現,才發現原來如此簡單。很多時候,並非問題本身有多難,只是沒有想到正確的方法。
本系列將盡可能簡潔地說明邏輯式編程語音的原理,並實現一門簡單的邏輯式編程語言。考慮到C#的用戶較多,因此選擇用C#來實現。實現的這門語言就叫NMiniKanren。文章總體內容如下:
故事從兩個正在吃午餐的程序員說起。
老明和小皮是就職於同一家傳統企業的程序員。這天,兩人吃着午餐。老明邊吃邊刷着抖音,鼻孔時不時噴出幾條米粉。
小皮是一臉麻木地刷着求職網和資訊網,忽然幾個大字映入眼底:《新型邏輯式編程語言重磅出世,即將顛覆IT界!》小皮一陣好奇,往下一翻,結果接着的是一些難懂的話,什麼“一階邏輯”,什麼“合一算法”,以及鬼畫符似的公式之類。
小皮看得索然無味,但被勾引起來的對邏輯式編程的興趣彷彿澳洲森林大火一樣難以平息。於是伸手拍下老明高举手機的左手,問道:“嘿!邏輯式編程有了解過么?是個啥玩意兒?”
“邏輯式編程啊……嘿嘿,前段時間剛好稍微了解了一下。”老明鼻孔朝天吸了兩口氣,“我說的稍微了解,是指實現了一門邏輯式編程語言。”
“不愧是資深老IT,了解也比別人深入一坨坨……”
“也就比你早來一年好不好……我是一邊看一本奇書一邊做的。Dan老師(Dan Friedman)寫的《The Reasoned Schemer》。這本書挺值得一看的,書中使用一門教學用的邏輯式編程語言,講解這門語言的特性、用法、以及原理。最後還給出了這門語言的實現。核心代碼只用了兩頁紙。
“所謂邏輯式編程,從使用上看是把聲明式編程發揮到極致的一種編程範式。普通的編程語言,大部分還是基於命令式編程,需要你告訴機器每一步執行什麼指令。而邏輯式編程的理念是,我們只需要告訴機器我們需要的目標,機器會根據這個目標自動探索執行過程。
“邏輯式編程的特點是可以反向運行。你可以像做數學題一樣,聲明未知量,列出方程,然後程序會為你求解未知量。”
“挺神奇的。聽起來有點像AI編程。不過這麼高級的東西怎麼沒有流行起來?感覺可以節省不少人力。”小皮忽然有種飯碗即將不保的感覺。
“嘿嘿……想得美。其實邏輯式編程,既不智能,也不好用。你回憶一下你中學的時候是怎麼解方程組的?”
“嗯……先盯一會方程組,看看它長得像不像有快捷解法的樣子。看不出來的話就用代入法慢慢算。這和邏輯式編程有什麼關係?”
“邏輯式編程並不智能,它只是把某種類似代入法的通用算法內置到解釋器里。邏輯式編程語言寫的程序運行時,不過是根據通用算法進行求解而已。它不像人一樣會去尋找更快捷的方法,同時也不能解決超綱問題。
“而且邏輯式編程語言的學習成本也不低。如果你要用好這門語言,你得把它使用的通用算法搞清楚。雖然你寫的聲明式的代碼,但內心要時刻清楚程序的執行過程。如果你拿它當個黑盒來用,那很可能你寫出來的程序的執行效率會非常低,甚至跑出一些莫名其妙的結果。”
“哦哦,要學會用它,還得先懂得怎麼實現它。這學習成本還挺高的。”小皮跟着吐槽,不過他知道老明表明上看似嫌棄邏輯式編程的實用性,私底下肯定玩得不亦樂乎,並且也喜歡跟別人分享。於是小皮接着道:“雖然應該是用不着,但感覺挺有意思的,再仔細講講唄。天天寫CRUD,腦子都淡出個鳥了。”
果然老明坐直起來:“《The Reasoned Schemer》用的這門邏輯式編程語言叫miniKanren,用Scheme/Lisp實現的。去年給你安利過Scheme了,現在掌握得怎麼樣?”
“一竅不通……”小皮大窘。去年到現在,小皮一直很忙,並沒有自學什麼東西。如果沒有外力驅動的話,他還將一直忙下去。
“果然如此。所以我順手也實現了個C#魔改版本的miniKanren。就叫NMiniKanren。我把NMiniKanren實現為C#的一個DSL。這樣的好處是方便熟悉C#或者Java的人快速上手;壞處是DSL會受限於C#語言的能力,代碼看起來沒有Scheme版那麼優雅。”老明用左手做了個打引號的動作,“先從簡單的例子開始吧。比如說,有個未知量q,我們的目標是讓q等於5或者等於6。那麼滿足條件的q值有哪些?”
“不就是5和6么……這也太簡單了吧。”
“Bingo!”老明打了個響指,“我們先用簡單的例子看看代碼結構。”只見老明兩指輕輕夾住一隻筷子,勾出幾條米粉,快速在桌上擺出如下代碼:
// k提供NMiniKanren的方法,q是待求解的未知變量。
var res = KRunner.Run(null /* null表示輸出所有可能的結果 */, (k, q) =>
{
// q == 5 或者 q == 6
return k.Any(
k.Eq(q, 5),
k.Eq(q, 6));
});
KRunner.PrintResult(res); // 輸出結果:[5, 6]
“代碼中,KRunner.Run用於運行一段NMiniKanren代碼,它的聲明如下。”老明繼續撥動米粉:
public class KRunner
{
public static IList<object> Run(int? n, Func<KRunner, FreshVariable, Goal> body)
{
...
}
}
“其中,參數n是返回結果的數量限制,n = null表示無限制;參數body是一個函數:
KRunner實例,用於引用NMiniKanren方法;“接着我們看函數體的代碼。k.Eq(q, 5)表示q需要等於5,k.Eq(q, 6)表示q需要等於6,k.Any表示滿足至少一個條件。整段代碼的意思為:求所有滿足q等於5或者q等於6的q值。顯然答案為5和6,程序的運行結果也是如此。很神奇吧?”
“你這米粉打碼的功夫更讓我驚奇……”小皮仔細看了一會,“原來如此。不過這DSL的語法確實看着比較累。”
“主要是我想做得簡單一些。其實使用C#的Lambda表達式也可以實現像……”老明勾出幾條米粉擺出q == 5 || q == 6表達式,“……這樣的語法,不過這樣會增加NMiniKanren實現的複雜度。況且這無非是前綴表達式或中綴表達式這種語法層面的差別而已,語義上並沒有變化。學習應先抓住重點,花里胡哨的東西可以放到最後再來琢磨。”
“嗯嗯。KRunner.Run里這個null的參數是做什麼用的呢?”
“KRunner.Run的第一個參數用來限制輸出結果的數量。null表示輸出所有可能的結果。還是上面例子的條件,我們改成限制只輸出1個結果。”小皮用筷子改了下代碼:
// k提供NMiniKanren的方法,q是待求解的未知變量。
var res = KRunner.Run(1 /* 輸出1個結果 */, (k, q) =>
{
// q == 5 或者 q == 6
return k.Any(
k.Eq(q, 5),
k.Eq(q, 6));
});
KRunner.PrintResult(res); // 輸出結果:[5]
“這樣程序只會輸出5一個結果。在一些包含遞歸的代碼中,可能會有無窮多個結果,這種情況下需要限制輸出結果的數量來避免程序不會終止。”
“原來如此。不過這個例子太簡單了,有沒有其他更好玩的例子。”
老明喝下一口湯,說:“好。時間不早了,我們回公司找個會議室慢慢說。”
到公司后,老明的講課開始了……
首先,要先明確NMiniKanren支持的數據類型。後續代碼都要基於數據類型來編寫,所以規定好數據類型是基礎中的基礎。
簡單起見,NMiniKanren只支持四種數據類型:
string:就是一個普普通通的值類型,僅有值相等判斷。int:同string。使用int是因為有時候想少寫兩個雙引號……KPair:二元組。可用來構造鏈表及其他複雜的數據結構。如果你學過Lisp會對這個數據結構很熟悉。下面詳細說明。null:這個類型只有null一個值。表示空引用或者空數組。KPair的定義為:
public class KPair
{
public object Lhs { get; set; }
public object Rhs { get; set; }
// methods
...
}
KPair除了用作二元組(其實是最少用的)外,更多的是用來構造鏈表。構造鏈表時,約定一個KPair作為一個鏈表的節點,Lhs為元素值,Rhs為一下個節點。當Rhs為null時鏈表結束。空鏈表用null表示。
public static KPair List(IEnumerable<object> lst)
{
var fst = lst.FirstOrDefault();
if (fst == null)
{
return null;
}
return new KPair(fst, List(lst.Skip(1)));
}
使用
null表示空鏈表其實並不合適,這裏純粹是為了簡單而偷了個懶。
我們知道,很多複雜的數據結構都是可以通過鏈表來構造的。所以雖然NMiniKanren只有三種數據類型,但可以表達很多數據結構了。
這時候小皮有疑問了:“C#本身已經自帶了List等容器了,為什麼還要用KPair來構造鏈表?”
“為了讓底層盡可能簡潔。”老明說道,“我們都知道,程序本質上分為數據結構和算法。算法是順着數據結構來實現的。簡潔的數據結構會讓算法的實現顯得更清晰。相比C#自帶的List,使用KPair構造的鏈表更加清晰簡潔。按照構造的方式,我們的鏈表定義為:
null;Lhs,並且Rhs是後續的鏈表。“鏈表相關的算法都會順着定義的這兩個分支實現:一個處理空鏈表的分支,一個處理非空鏈表的遞歸代碼。比如說判斷一個變量是不是鏈表的方法:
public static bool IsList(object o)
{
// 空鏈表
if (o == null)
{
return true;
}
// 非空鏈表
if (o is KPair p)
{
// 遞歸
return IsList(p.Rhs);
}
// 非鏈表
return false;
}
“以及判斷一個元素是不是在鏈表中的方法:
public static bool Memeber(object lst, object e)
{
// 空鏈表
if (lst == null)
{
return false;
}
// 非空鏈表
if (lst is KPair p)
{
if (p.Lhs == null && e == null || p.Lhs.Equals(e))
{
return true;
}
else
{
// 遞歸
return Memeber(p.Rhs, e);
}
}
// 非鏈表
return false;
}
“數據類型明確后,接下來我們來看看NMiniKanren能做什麼。”
編寫NMiniKanren代碼是一個構造目標(Goal類型)的過程。NMiniKanren解釋器運行時將求解使得目標成立的所有未知量的值。
顯然,有兩個平凡的目標:
k.Succeed:永遠成立,未知量可取任意值。k.Fail:永遠不成立,無論未知量為何值都不成立。其中k是KRunner的一個實例。C#跟Java一樣不能定義獨立的函數和常量,所以我們DSL需要的函數和常量就都定義為KRunner的方法或屬性。後面不再對k進行複述。
一個基本的目標是k.Eq(v1, v2)。這也是NMiniKanren唯一一個使用值來構造的目標,它表示值v1和v2應該相等。也就是說,當v1與v2相等時,目標k.Eq(v1, v2)成立;否則不成立。
這裏的相等,指的是值相等:
string類型相等當且僅當值相等。KPair類型相等當且僅當它們的Lhs相等且Rhs相等。從KPair相等的定義,可以推出由KPair構造的數據結構(比如鏈表),相等條件為當且僅當它們結構一樣且對應的值相等。
接下來我們看幾個例子。
KRunner.PrintResult(KRunner.Run(null, (k, q) =>
{
return k.Eq(q, 5);
})); // 輸出[5]
直接q等於5。
KRunner.PrintResult(KRunner.Run(null, (k, q) =>
{
return k.Eq(q, k.List(1, 2));
})); // 輸出[(1 2)]
k.List(1, 2)相當於new KPair(1, new KPair(2, null)),用來快速構造鏈表。
KRunner.PrintResult(KRunner.Run(null, (k, q) =>
{
return k.Eq(k.List(1, q), k.List(1, 2));
})); // 輸出[2]
這個例子比較像一個方程了。q匹配k.List(1, 2)的第二項,也就是2。
KRunner.PrintResult(KRunner.Run(null, (k, q) =>
{
return k.Eq(k.List(2, q), k.List(1, 2));
})); // 輸出[]
由於k.List(2, q)的第一項和k.List(1, 2)的第一項不相等,所以這個目標無法成立,q沒有值。
KRunner.PrintResult(KRunner.Run(null, (k, q) =>
{
return k.Fail;
})); // 輸出[]
目標無法成立,q沒有值。
KRunner.PrintResult(KRunner.Run(null, (k, q) =>
{
return k.Succeed;
})); // 輸出[_0]
目標恆成立,q可取任意值。輸出_0表示一個可取任意值的自由變量。
目標可以看作布爾表達式,因此可以通過“與或非”運算,用簡單的目標構造成複雜的“組合”目標。我們把被用來構造“組合”目標的目標叫做該“組合”目標的子目標。
在前面的例子中,我們只有一個未知量q。q既是未知量,也是程序輸出。
在處理更複雜的問題時,通常需要定義更多的未知量。定義未知量的方法是k.Fresh:
// 定義x, y兩個未知量
var x = k.Fresh()
var y = k.Fresh()
新定義的未知量和q一樣,可以用來構造目標:
// x == 2
k.Eq(x, 2)
// x == y
k.Eq(x, y)
使用“與”運算組合的目標,僅當所有子目標成立時,目標才成立。
使用方法k.All來構造“與”運算組合的目標。
var g = k.All(g1, g2, g3, ...)
當且僅當g1, g2, g3, ……,都成立時,g才成立。
特別的,空子目標的情況,即k.All(),恆成立。
KRunner.PrintResult(KRunner.Run(null, (k, q) =>
{
return k.All(
k.Eq(q, 1),
k.Eq(q, 2));
})); // 輸出[]
KRunner.PrintResult(KRunner.Run(null, (k, q) =>
{
var x = k.Fresh();
var y = k.Fresh();
return k.All(
k.Eq(x, 1),
k.Eq(y, x),
k.Eq(q, k.List(x, y)));
})); // 輸出[(1 1)]
使用“或”運算組合的目標,只要一個子目標成立時,目標就成立。
使用方法k.Any來構造“或”運算組合的目標。
var g = k.Any(g1, g2, g3, ...)
當g1, g2, g3, ……中至少一個成立,g成立。
特別的,空子目標的情況,即k.Any(),恆不成立。
KRunner.PrintResult(KRunner.Run(null, (k, q) =>
{
return k.Any(
k.Eq(q, 5),
k.Eq(q, 6));
})); // 輸出[5, 6]
KRunner.PrintResult(KRunner.Run(null, (k, q) =>
{
var x = k.Fresh();
var y = k.Fresh();
return k.All(
k.Any(k.Eq(x, 5), k.Eq(y, 6)),
k.Eq(q, k.List(x, y)));
})); // 輸出[(5 _0), (_0 6)]
MiniKanren(以及NMiniKanren)不支持“非”運算。支持“非”會讓miniKanren的實現複雜很多。
這或許令人驚訝。“與或非”在邏輯代數中一直像是連體嬰兒似的扎堆出現。並且“非”運算是單目運算符,看起來應該更簡單。
然而,“與”和“或”運算是在已知的兩(多)個集合中取交集或者並集,結果也是已知的。而“非”運算則是把一個已知的集合映射到可能未知的集合,遍歷“非”運算的結果可能會很久或者就是不可能的。
對於基於圖搜索和代入法求解的miniKanren來說,支持“非”運算需要對核心的數據結構和算法做較大改變。因此以教學為目的的miniKanren沒有支持“非”運算。
不過,在一定程度上,也是有不完整替代方法的。
If是一個特殊的構造目標的方式。對應《The Reasoned Schemer》中的conda。
var g = k.If(g1, g2, g3)
如果g1且g2成立,那麼g成立;否則當且僅當g3成立時,g成立。
這個和k.Any(k.All(g1, g2), g3)很像,但他們是有區別的:
k.Any(k.All(g1, g2), g3)會解出所有讓k.All(g1, g2)或者g3成立的解k.If(g1, g2, g3)如果k.All(g1, g2)有解,那麼只給出使k.All(g1, g2)成立的解;否則再求使得g3成立的解。也可以說,If是短路的。
這麼詭異的特性有什麼用呢?
它可以部分地實現“非”運算的功能:
k.If(g, k.Fail, k.Succeed)
這個這裏先不詳細展開了,後面用到再說。
這是一個容易被忽略的問題。如果程序需要求出所有的解,那麼輸出順序影響不大。但是一些情況下,求解速度很慢,或者解的數量太多甚至無窮,這時只求前幾個解,那麼輸出的內容就和輸出順序有關了。
因為miniKanren以圖遍歷的方式來查找問題的解,所以解的順序其實也是解釋器運行時遍歷的順序。先看如下例子:
KRunner.PrintResult(KRunner.Run(null, (k, q) =>
{
var x = k.Fresh();
var y = k.Fresh();
return k.All(
k.Any(k.Eq(x, 1), k.Eq(x, 2)),
k.Any(k.Eq(y, "a"), k.Eq(y, "b")),
k.Eq(q, k.List(x, y)));
})); // 輸出[(1 a), (1 b), (2 a), (2 b)]
有兩個未知變量x和y,x可能的取值為1或2,y可能的取值為a或b。可以看到,程序查找解的順序為:
x值為1
y值為a,q=(1 a)y值為b,q=(1 b)x值為2
y值為a,q=(2 a)y值為b,q=(2 b)如果要改變這個順序,我們有一個交替版的“與”運算k.Alli:
KRunner.PrintResult(KRunner.Run(null, (k, q) =>
{
var x = k.Fresh();
var y = k.Fresh();
return k.Alli(
k.Any(k.Eq(x, 1), k.Eq(x, 2)),
k.Any(k.Eq(y, "a"), k.Eq(y, "b")),
k.Eq(q, k.List(x, y)));
})); // 輸出[(1 a), (2 a), (1 b), (2 b)]
不過這個交替版也不是交替得很漂亮。下面增加x可能的取值到3個:
KRunner.PrintResult(KRunner.Run(null, (k, q) =>
{
var x = k.Fresh();
var y = k.Fresh();
return k.Alli(
k.Any(k.Eq(x, 1), k.Eq(x, 2), k.Eq(x, 3)),
k.Any(k.Eq(y, "a"), k.Eq(y, "b")),
k.Eq(q, k.List(x, y)));
})); // 輸出[(1 a), (2 a), (1 b), (3 a), (2 b), (3 b)]
同樣,“或”運算也有交替版。
正常版:
KRunner.PrintResult(KRunner.Run(null, (k, q) =>
{
return k.Any(
k.Any(k.Eq(q, 1), k.Eq(q, 2)),
k.Any(k.Eq(q, 3), k.Eq(q, 4)));
})); // 輸出[1, 2, 3, 4]
交替版:
KRunner.PrintResult(KRunner.Run(null, (k, q) =>
{
return k.Anyi(
k.Any(k.Eq(q, 1), k.Eq(q, 2)),
k.Any(k.Eq(q, 3), k.Eq(q, 4)));
})); // 輸出[1, 3, 2, 4]
後面講到miniKanren實現原理時會解釋正常版、交替版為什麼會是這種表現。
無遞歸,不編程!
遞歸給予了程序語言無限的可能。NMiniKanren也是支持遞歸的。下面我們實現一個方法,這個方法構造的目標要求指定的值或者未知量是一個所有元素都為1的鏈表。
一個值或者未知量的元素都為1,用遞歸的方式表達是:
直譯為代碼就是:
public static Goal AllOne_Wrong(this KRunner k, object lst)
{
var d = k.Fresh();
return k.Any(
// 空鏈表
k.Eq(lst, null),
// 非空
k.All(
k.Eq(lst, k.Pair(1, d)), // 第一個元素是1
k.AllOne_Wrong(d))); // 剩餘部分的元素都是1
}
直接運行這段代碼,死循環。
為什麼呢?因為我們直接使用C#的方法來定義函數,C#在構造目標的時候,會運行最後一行的k.AllOne_Wrong(d),於是就陷入死循環了。
為了避免死循環,在遞歸調用的地方,需要用k.Recurse方法特殊處理一下,讓遞歸的部分變為惰性求值,防止直接調用:
public static Goal AllOne(this KRunner k, object lst)
{
var d = k.Fresh();
return k.Any(
k.Eq(lst, null),
k.All(
k.Eq(lst, k.Pair(1, d)),
k.Recurse(() => k.AllOne(d))));
}
隨便構造兩個問題運行一下:
KRunner.PrintResult(KRunner.Run(null, (k, q) =>
{
var x = k.Fresh();
var y = k.Fresh();
return k.All(
k.AllOne(k.List(1, x, y, 1)),
k.Eq(q, k.List(x, y)));
})); // 輸出[(1 1)]
KRunner.PrintResult(KRunner.Run(null, (k, q) =>
{
var x = k.Fresh();
var y = k.Fresh();
return k.All(
k.AllOne(k.List(1, x, y, 0)),
k.Eq(q, k.List(x, y)));
})); // 輸出[]
k.Recurse這種處理方法其實是比較醜陋而且不好用的。特別是多個函數相互調用引起遞歸的情況,很可能會漏寫k.Recurse導致死循環。
聽到這裏,小皮疑惑道:“這個有點丑誒。剛剛網上瞄了下《The Reasoned Schemer》,發現人家的遞歸併不需要這種特殊處理。看起來直接調用就OK了,跟普通程序沒啥兩樣,很美很和諧。”
“因為《The Reasoned Schemer》使用Lisp的宏實現的miniKanren,宏的機制會有類似惰性計算的效果。”老明用擦白板的抹布拍了下小皮的腦袋,“可惜你不會Lisp。如果你不努力提升自己,那丑一點也只能將就着看了。”
MiniKanren沒有直接支持數值計算。也就是說,miniKanren不能直接幫你解像2 + x = 5的這種方程。如果要直接支持數值計算,需要實現很多數學相關的運算和變換,會讓miniKanren的實現變得非常複雜。MiniKanren是教學性質的語言,只支持了最基本的邏輯判斷功能。
“沒有‘直接’支持。”小皮敏銳地發現了關鍵,“也就是可以間接支持咯?”
“沒錯!你想想,0和1是我們支持的符號,與和或也是我們支持的運算符!”老明興奮起來了。
“二進制?”
“是的!任何一本計算機組成原理教程都會教你怎麼做!這裏就不多說了,你可以自己回去試一下。”
“嗯嗯。我以前這門課學得還不錯,現在還記得大概是先實現半加器和全加器,然後構造加法器和乘法器等。”小皮幹勁十足,從底層開始讓他想起了小時候玩泥巴的樂趣。
“而且用miniKanren實現的不是一般的加法器和乘法器,是可以反向運行的加法器和乘法器。”
“有意思,晚上下班回去就去試試。”小皮真心地說。正如他下班回家躺床上后,就再也不想動彈一樣真心實意。
(注:《The Reasoned Schemer》第7章、第8章會講到相關內容。)
“好了,NMiniKanren語言的介紹就先說到這裏了。”老明拍了拍手,看了看前面的例子,撇了撇嘴,“以C#的DSL方式實現出來果然丑很多,語法上都不一致了。不過核心功能都還在。”
“接下來就是最有意思的部分,NMiniKanren的原理了吧?”
“是的。不過在繼續之前,還有個問題。”
“啥問題?”
“中午米線都用來打碼了。現在肚子餓了,你要請我吃下午茶。”
NMiniKanren的源碼在:https://github.com/sKabYY/NMiniKanren
示例代碼在:https://github.com/sKabYY/NMiniKanren/tree/master/NMiniKaren.Tests
本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理
【其他文章推薦】
※網頁設計公司推薦不同的風格,搶佔消費者視覺第一線
※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益
※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面
※南投搬家公司費用需注意的眉眉角角,別等搬了再說!
※新北清潔公司,居家、辦公、裝潢細清專業服務
在之前的文章我們介紹了一下 Java 中的 集合框架中的Collection 的子接口 List的 增刪改查和與數組間相互轉換的方法,本章我們來看一下 Java 集合框架中的Collection 的子接口 List 的另外一些方法。
我們在使用集合的時候難免會對其中的元素進行排序,因為 Set 集合本身是無序的,所以本章將着重講解 List 集合,如下:
1 import java.util.ArrayList; 2 import java.util.Collections; 3 import java.util.List; 4 import java.util.Random; 5 6 /** 7 * 排序集合元素 8 * 排序集合使用的是集合的工具類 Collections 的靜態方法 sort 9 * 排序僅能對 List 集合進行,因為 Set 部分實現類是無序的 10 * */ 11 public class Main { 12 public static void main(String[] args) { 13 List<Integer> list = new ArrayList<Integer>(); 14 Random random = new Random(); 15 for(int i=0;i<10;i++){ 16 list.add(random.nextInt(100)); 17 } 18 System.out.println(list); // [49, 24, 29, 59, 56, 1, 1, 5, 49, 60] 19 20 Collections.sort(list); 21 System.out.println(list); // [1, 1, 5, 24, 29, 49, 49, 56, 59, 60] 22 } 23 }
在上面的代碼中,我們隨機生成了一些正數並添加到 List 集合中,我們通過 sort 方法,系統變自動按照自然數的排列方法為我們做好了排序,很是方便,那如果 List 中使我們自己定義的內容,比如說一個類,那該如何排序呢,如下:
import java.util.ArrayList; import java.util.Collections; import java.util.List; /** * 排序集合元素 * 排序集合使用的是集合的工具類 Collections 的靜態方法 sort * 排序僅能對 List 集合進行,因為 Set 部分實現類是無序的 */ public class Main { public static void main(String[] args) { List<Point> list = new ArrayList<Point>(); list.add(new Point(1, 2)); list.add(new Point(2, 3)); list.add(new Point(4, 2)); list.add(new Point(2, 5)); list.add(new Point(9, 3)); list.add(new Point(7, 1)); System.out.println(list); // [(1, 2), (2, 3), (4, 2), (2, 5), (9, 3), (7, 1)] // Collections.sort(list); // 編譯錯誤,不知道 Point 排序規則 /** * sort 方法要求集合必須實現 Comparable 接口 * 該接口用於規定實現類事可以比較的 * 其中一個 compareTo 方法時用來定義比較大小的規則 * */ Collections.sort(list); System.out.println(list); // [(1, 2), (2, 3), (4, 2), (2, 5), (7, 1), (9, 3)] } } class Point implements Comparable<Point> { // 定義為泛型 T 類型 private int x; private int y; public Point(int x, int y) { this.x = x; this.y = y; } public int getX() { return x; } public void setX(int x) { this.x = x; } public int getY() { return y; } public void setY(int y) { this.y = y; } @Override public String toString() { return "(" + x + ", " + y + ")"; } /** * 當實現了 Comparable 接口后,需要重寫下面的方法 * 該方法的作用是定義當前對象與給定參數對象比較大小的規則 * 返回值為一個 int 值,該值表示大小關係 * 它不關注具體的取值是多少,而關注的是取值範圍 * 當返回值 >0 時:當前對象不參數對象大 * 當返回值 <0 時:當前對象比參數對象小 * 當返回值 =0 時:兩個對象相等 */ @Override public int compareTo(Point o) { /** * 比較規則,點到原點的距離長的打 * */ int len = this.x * this.x + this.y * this.y; int olen = o.x * o.x + o.y * o.y; return len - olen; } }
在上面的代碼中,我們跟之前一樣定義了一個 Point 類,然後實例化后存入 list 集合中,如果我們相對 Point 進行排序,如果直接調用 sort 方法會報錯,這是由於編譯器不知道我們定義的 Point 類的排序規則,所以我們需要通過接口 Comparable 接口來自定義排序規則,如下圖:
我們可以通過坐標系中點到原點的距離來判斷 Point 的大小,即 x*x+y*y 的大小,然後通過重寫 compareTo 方法來實現 sort 。
實際上雖然能實現,但是並不理想,為了實現一個 sort 方法,要求我們的集合元素必須實現 Comparable 接口並且定義比較規則,這種我們想使用某個功能,而它要求我們修改程序的現象稱為“侵入性”。修改的代碼越多,侵入性越強,越不利於程序的擴展。
我們再來看一下下面的排序:
1 import java.util.ArrayList; 2 import java.util.Collections; 3 import java.util.List; 4 5 public class Main { 6 public static void main(String[] args) { 7 List<String> list1 = new ArrayList<String>(); 8 list1.add("Java"); 9 list1.add("c++"); 10 list1.add("Python"); 11 list1.add("PHP"); 12 Collections.sort(list1); 13 System.out.println(list1); // [Java, PHP, Python, c++] 14 15 List<String> list2 = new ArrayList<String>(); 16 list2.add("孫悟空"); 17 list2.add("豬八戒"); 18 list2.add("唐僧"); 19 list2.add("六小齡童"); 20 Collections.sort(list2); 21 System.out.println(list2); // [六小齡童, 唐僧, 孫悟空, 豬八戒] 22 23 } 24 }
在上面的排序中,當我們對字母或漢字進行排序時,其實是比的第一個字的 Unicode 碼,那如果我們不使用 Comparable 的接口該如何自定義我們想要的實現規則呢?比如根據字符串的長度來進行排序,其實 sort 有一個額外的重載方法來實現我們想要的結果,如下:
1 import java.util.ArrayList; 2 import java.util.Collections; 3 import java.util.Comparator; 4 import java.util.List; 5 6 public class Main { 7 public static void main(String[] args) { 8 /** 9 * 重載的 sort 方法要求傳入一個額外的比較器 10 * 該方法不再要求集合元素必須實現 Comparable 接口 11 * 並且不再使用集合元素資深的比較規則排序了 12 * 而是根據給定的這個額外的比較器的比較規則對集合元素進行排序 13 * 實際開發中也推薦使用這種方式進行集合元素排序 14 * 若集合元素是自定義的 15 * 創建比較器時推薦使用匿名內部類的形式 16 */ 17 List<String> list1 = new ArrayList<String>(); 18 list1.add("Java"); 19 list1.add("c++"); 20 list1.add("Python"); 21 list1.add("PHP"); 22 MyComparator myComparator1 = new MyComparator(); 23 Collections.sort(list1, myComparator1); // 重載 sort 24 System.out.println(list1); // [c++, PHP, Java, Python] 25 26 List<String> list2 = new ArrayList<String>(); 27 list2.add("孫悟空"); 28 list2.add("豬八戒"); 29 list2.add("唐僧"); 30 list2.add("六小齡童"); 31 // 匿名內部類形式創建 32 Comparator<String> myComparator2 = new Comparator<String>() { 33 @Override 34 public int compare(String o1, String o2) { 35 return o1.length() - o2.length(); 36 } 37 }; 38 Collections.sort(list2, myComparator2); // 重載 sort 39 System.out.println(list2); // [唐僧, 孫悟空, 豬八戒, 六小齡童] 40 41 } 42 } 43 44 /** 45 * 定義一個額外的比較器 46 * 該方法用來定義 o1 和 o2 的比較 47 * 若返回值 >0:o1>o2 48 * 若返回值 <0:o1<o2 49 * 若返回值 =0:兩個對象相等 50 */ 51 class MyComparator implements Comparator<String> { 52 @Override 53 public int compare(String o1, String o2) { 54 /** 55 * 字符串中字符多的大 56 * */ 57 return o1.length() - o2.length(); 58 } 59 }
本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理
【其他文章推薦】
※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面
※網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!
※想知道最厲害的網頁設計公司"嚨底家"!
※幫你省時又省力,新北清潔一流服務好口碑
※別再煩惱如何寫文案,掌握八大原則!