SpringBoot系列之i18n集成教程

目錄

SpringBoot系統之i18n國際化語言集成教程
@

1、環境搭建

本博客介紹一下SpringBoot集成i18n,實現系統語言國際化處理,ok,先創建一個SpringBoot項目,具體的參考我的博客專欄:

環境準備:

  • IntelliJ IDEA
  • Maven

項目集成:

  • Thymeleaf(模板引擎,也可以選jsp或者freemark)
  • SpringBoot2.2.1.RELEASE

2、resource bundle資源配置

ok,要實現國際化語言,先要創建resource bundle文件:
在resources文件夾下面創建一個i18n的文件夾,其中:

  • messages.properties是默認的配置
  • messages_zh_CN.properties是(中文/中國)
  • messages_en_US.properties是(英文/美國)
  • etc.

    IDEA工具就提供了很簡便的自動配置功能,如圖,只要點擊新增按鈕,手動輸入,各配置文件都會自動生成屬性

    messages.properties:

messages.loginBtnName=登錄~
messages.password=密碼~
messages.rememberMe=記住我~
messages.tip=請登錄~
messages.username=用戶名~

messages_zh_CN.properties:

messages.loginBtnName=登錄
messages.password=密碼
messages.rememberMe=記住我
messages.tip=請登錄
messages.username=用戶名

messages_en_US.properties:

messages.loginBtnName=login
messages.password=password
messages.rememberMe=Remember me
messages.tip=Please login in
messages.username=userName

在項目的application.properties修改默認配置,讓SpringBoot的自動配置能讀取到resource bundle資源文件

## 配置i18n
# 默認是i18n(中文/中國)
spring.mvc.locale=zh_CN
# 配置resource bundle資源文件的前綴名eg:i18n是文件夾名,messages是資源文件名,支持的符號有.號或者/
spring.messages.basename=i18n.messages
# 設置緩存時間,2.2.1是s為單位,之前版本才是毫秒
spring.messages.cache-duration=1
# 設置資源文件編碼格式為utf8
spring.messages.encoding=utf-8

注意要點:

  • spring.messages.basename必須配置,否則SpringBoot的自動配置將失效
    MessageSourceAutoConfiguration.ResourceBundleCondition 源碼:
protected static class ResourceBundleCondition extends SpringBootCondition {
        //定義一個map緩存池
        private static ConcurrentReferenceHashMap<String, ConditionOutcome> cache = new ConcurrentReferenceHashMap<>();

        @Override
        public ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeMetadata metadata) {
            String basename = context.getEnvironment().getProperty("spring.messages.basename", "messages");
            ConditionOutcome outcome = cache.get(basename);//緩存拿得到,直接從緩存池讀取
            if (outcome == null) {//緩存拿不到,重新讀取
                outcome = getMatchOutcomeForBasename(context, basename);
                cache.put(basename, outcome);
            }
            return outcome;
        }

        private ConditionOutcome getMatchOutcomeForBasename(ConditionContext context, String basename) {
            ConditionMessage.Builder message = ConditionMessage.forCondition("ResourceBundle");
            for (String name : StringUtils.commaDelimitedListToStringArray(StringUtils.trimAllWhitespace(basename))) {
                for (Resource resource : getResources(context.getClassLoader(), name)) {
                    if (resource.exists()) {
                    //匹配resource bundle資源
                        return ConditionOutcome.match(message.found("bundle").items(resource));
                    }
                }
            }
            return ConditionOutcome.noMatch(message.didNotFind("bundle with basename " + basename).atAll());
        }
        //解析資源文件
        private Resource[] getResources(ClassLoader classLoader, String name) {
            String target = name.replace('.', '/');//spring.messages.basename參數值的點號換成斜桿
            try {
                return new PathMatchingResourcePatternResolver(classLoader)
                        .getResources("classpath*:" + target + ".properties");
            }
            catch (Exception ex) {
                return NO_RESOURCES;
            }
        }

    }
  • cache-duration在2.2.1版本,指定的是s為單位,找到SpringBoot的MessageSourceAutoConfiguration自動配置類

3、LocaleResolver類

SpringBoot默認採用AcceptHeaderLocaleResolver類作為默認LocaleResolver,LocaleResolver類的作用就是作為i18n的分析器,獲取對應的i18n配置,當然也可以自定義LocaleResolver類


import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.lang.Nullable;
import org.springframework.util.StringUtils;
import org.springframework.web.servlet.LocaleResolver;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Locale;

/**
 * <pre>
 *  自定義LocaleResolver類
 * </pre>
 * @author nicky
 * <pre>
 * 修改記錄
 *    修改后版本:     修改人:  修改日期: 2019年11月23日  修改內容:
 * </pre>
 */
public class CustomLocalResolver implements LocaleResolver {

    Logger LOG = LoggerFactory.getLogger(this.getClass());

    @Nullable
    private Locale defaultLocale;

    public void setDefaultLocale(@Nullable Locale defaultLocale) {
        this.defaultLocale = defaultLocale;
    }

    @Nullable
    public Locale getDefaultLocale() {
        return this.defaultLocale;
    }

    @Override
    public Locale resolveLocale(HttpServletRequest request) {
        Locale defaultLocale = this.getDefaultLocale();//獲取application.properties默認的配置
        if(defaultLocale != null && request.getHeader("Accept-Language") == null) {
            return defaultLocale;//http請求頭沒獲取到Accept-Language才採用默認配置
        } else {//request.getHeader("Accept-Language")獲取得到的情況
            Locale requestLocale = request.getLocale();//獲取request.getHeader("Accept-Language")的值
            String localeFlag = request.getParameter("locale");//從URL獲取的locale值
            //LOG.info("localeFlag:{}",localeFlag);
            //url鏈接有傳locale參數的情況,eg:zh_CN
            if (!StringUtils.isEmpty(localeFlag)) {
                String[] split = localeFlag.split("_");
                requestLocale = new Locale(split[0], split[1]);
            }
            //沒傳的情況,默認返回request.getHeader("Accept-Language")的值
            return requestLocale;
        }
    }

    @Override
    public void setLocale(HttpServletRequest request, HttpServletResponse response, Locale locale) {

    }
}

4、I18n配置類

I18n還是要繼承WebMvcConfigurer,注意,2.2.1版本才是實現接口就可以,之前1.+版本是要實現WebMvcConfigurerAdapter適配器類的

import com.example.springboot.i18n.component.CustomLocalResolver;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.web.servlet.WebMvcProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.LocaleResolver;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import org.springframework.web.servlet.i18n.LocaleChangeInterceptor;

/**
 * <pre>
 *  I18nConfig配置類
 * </pre>
 * <p>
 * <pre>
 * @author nicky.ma
 * 修改記錄
 *    修改后版本:     修改人:  修改日期: 2019/11/24 11:15  修改內容:
 * </pre>
 */
 //Configuration必須加上,不然不能加載到Spring容器
@Configuration
//使WebMvcProperties配置類可用,這個可以不加上,本博客例子才用
@EnableConfigurationProperties({ WebMvcProperties.class})
public class I18nConfig implements WebMvcConfigurer{
    
    //裝載WebMvcProperties 屬性
    @Autowired
    WebMvcProperties webMvcProperties;
    /**
     * 定義SessionLocaleResolver
     * @Author nicky.ma
     * @Date 2019/11/24 13:52
     * @return org.springframework.web.servlet.LocaleResolver
     */
//    @Bean
//    public LocaleResolver localeResolver() {
//        SessionLocaleResolver sessionLocaleResolver = new SessionLocaleResolver();
//        // set default locale
//        sessionLocaleResolver.setDefaultLocale(Locale.US);
//        return sessionLocaleResolver;
//    }

    /**
     * 定義CookieLocaleResolver
     * @Author nicky.ma
     * @Date 2019/11/24 13:51
     * @return org.springframework.web.servlet.LocaleResolver
     */
//    @Bean
//    public LocaleResolver localeResolver() {
//        CookieLocaleResolver cookieLocaleResolver = new CookieLocaleResolver();
//        cookieLocaleResolver.setCookieName("Language");
//        cookieLocaleResolver.setCookieMaxAge(1000);
//        return cookieLocaleResolver;
//    }

    /**
     * 自定義LocalResolver
     * @Author nicky.ma
     * @Date 2019/11/24 13:45
     * @return org.springframework.web.servlet.LocaleResolver
     */
    @Bean
    public LocaleResolver localeResolver(){
        CustomLocalResolver localResolver = new CustomLocalResolver();
        localResolver.setDefaultLocale(webMvcProperties.getLocale());
        return localResolver;
    }

    /**
     * 定義localeChangeInterceptor
     * @Author nicky.ma
     * @Date 2019/11/24 13:45
     * @return org.springframework.web.servlet.i18n.LocaleChangeInterceptor
     */
    @Bean
    public LocaleChangeInterceptor localeChangeInterceptor(){
        LocaleChangeInterceptor localeChangeInterceptor = new LocaleChangeInterceptor();
        //默認的請求參數為locale,eg: login?locale=zh_CN
        localeChangeInterceptor.setParamName(LocaleChangeInterceptor.DEFAULT_PARAM_NAME);
        return localeChangeInterceptor;
    }

    /**
     * 註冊攔截器
     * @Author nicky.ma
     * @Date 2019/11/24 13:47
     * @Param [registry]
     * @return void
     */
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
     registry.addInterceptor(localeChangeInterceptor()).addPathPatterns("/**");
    }
}

注意要點:

  • 舊版代碼可以不加LocaleChangeInterceptor 攔截器,2.2.1版本必須通過攔截器
  • 如下代碼,bean的方法名必須為localeResolver,否則會報錯
@Bean
    public LocaleResolver localeResolver(){
        CustomLocalResolver localResolver = new CustomLocalResolver();
        localResolver.setDefaultLocale(webMvcProperties.getLocale());
        return localResolver;
    }

原理:
跟一下源碼,點進LocaleChangeInterceptor類

DispatcherServlet是Spring一個很重要的分發器類,在DispatcherServlet的一個init方法里找到這個LocaleResolver的init方法

這個IOC獲取的bean類名固定為localeResolver,寫例子的時候,我就因為改了bean類名,導致一直報錯,跟了源碼才知道Bean類名要固定為localeResolver

拋異常的時候,也是會獲取默認的LocaleResolver的

找到資源文件,確認,還是默認為AcceptHeaderLocaleResolver

配置了locale屬性的時候,還是選用AcceptHeaderLocaleResolver作為默認的LocaleResolver

spring.mvc.locale=zh_CN

WebMvcAutoConfiguration.localeResolver方法源碼,ConditionalOnMissingBean主鍵的意思是LocaleResolver沒有自定義的時候,才作用,ConditionalOnProperty的意思,有配了屬性才走這裏的邏輯

  • 攔截器攔截的請求參數默認為locale,要使用其它參數,必須通過攔截器設置 ,eg:localeChangeInterceptor.setParamName("lang");
  • LocalResolver種類有:CookieLocaleResolver(Cookie)、SessionLocaleResolver(會話)、FixedLocaleResolver、AcceptHeaderLocaleResolver(默認)、.etc

5、Thymeleaf集成

本博客的模板引擎採用Thymeleaf的,所以新增項目時候就要加上maven相關依賴,沒有的話,自己加上:

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

ok,然後去找個bootstrap的登錄頁面,本博客已尚硅谷老師的例子為例,進行拓展,引入靜態資源文件:

Thymeleaf的i18n支持是採用#符號的

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
    <head>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
        <meta name="description" content="">
        <meta name="author" content="">
        <title>SpringBoot i18n example</title>
        <!-- Bootstrap core CSS -->
        <link href="asserts/css/bootstrap.min.css" th:href="@{asserts/css/bootstrap.min.css}" rel="stylesheet">
        <!-- Custom styles for this template -->
        <link href="asserts/css/signin.css" th:href="@{asserts/css/signin.css}" rel="stylesheet">
    </head>

    <body class="text-center">
        <form class="form-signin" action="dashboard.html">
            <img class="mb-4" th:src="@{asserts/img/bootstrap-solid.svg}" alt="" width="72" height="72">
            <h1 class="h3 mb-3 font-weight-normal" th:text="#{messages.tip}">Please sign in</h1>
            <label class="sr-only" th:text="#{messages.username}">Username</label>
            <input type="text" class="form-control" th:placeholder="#{messages.username}" required="" autofocus="">
            <label class="sr-only" th:text="#{messages.password} ">Password</label>
            <input type="password" class="form-control" th:placeholder="#{messages.password}" required="">
            <div class="checkbox mb-3">
                <label>
          <input type="checkbox" value="remember-me" > [[#{messages.rememberMe}]]
        </label>
            </div>
            <button class="btn btn-lg btn-primary btn-block" type="submit" th:text="#{messages.loginBtnName}">Sign in</button>
            <p class="mt-5 mb-3 text-muted">© 2019</p>
            <a class="btn btn-sm" th:href="@{/login(locale='zh_CN')} ">中文</a>
            <a class="btn btn-sm" th:href="@{/login(locale='en_US')} ">English</a>
        </form>

    </body>

</html>

切換中文網頁:

切換英文網頁:

當然不點鏈接傳locale的方式也是可以自動切換的,瀏覽器設置語言:

原理localeResolver類會獲取Accept language參數

附錄:
logging manual:
example source:

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理【其他文章推薦】

網頁設計公司推薦更多不同的設計風格,搶佔消費者視覺第一線

※廣告預算用在刀口上,網站設計公司幫您達到更多曝光效益

※自行創業 缺乏曝光? 下一步"網站設計"幫您第一時間規劃公司的門面形象

程序員一般通過什麼途徑接私活?

二哥,你好,我想知道一般程序猿都如何接私活,我也想接,能告訴我一些方法嗎?

上面是一個讀者“煩不煩”問我的一個問題。其實不止是“煩不煩”,還有很多讀者問過我類似這樣的問題。

我接的私活不算多,掙到的錢也沒有多少,加起來不到 20W。說實話,這個數目說出來我是有點心虛的,畢竟太少了,大家輕噴。但我想,恰好配得上“一般程序員”這個稱號啊。畢竟蒼蠅再小也是肉,我也算是有經驗的人了。

唾棄接私活、做外包的程序員有很多很多,曾經高傲的我也嫌棄過。但沒辦法,為了掙點零花錢,我垂下了高昂的頭。記得有位朋友曾說過,當年沈從文為了生計,寫了很多稱不上他自己喜歡的文字給報刊。

聽朋友這麼一說,我也不再覺得“接私活”是多麼一件值得羞愧的事情了。人首先要活着,才有體力講情懷啊。好了,言歸正傳,我來替“煩不煩”同學介紹幾個容易上手的操作。

01、朋友介紹

大體上,天底下做生意都只有一條捷徑:從熟人下手。

“哥們,聽說你有個朋友是做程序員的,我這有台電腦不知道為啥黑屏了,能問問他知道什麼原因嗎?要是能修好,保准請你吃頓大餐。”

“老弟啊,我有一個朋友說最近流行炒鞋,我想你不是程序員嘛,找你最合適了,要不我把他推薦給你,談成的話給我發個紅包就行了。”

我的第一個私活,就是之前在蘇州的一個同事介紹的。不過最後黃了。我搞了兩周時間(技術框架用的 JEPF),同事說甲方換方案了,沒把我氣壞。

同事礙於情面,說有機會請我吃頓飯。這一等就是 3 年,3 年過去了,飯還是沒有吃到。主要是因為我這位朋友在蘇州,我在洛陽,吃飯是沒辦法遠程完成啊。

第二個私活,是之前在蘇州的一個領導介紹的。由同事升級為領導,多少靠譜了點。這次做的是蘇州相城區的一個电子商務網站。前後做了三個多月,最後拿到手的錢也就不到一萬塊錢。

現在感覺自己當時是在出售廉價勞動力,何止是廉價,簡直是公益事業。不過,第一次接私活,拿到錢買了個華為的 MateBook,真香。

第二個私活做完后,領導可能覺得虧待了我,良心難安,就介紹了第三個私活給我。這次蠻輕鬆的,一個月搞定,還不累,兩萬塊到手。

既然是私活,當然都是利用業餘時間做的。這個投入的成本和實際得到的回報是一定要考慮的

我第一個私活打了水漂,辛苦了兩周,零回報。不過,這也是接私活常有的事,需要用平常心來對待。

第二個私活說實話非常辛苦,有幾次熬到半夜兩三點,當時覺得太不划算了。但當初自己接了,就只能忍着拼到底。畢竟咱是敬業愛崗的好同志。

第三個私活就相對輕鬆多了,單位時間內的收益非常高,算下來一個小時有 500 的工時費吧,就彷彿是對前兩個的補償。

總結一下,朋友介紹的項目相對來說還是比較靠譜的,前提條件是要有一定的“人情世故”原始成本積累。如果我當時在蘇州表現得不夠優異,和同事、領導的關係相處的不夠融洽,那自然他們也不會時隔多年後再找到我。

記住一點,做事的同時要好好的做人。當你既有能力,又值得信任的時候,私活就會找上門來

02、個人品牌

既然是朋友,自然就不會有很多。也就意味着,單純依賴朋友介紹的私活來源是有限度的。那如果想接更多的私活,該怎麼辦呢?

這就需要個人品牌了。

我平常不是喜歡寫作嘛,分享了很多技術文章在各大平台上,瀏覽量還算不錯。博客園上的排名和瀏覽量都能拿得出手。

博客地址:

當你做了一件事,並且一直在堅持,況且還做出了一定的成績,自然就會有生意主動找上門來——花香蜂自來嘛

寫博客的好處有很多,比如說吸引一批忠實的讀者,他們追隨你的文字,喜歡你的風格;再比如說勾引一些出版社,他們欣賞你的文字,願意合作互利共贏。

最後,還會有一些做私活的甲方。以前,我總覺得這是不可能發生的事情,他們是怎麼找到我的?很不可思議,但互聯網就是這麼神奇,你覺得不可能,它卻悄悄地發生着。

第一個通過這個途徑找到我的甲方,姓康。康哥找到我后,一上來就對我一頓吹捧(甭管是真是假)。信任建立起來后,他就說自己在醞釀一個很牛逼的項目,看我有沒有意向一起做。

然後呢,承諾項目成功后,再給我一定數額的獎勵金,並且寫到了合同里。吃完他這個大餅,我很飽,忍不住打了好幾個嗝。

再然後,我們就開始整理需求,然後我出報價,他再砍價;他再提需求,我再加價。最後呢,項目總款談到 7.5 萬,兩個多月的工期。合同的細節也敲定的差不多了。

結果,黃了。和我合夥的一個開發人員小何覺得甲方新提的需求需要再追加 600 塊,甲方覺得這點錢擱不住再追加了。總之呢,7.5W 的項目就因為這個細節黃了,很遺憾。

第二個通過這個途徑找到我的甲方,叫鵬哥。開發一個網站,總價一萬多,吃了上次的虧后,我自己就不想參与了,就找了一個讀者(小李)做。

結果這個項目爛尾了。小李交付的產物我自己都覺得不好意思,bug 非常多。在我看來,既然項目的訂金已經收了,作為開發人員,至少應該交付一個說得過去的產物——負責任吧。

很遺憾,個人品牌招攬來的前兩個私活最後都搞砸了。這裡有必要總結一下:作為程序員,既然打定主意要接私活,那麼接到的時候一定要珍惜。如果一開始覺得價錢低,就趁早拒絕,免得因為需求變動等等原因砸了招牌

當然了,通過這個途徑也做成了四單,每單的價格差不多兩萬。這裏就不再詳談了。

個人品牌的確可以引流來更多的私活,但與此同時,也會浪費很多時間。

像這種泛泛之談的意向客戶有很多。話說,我啥時候變成“社會王”了,我特么是正兒八經的“王老師”好不好?

03、外包平台

外包平台有很多,我就不再一一列舉了。只說幾個我認為還不錯的平台,也不打算細說,免得有些讀者“誇我”良苦用心地在打廣告。

04、一點忠告

在我寫這篇文章的時候,突然收到朋友的一條信息,說她們公司剛剛辭退了一位員工,還通報批評了,就因為接私活被舉報了——她們公司一般不辭退員工,這下子相當於鐵飯碗丟了。

所以說呢,接私活是有風險的。並且在我看來,如果主業沒有遇到瓶頸,強烈不建議接私活。就好比一個小孩子走路還不會,就要求他要跑起來。

時間對於一個程序員來說很寶貴,尤其是一個正在成長中的程序員。

如果你確實急用錢,價格又合適,那就去做。如果不怎麼缺錢,我再強調一次,別去接私活。私活的錢不好掙是一個方面,更重要的是如果你把做私活的時間花在提升自己上,產生的價值就要大得多。等你提升了自己,提升了固定薪水,遠比拿的這點私活的錢划算。千萬不要“撿了芝麻丟了西瓜”。

如果你像我,主業上遇到了瓶頸,平時的時間比較充分,想有一些額外的收入,同時為了保持技術的熟練度,這種情況下,是可以考慮接一些私活的。對於那種投入時間巨大,回報很可憐的項目,千萬不要接!

另外呢,如果甲方只提供幾個簡單的想法,甚至幾張圖片,更或者發一個參照的效果網站,就可以直接忽視了,這類通通不靠譜!

最後呢,還要說一句,如果訂金都收了,自己就算是覺得吃了虧,也應該有點職業素質,把像樣的產品交付,千萬別應付。

謝謝大家的閱讀,原創不易,喜歡就隨手點個贊,這將是我最強的寫作動力。

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理【其他文章推薦】

※如何讓商品強力曝光呢? 網頁設計公司幫您建置最吸引人的網站,提高曝光率!!

網頁設計一頭霧水??該從何著手呢? 找到專業技術的網頁設計公司,幫您輕鬆架站!

※想知道最厲害的台北網頁設計公司推薦台中網頁設計公司推薦專業設計師”嚨底家”!!

three.js使用gpu選取物體並計算交點位置

光線投射法

使用three.js自帶的光線投射器(Raycaster)選取物體非常簡單,代碼如下所示:

var raycaster = new THREE.Raycaster();
var mouse = new THREE.Vector2();

function onMouseMove(event) {
    // 計算鼠標所在位置的設備坐標
    // 三個坐標分量都是-1到1
    mouse.x = event.clientX / window.innerWidth * 2 - 1;
    mouse.y = - (event.clientY / window.innerHeight) * 2 + 1;
}

function pick() {
    // 使用相機和鼠標位置更新選取光線
    raycaster.setFromCamera(mouse, camera);

    // 計算與選取光線相交的物體
    var intersects = raycaster.intersectObjects(scene.children);
}

它是採用包圍盒過濾,計算投射光線與每個三角面元是否相交實現的。

但是,當模型非常大,比如說有40萬個面,通過遍歷的方法選取物體和計算碰撞點位置將非常慢,用戶體驗不好。

但是使用gpu選取物體不存在這個問題。無論場景和模型有多大,都可以在一幀內獲取到鼠標所在點的物體和交點的位置。

使用GPU選取物體

實現方法很簡單:

1.  創建選取材質,將場景中的每個模型的材質替換成不同的顏色。

2. 讀取鼠標位置像素顏色,根據顏色判斷鼠標位置的物體。

具體實現代碼:

1. 創建選取材質,遍歷場景,將場景中每個模型替換為不同的顏色。

let maxHexColor = 1;

// 更換選取材質
scene.traverseVisible(n => {
    if (!(n instanceof THREE.Mesh)) {
        return;
    }
    n.oldMaterial = n.material;
    if (n.pickMaterial) { // 已經創建過選取材質了
        n.material = n.pickMaterial;
        return;
    }
    let material = new THREE.ShaderMaterial({
        vertexShader: PickVertexShader,
        fragmentShader: PickFragmentShader,
        uniforms: {
            pickColor: {
                value: new THREE.Color(maxHexColor)
            }
        }
    });
    n.pickColor = maxHexColor;
    maxHexColor++;
    n.material = n.pickMaterial = material;
});

 

PickVertexShader:

void main() {
    gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}

 

PickFragmentShader:

uniform vec3 pickColor;

void main() {
    gl_FragColor = vec4(pickColor, 1.0);
}

 

2.  將場景繪製在WebGLRenderTarget上,讀取鼠標所在位置的顏色,判斷選取的物體。

let renderTarget = new THREE.WebGLRenderTarget(width, height);
let pixel = new Uint8Array(4);

// 繪製並讀取像素
renderer.setRenderTarget(renderTarget);
renderer.clear();
renderer.render(scene, camera);
renderer.readRenderTargetPixels(renderTarget, offsetX, height - offsetY, 1, 1, pixel); // 讀取鼠標所在位置顏色

// 還原原來材質,並獲取選中物體
const currentColor = pixel[0] * 0xffff + pixel[1] * 0xff + pixel[2];

let selected = null;

scene.traverseVisible(n => {
    if (!(n instanceof THREE.Mesh)) {
        return;
    }
    if (n.pickMaterial && n.pickColor === currentColor) { // 顏色相同
        selected = n; // 鼠標所在位置的物體
    }
    if (n.oldMaterial) {
        n.material = n.oldMaterial;
        delete n.oldMaterial;
    }
});

說明:offsetX和offsetY是鼠標位置,height是畫布高度。readRenderTargetPixels一行的含義是選取鼠標所在位置(offsetX, height – offsetY),寬度為1,高度為1的像素的顏色。

pixel是Uint8Array(4),分別保存rgba顏色的四個通道,每個通道取值範圍是0~255。

完整實現代碼:

使用GPU獲取交點位置

實現方法也很簡單:

1. 創建深度着色器材質,將場景深度渲染到WebGLRenderTarget上。

2. 計算鼠標所在位置的深度,根據鼠標位置和深度計算交點位置。

具體實現代碼:

1. 創建深度着色器材質,將深度信息以一定的方式編碼,渲染到WebGLRenderTarget上。

深度材質:

const depthMaterial = new THREE.ShaderMaterial({
    vertexShader: DepthVertexShader,
    fragmentShader: DepthFragmentShader,
    uniforms: {
        far: {
            value: camera.far
        }
    }
});

DepthVertexShader:

precision highp float;

uniform float far;

varying float depth;

void main() {
    gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
    depth = gl_Position.z / far;
}

DepthFragmentShader:

precision highp float;

varying float depth;

void main() {
    float hex = abs(depth) * 16777215.0; // 0xffffff

    float r = floor(hex / 65535.0);
    float g = floor((hex - r * 65535.0) / 255.0);
    float b = floor(hex - r * 65535.0 - g * 255.0);
    float a = sign(depth) >= 0.0 ? 1.0 : 0.0; // depth大於等於0,為1.0;小於0,為0.0。

    gl_FragColor = vec4(r / 255.0, g / 255.0, b / 255.0, a);
}

重要說明:

a. gl_Position.z是相機空間中的深度,是線性的,範圍從cameraNear到cameraFar。可以直接使用着色器varying變量進行插值。

b. gl_Position.z / far的原因是,將值轉換到0~1範圍內,便於作為顏色輸出。

c. 不能使用屏幕空間中的深度,透視投影后,深度變為-1~1,大部分非常接近1(0.9多),不是線性的,幾乎不變,輸出的顏色幾乎不變,非常不準確。

d. 在片元着色器中獲取深度方法:相機空間深度為
gl_FragCoord.z,屏幕空間深度為
gl_FragCoord.z /  gl_FragCoord.w

e. 上述描述都是針對透視投影,正投影中gl_Position.w為1,使用相機空間和屏幕空間深度都是一樣的。

f. 為了盡可能準確輸出深度,採用rgb三個分量輸出深度。gl_Position.z/far範圍在0~1,乘以0xffffff,轉換為一個rgb顏色值,r分量1表示65535,g分量1表示255,b分量1表示1。

 

完整實現代碼:

 

2. 讀取鼠標所在位置的顏色,將讀取到的顏色值還原為相機空間深度值。

a. 將“加密”處理后的深度繪製在WebGLRenderTarget上。讀取顏色方法

let renderTarget = new THREE.WebGLRenderTarget(width, height);
let pixel = new Uint8Array(4);

scene.overrideMaterial = this.depthMaterial;

renderer.setRenderTarget(renderTarget);

renderer.clear();
renderer.render(scene, camera);
renderer.readRenderTargetPixels(renderTarget, offsetX, height - offsetY, 1, 1, pixel);

說明:offsetX和offsetY是鼠標位置,height是畫布高度。readRenderTargetPixels一行的含義是選取鼠標所在位置(offsetX, height – offsetY),寬度為1,高度為1的像素的顏色。

pixel是Uint8Array(4),分別保存rgba顏色的四個通道,每個通道取值範圍是0~255。

 

b. 將“加密”后的相機空間深度值“解密”,得到正確的相機空間深度值。

if (pixel[2] !== 0 || pixel[1] !== 0 || pixel[0] !== 0) {
    let hex = (this.pixel[0] * 65535 + this.pixel[1] * 255 + this.pixel[2]) / 0xffffff;

    if (this.pixel[3] === 0) {
        hex = -hex;
    }

    cameraDepth = -hex * camera.far; // 相機坐標系中鼠標所在點的深度(注意:相機坐標系中的深度值為負值)
}

 

3. 根據鼠標在屏幕上的位置和相機空間深度,插值反算交點世界坐標系中的坐標。

let nearPosition = new THREE.Vector3(); // 鼠標屏幕位置在near處的相機坐標系中的坐標
let farPosition = new THREE.Vector3(); // 鼠標屏幕位置在far處的相機坐標系中的坐標
let world = new THREE.Vector3(); // 通過插值計算世界坐標

// 設備坐標
const deviceX = this.offsetX / width * 2 - 1;
const deviceY = - this.offsetY / height * 2 + 1;

// 近點
nearPosition.set(deviceX, deviceY, 1); // 屏幕坐標系:(0, 0, 1)
nearPosition.applyMatrix4(camera.projectionMatrixInverse); // 相機坐標系:(0, 0, -far)

// 遠點
farPosition.set(deviceX, deviceY, -1); // 屏幕坐標系:(0, 0, -1)
farPosition.applyMatrix4(camera.projectionMatrixInverse); // 相機坐標系:(0, 0, -near)

// 在相機空間,根據深度,按比例計算出相機空間x和y值。
const t = (cameraDepth - nearPosition.z) / (farPosition.z - nearPosition.z);

// 將交點從相機空間中的坐標,轉換到世界坐標系坐標。
world.set(
    nearPosition.x + (farPosition.x - nearPosition.x) * t,
    nearPosition.y + (farPosition.y - nearPosition.y) * t,
    cameraDepth
);
world.applyMatrix4(camera.matrixWorld);

 

完整代碼:

相關應用

使用gpu選取物體並計算交點位置,多用於需要性能非常高的情況。例如:

1. 鼠標移動到三維模型上的hover效果。

2. 添加模型時,模型隨着鼠標移動,實時預覽模型放到場景中的效果。

3. 距離測量、面積測量等工具,線條和多邊形隨着鼠標在平面上移動,實時預覽效果,並計算長度和面積。

4. 場景和模型非常大,光線投射法選取速度很慢,用戶體驗非常不好。

這裏給一個使用gpu選取物體和實現鼠標hover效果的圖片。紅色邊框是選取效果,黃色半透明效果是鼠標hover效果。

 

 

 

看不明白?可能你不太熟悉three.js中的各種投影運算。下面給出three.js中的投影運算公式。

 

three.js中的投影運算

1. modelViewMatrix = camera.matrixWorldInverse * object.matrixWorld

2. viewMatrix = camera.matrixWorldInverse

3. modelMatrix = object.matrixWorld

4. project = applyMatrix4( camera.matrixWorldInverse ).applyMatrix4( camera.projectionMatrix )

5. unproject = applyMatrix4( camera.projectionMatrixInverse ).applyMatrix4( camera.matrixWorld )

6. gl_Position = projectionMatrix * modelViewMatrix * position
                      = projectionMatrix * camera.matrixWorldInverse * matrixWorld * position
                      = projectionMatrix * viewMatrix * modelMatrix * position

 

參考資料:

1. 完整實現代碼:

2. 基於three.js的開源三維場景編輯器:

3. OpenGL中使用着色器繪製深度值:https://stackoverflow.com/questions/6408851/draw-the-depth-value-in-opengl-using-shaders

4. 在glsl中,獲取真實的片元着色器深度值:https://gamedev.stackexchange.com/questions/93055/getting-the-real-fragment-depth-in-glsl

 

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理【其他文章推薦】

※帶您來了解什麼是 USB CONNECTOR  ?

※自行創業 缺乏曝光? 下一步"網站設計"幫您第一時間規劃公司的門面形象

※如何讓商品強力曝光呢? 網頁設計公司幫您建置最吸引人的網站,提高曝光率!!

※綠能、環保無空污,成為電動車最新代名詞,目前市場使用率逐漸普及化

※廣告預算用在刀口上,網站設計公司幫您達到更多曝光效益

SpringSecurity退出功能實現的正確方式

本文將介紹在Spring Security框架下如何實現用戶的”退出”logout的功能。其實這是一個非常簡單的功能,我見過很多的程序員在使用了Spring Security之後,仍然去自己寫controller方法實現logout功能,這種做法就好像耕地,你有机械設備你不用,你非要用牛。

一、logout最簡及最佳實踐

其實使用Spring Security進行logout非常簡單,只需要在spring Security配置類配置項上加上這樣一行代碼:http.logout()。關於spring Security配置類的其他很多實現、如:HttpBasic模式、formLogin模式、自定義登錄驗證結果、使用權限表達式、session會話管理,在本號的之前的文章已經都寫過了。本節的核心內容就是在原有配置的基礎上,加上這樣一行代碼:http.logout()。

@Configuration
@EnableWebSecurity
public class SecSecurityConfig extends WebSecurityConfigurerAdapter {
 
    @Override
    protected void configure(final HttpSecurity http) throws Exception {
        http.logout();
   }

}

加上logout配置之後,在你的“退出”按鈕上使用/logtou作為請求登出的路徑。

<a href="/logout" >退出</a>

logout功能我們就完成了。實際上的核心代碼只有兩行。

二、默認的logout做了什麼?

雖然我們簡簡單單的實現了logout功能,是不是還不足夠放心?我們下面就來看一下Spring Security默認在logout過程中幫我們做了哪些動作。

  • 當前session失效,即:logout的核心需求,session失效就是訪問權限的回收。
  • 刪除當前用戶的 remember-me“記住我”功能信息
  • clear清除當前的 SecurityContext
  • 重定向到登錄頁面,loginPage配置項指定的頁面

通常對於一個應用來講,以上動作就是logout功能所需要具備的功能了。

三、個性化配置

雖然Spring Security默認使用了/logout作為退出處理請求路徑,登錄頁面作為退出之後的跳轉頁面。這符合絕大多數的應用的開發邏輯,但有的時候我們需要一些個性化設置,如下:

 http.logout()
     .logoutUrl("/signout")
     .logoutSuccessUrl("/aftersignout.html")
     .deleteCookies("JSESSIONID")
  • 通過指定logoutUrl配置改變退出請求的默認路徑,當然html退出按鈕的請求url也要修改
  • 通過指定logoutSuccessUrl配置,來顯式指定退出之後的跳轉頁面
  • 還可以使用deleteCookies刪除指定的cookie,參數為cookie的名稱

四、LogoutSuccessHandler

如果上面的個性化配置,仍然滿足不了您的應用需求。可能您的應用需要在logout的時候,做一些特殊動作,比如登錄時長計算,清理業務相關的數據等等。你可以通過實現LogoutSuccessHandler 接口來實現你的業務邏輯。

@Component
public class MyLogoutSuccessHandler implements LogoutSuccessHandler {
    
    @Override
    public void onLogoutSuccess(HttpServletRequest request, 
                                HttpServletResponse response, 
                                Authentication authentication) 
                                throws IOException, ServletException {
        //這裏書寫你自己的退出業務邏輯
        
        // 重定向到登錄頁
        response.sendRedirect("/login.html");
    }
}

然後進行配置使其生效,核心代碼就是一行logoutSuccessHandler。注意logoutSuccessUrl不要與logoutSuccessHandler一起使用,否則logoutSuccessHandler將失效。

@Configuration
@EnableWebSecurity
public class SecSecurityConfig extends WebSecurityConfigurerAdapter {
    
@Autowired
    private MyLogoutSuccessHandler myLogoutSuccessHandler;

    @Override
    protected void configure(final HttpSecurity http) throws Exception {
         http.logout()
             .logoutUrl("/signout")
             //.logoutSuccessUrl(``"/aftersignout.html"``)
             .deleteCookies("JSESSIONID")
              //自定義logoutSuccessHandler
             .logoutSuccessHandler(myLogoutSuccessHandler);   
   }
}

期待您的關注

  • 博主最近新寫了一本書:
  • 本文轉載註明出處(必須帶連接,不能只轉文字):。

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理【其他文章推薦】

※為什麼 USB CONNECTOR 是電子產業重要的元件?

網頁設計一頭霧水??該從何著手呢? 找到專業技術的網頁設計公司,幫您輕鬆架站!

※想要讓你的商品成為最夯、最多人討論的話題?網頁設計公司讓你強力曝光

※想知道最厲害的台北網頁設計公司推薦台中網頁設計公司推薦專業設計師”嚨底家”!!

java中的transient關鍵字詳解

目錄

前言
說實話學了一段時間java的朋友對於transient這個關鍵字依舊很陌生基本沒怎麼用過,但是transient關鍵字在java中卻起到了不可或缺的地位!如果要說講到,我覺得最可能出現的地方是IO流中對象流(也叫序列化流)的時候會講到!

相信很多人都是直到自己碰到才會關心這個關鍵字,記得博主第一次碰到transient關鍵字是在閱讀JDK源碼的時候。在學習java的過程中transient關鍵字少見的原因其實離不開它的作用:transient關鍵字的主要作用就是讓某些被transient關鍵字修飾的成員屬性變量不被序列化。實際上也正是因此,在學習過程中很少用得上序列化操作,一般都是在實際開發中!至於序列化,相信有很多小白童鞋一直迷迷糊糊或者沒有具體的概念,這都不是事,下面博主會很清楚的讓你記住啥是序列化,保證你這輩子忘不了(貌似有點誇張,有點裝b,感覺要被打)

@

1、何謂序列化?

說起序列化,隨之而來的另一個概念就是反序列化,小白童鞋不要慌,記住了序列化就相當於記住了反序列化,因為反序列化就是序列化反過來,所以博主建議只記住序列化概念即可,省的搞暈自己。

專業術語定義的序列化:

Java提供了一種對象序列化的機制。用一個字節序列可以表示一個對象,該字節序列包含該對象的數據、對象的類型和對象中存儲的屬性等信息。字節序列寫出到文件之後,相當於文件中持久保存了一個對象的信息。反之,該字節序列還可以從文件中讀取回來,重構對象,對它進行反序列化。對象的數據、對象的類型和對象中存儲的數據信息,都可以用來在內存中創建對象。

宜春的術語定義序列化:

序列化: 字節 ——> 對象

其實,我總結的就是上面的結論,如果不理解,直接參照專業術語的定義,理解之後就記住我的話就行了,記不住,請打死我(我踢m簡直就是個天才)

圖理解序列化:

啥?你不懂啥是字節?其實,我在一篇IO流的文章里就已經介紹了序列化,放心,絕對特別詳細~光看文章名字就知道了~

2、為何要序列化?

從上一節提到序列化的概念,知道概念之後,我們就必須要知道 為何要序列化了。

講為何要序列化原因之前,博主我舉個栗子:

就像你去街上買菜,一般操作都是用塑料袋給包裝起來,直到回家要做菜的時候就把菜給拿出來。而這一系列操作就像極了序列化和反序列化!

Java中對象的序列化指的是將對象轉換成以字節序列的形式來表示,這些字節序列包含了對象的數據和信息,一個序列化后的對象 可以被寫到數據庫或文件中,也可用於 網絡傳輸,一般當我們使用 緩存cache(內存空間不夠有可能會本地存儲到硬盤)或 遠程調用rpc(網絡傳輸)的時候,經常需要讓我們的實體類實現Serializable接口,目的就是為了讓其可序列化。

  • 在開發過程中要使用transient關鍵字修飾的栗子:

如果一個用戶有一些密碼等信息,為了安全起見,不希望在網絡操作中被傳輸,這些信息對應的變量就可以加上transient關鍵字。換句話說,這個字段的生命周期僅存於調用者的內存中而不會寫到磁盤裡持久化。

  • 在開發過程中不需要transient關鍵字修飾的栗子:

1、類中的字段值可以根據其它字段推導出來。
2、看具體業務需求,哪些字段不想被序列化;

不知道各位有木有想過為什麼要不被序列化呢?其實主要是為了節省存儲空間。優化程序!

PS:記得之前看HashMap源碼的時候,發現有個字段是用transient修飾的,我覺得還是有道理的,確實沒必要對這個modCount字段進行序列化,因為沒有意義,modCount主要用於判斷HashMap是否被修改(像put、remove操作的時候,modCount都會自增),對於這種變量,一開始可以為任何值,0當然也是可以(new出來、反序列化出來、或者克隆clone出來的時候都是為0的),沒必要持久化其值。

當然,序列化后的最終目的是為了反序列化,恢復成原先的Java對象,要不然序列化后幹嘛呢,就像買菜一樣,用塑料袋包裹最後還是為了方便安全到家再去掉塑料袋,所以序列化后的字節序列都是可以恢復成Java對象的,這個過程就是反序列化。

3、序列化與transient的使用

 1、需要做序列化的對象的類,必須實現序列化接口:Java.lang.Serializable 接口(一個標誌接口,沒有任何抽象方法),Java 中大多數類都實現了該接口,比如:StringInteger類等,不實現此接口的類將不會使任何狀態序列化或反序列化,會拋NotSerializableException異常 。

  2、底層會判斷,如果當前對象是 Serializable 的實例,才允許做序列化,Java對象 instanceof Serializable 來判斷。

  3、在 Java 中使用對象流ObjectOutputStream來完成序列化以及ObjectInputStream流反序列化   

  1. ==ObjectOutputStream:通過 writeObject()方法做序列化操作== 

  2. ==ObjectInputStream:通過 readObject() 方法做反序列化操作==

4、該類的所有屬性必須是可序列化的。如果有一個屬性不需要可序列化的,則該屬性必須註明是瞬態的,使用transient 關鍵字修飾。

由於字節嘛所以肯定要涉及流的操作,也就是對象流也叫序列化流ObjectOutputstream,下面進行多種情況分析序列化的操作代碼!

在這裏,我真的強烈建議看宜春博客的讀者朋友,請試着去敲,切記一眼帶過或者複製過去運行就完事了,特別是小白童鞋,相信我!你一定會有不一樣的收穫。千萬不要覺得浪費時間,有時候慢就是快,宜春親身體會!

3.1、沒有實現Serializable接口進行序列化情況

package TransientTest;
import java.io.*;

class UserInfo {  //================================注意這裏沒有實現Serializable接口
    private String name;
    private transient String password;

    public UserInfo(String name,String psw) {
        this.name = name;
        this.password=psw;
    }

    @Override
    public String toString() {
        return "UserInfo{" +
                "name='" + name + '\'' +
                ", password='" + password + '\'' +
                '}';
    }
}

public class TransientDemo {
    public static void main(String[] args) {

        UserInfo userInfo=new UserInfo("老王","123");
        System.out.println("序列化之前信息:"+userInfo);

        try {
            ObjectOutputStream output=new ObjectOutputStream(new FileOutputStream("userinfo.txt"));
            output.writeObject(new UserInfo("老王","123"));
            output.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

運行結果

3.2、實現Serializable接口序列化情況

當我們加上實現Serializable接口再運行會發現,項目中出現的userinfo.txt文件內容是這樣的:

其實這都不是重點,重點是序列化操作成功了!

3.3、普通序列化情況

package TransientTest;
import java.io.*;

class UserInfo implements Serializable{  //第一步實現Serializable接口
    private String name;
    private String password;//都是普通屬性==============================

    public UserInfo(String name,String psw) {
        this.name = name;
        this.password=psw;
    }

    @Override
    public String toString() {
        return "UserInfo{" +
                "name='" + name + '\'' +
                ", password='" + password + '\'' +
                '}';
    }
}

public class TransientDemo {
    public static void main(String[] args) throws ClassNotFoundException {

        UserInfo userInfo=new UserInfo("程序員老王","123");
        System.out.println("序列化之前信息:"+userInfo);

        try {
            ObjectOutputStream output=new ObjectOutputStream(new FileOutputStream("userinfo.txt")); //第二步開始序列化操作
            output.writeObject(new UserInfo("程序員老王","123"));
            output.close();
        } catch (IOException e) {
            e.printStackTrace();
        }

        try {
            ObjectInputStream input=new ObjectInputStream(new FileInputStream("userinfo.txt"));//第三步開始反序列化操作
            Object o = input.readObject();//ObjectInputStream的readObject方法會拋出ClassNotFoundException
            System.out.println("序列化之後信息:"+o);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

運行結果:

序列化之前信息:UserInfo{name='程序員老王', password='123'}
序列化之後信息:UserInfo{name='程序員老王', password='123'}

3.4、transient序列化情況

package TransientTest;
import java.io.*;

class UserInfo implements Serializable{  //第一步實現Serializable接口
    private String name;
    private transient String password; //特別注意:屬性由transient關鍵字修飾===========

    public UserInfo(String name,String psw) {
        this.name = name;
        this.password=psw;
    }

    @Override
    public String toString() {
        return "UserInfo{" +
                "name='" + name + '\'' +
                ", password='" + password + '\'' +
                '}';
    }
}

public class TransientDemo {
    public static void main(String[] args) throws ClassNotFoundException {

        UserInfo userInfo=new UserInfo("程序員老王","123");
        System.out.println("序列化之前信息:"+userInfo);

        try {
            ObjectOutputStream output=new ObjectOutputStream(new FileOutputStream("userinfo.txt")); //第二步開始序列化操作
            output.writeObject(new UserInfo("程序員老王","123"));
            output.close();
        } catch (IOException e) {
            e.printStackTrace();
        }

        try {
            ObjectInputStream input=new ObjectInputStream(new FileInputStream("userinfo.txt"));//第三步開始反序列化操作
            Object o = input.readObject();//ObjectInputStream的readObject方法會拋出ClassNotFoundException
            System.out.println("序列化之後信息:"+o);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

運行結果:

序列化之前信息:UserInfo{name='程序員老王', password='123'}
序列化之後信息:UserInfo{name='程序員老王', password='null'}

特別注意結果,添加transient修飾的屬性值為默認值null!如果被transient修飾的屬性為int類型,那它被序列化之後值一定是0,當然各位可以去試試,這能說明什麼呢?說明被標記為transient的屬性在對象被序列化的時候不會被保存(或者說變量不會持久化)

3.5、static序列化情況

package TransientTest;
import java.io.*;

class UserInfo implements Serializable{  //第一步實現Serializable接口
    private String name;
    private static String password; //特別注意:屬性由static關鍵字修飾==============

    public UserInfo(String name, String psw) {
        this.name = name;
        this.password=psw;
    }

    @Override
    public String toString() {
        return "UserInfo{" +
                "name='" + name + '\'' +
                ", password='" + password + '\'' +
                '}';
    }
}

public class TransientDemo {
    public static void main(String[] args) throws ClassNotFoundException {

        UserInfo userInfo=new UserInfo("程序員老王","123");
        System.out.println("序列化之前信息:"+userInfo);

        try {
            ObjectOutputStream output=new ObjectOutputStream(new FileOutputStream("userinfo.txt")); //第二步開始序列化操作
            output.writeObject(new UserInfo("程序員老王","123"));
            output.close();
        } catch (IOException e) {
            e.printStackTrace();
        }

        try {
            ObjectInputStream input=new ObjectInputStream(new FileInputStream("userinfo.txt"));//第三步開始反序列化操作
            Object o = input.readObject();//ObjectInputStream的readObject方法會拋出ClassNotFoundException
            System.out.println("序列化之後信息:"+o);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

運行結果:

序列化之前信息:UserInfo{name='程序員老王', password='123'}
序列化之後信息:UserInfo{name='程序員老王', password='123'}

這個時候,你就會錯誤的認為static修飾的也被序列化了,其實不然,實際上這裏很容易被搞暈!明明取出null(默認值)就可以說明不會被序列化,這裏明明沒有變成默認值,為何還要說static不會被序列化呢?

實際上,反序列化后類中static型變量name的值實際上是當前JVM中對應static變量的值,這個值是JVM中的並不是反序列化得出的。也就是說被static修飾的變量並沒有參与序列化!但是咱也不能口說無憑啊,是的,那我們就來看兩個程序對比一下就明白了!

第一個程序:這是一個沒有被static修飾的name屬性程序:

package Thread;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;

class UserInfo implements Serializable {
    private String name;
    private transient String psw;

    public UserInfo(String name, String psw) {
        this.name = name;
        this.psw = psw;
    }

    public  String getName() {
        return name;
    }

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

    public String getPsw() {
        return psw;
    }

    public void setPsw(String psw) {
        this.psw = psw;
    }

    public String toString() {
        return "name=" + name + ", psw=" + psw;
    }
}
public class TestTransient {
    public static void main(String[] args) {
        UserInfo userInfo = new UserInfo("程序員老過", "456");
        System.out.println(userInfo);
        try {
            // 序列化,被設置為transient的屬性沒有被序列化
            ObjectOutputStream o = new ObjectOutputStream(new FileOutputStream("UserInfo.txt"));
            o.writeObject(userInfo);
            o.close();
        } catch (Exception e) {
            // TODO: handle exception
            e.printStackTrace();
        }
        try {
            //在反序列化之前改變name的值 =================================注意這裏的代碼
            userInfo.setName("程序員老改");
            // 重新讀取內容
            ObjectInputStream in = new ObjectInputStream(new FileInputStream("UserInfo.txt"));
            UserInfo readUserInfo = (UserInfo) in.readObject();
            //讀取后psw的內容為null
            System.out.println(readUserInfo.toString());
        } catch (Exception e) {
            // TODO: handle exception
            e.printStackTrace();
        }
    }
}

運行結果:

name=程序員老過, psw=456
name=程序員老過, psw=null

從程序運行結果中可以看出,在反序列化之前試着改變name的值為程序員老改,結果是沒有成功的!

第二個程序:這是一個被static修飾的name屬性程序:

package Thread;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;

class UserInfo implements Serializable {
    private static final long serialVersionUID = 996890129747019948L;
    private static String name;
    private transient String psw;

    public UserInfo(String name, String psw) {
        this.name = name;
        this.psw = psw;
    }

    public  String getName() {
        return name;
    }

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

    public String getPsw() {
        return psw;
    }

    public void setPsw(String psw) {
        this.psw = psw;
    }

    public String toString() {
        return "name=" + name + ", psw=" + psw;
    }
}
public class TestTransient {
    public static void main(String[] args) {
        UserInfo userInfo = new UserInfo("程序員老過", "456");
        System.out.println(userInfo);
        try {
            // 序列化,被設置為transient的屬性沒有被序列化
            ObjectOutputStream o = new ObjectOutputStream(new FileOutputStream("UserInfo.txt"));
            o.writeObject(userInfo);
            o.close();
        } catch (Exception e) {
            // TODO: handle exception
            e.printStackTrace();
        }
        try {
            //在反序列化之前改變name的值
            userInfo.setName("程序員老改");
            // 重新讀取內容
            ObjectInputStream in = new ObjectInputStream(new FileInputStream("UserInfo.txt"));
            UserInfo readUserInfo = (UserInfo) in.readObject();
            //讀取后psw的內容為null
            System.out.println(readUserInfo.toString());
        } catch (Exception e) {
            // TODO: handle exception
            e.printStackTrace();
        }
    }
}

運行結果:

name=程序員老過, psw=456
name=程序員老改, psw=null

從程序運行結果中可以看出,在反序列化之前試着改變name的值為程序員老改,結果是成功的!現在對比一下兩個程序是不是就很清晰了?

static關鍵字修飾的成員屬性優於非靜態成員屬性加載到內存中,同時靜態也優於對象進入到內存中,被static修飾的成員變量不能被序列化,序列化的都是對象,靜態變量不是對象狀態的一部分,因此它不參与序列化。所以將靜態變量聲明為transient變量是沒有用處的。因此,反序列化后類中static型變量name的值實際上是當前JVM中對應static變量的值,這個值是JVM中的並不是反序列化得出的。

如果對static關鍵字還是不太清楚理解的童鞋可以參考這篇文章,應該算是不錯的:

3.6、final序列化情況

對於final關鍵字來講,final變量將直接通過值參与序列化,至於代碼程序我就不再貼出來了,大家可以試着用final修飾驗證一下!

主要注意的是final 和transient可以同時修飾同一個變量,結果也是一樣的,對transient沒有影響,這裏主要提一下,希望各位以後在開發中遇到這些情況不會滿頭霧水!

4、java類中serialVersionUID作用

既然提到了transient關鍵字就不得不提到序列化,既然提到了序列化,就不得不提到serialVersionUID了,它是啥呢?基本上有序列化就會存在這個serialVersionUID。

serialVersionUID適用於Java的序列化機制。簡單來說,Java的序列化機制是通過判斷類的serialVersionUID來驗證版本一致性的。在進行反序列化時,JVM會把傳來的字節流中的serialVersionUID與本地相應實體類的serialVersionUID進行比較,如果相同就認為是一致的,可以進行反序列化,否則就會出現序列化版本不一致的異常,即是InvalidCastException,在開發中有時候可寫可不寫,建議最好還是寫上比較好。

5、transient關鍵字小結

1、變量被transient修飾,變量將不會被序列化
2、transient關鍵字只能修飾變量,而不能修飾方法和類。
3、被static關鍵字修飾的變量不參与序列化,一個靜態static變量不管是否被transient修飾,均不能被序列化。
4、final變量值參与序列化,final transient同時修飾變量,final不會影響transient,一樣不會參与序列化

第二點需要注意的是:本地變量是不能被transient關鍵字修飾的。變量如果是用戶自定義類變量,則該類需要實現Serializable接口

第三點需要注意的是:反序列化后類中static型變量的值實際上是當前JVM中對應static變量的值,這個值是JVM中的並不是反序列化得出的。

結語:被transient關鍵字修飾導致不被序列化,其優點是可以節省存儲空間。優化程序!隨之而來的是會導致被transient修飾的字段會重新計算,初始化!

如果本文對你有一點點幫助,那麼請點個讚唄,謝謝~

若有不足或者不正之處,歡迎指正批評,感激不盡!如果有疑問歡迎留言,絕對第一時間回復!

最後,歡迎各位關注宜春的公眾號,一起探討技術,嚮往技術,追求技術,說好了來了就是盆友喔…

  

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理【其他文章推薦】

USB CONNECTOR掌控什麼技術要點? 帶您認識其相關發展及效能

※評比前十大台北網頁設計台北網站設計公司知名案例作品心得分享

※智慧手機時代的來臨,RWD網頁設計已成為網頁設計推薦首選

小白學 Python 爬蟲(3):前置準備(二)Linux基礎入門

人生苦短,我用 Python

前文傳送門:

Linux 基礎

CentOS 官網: 。

CentOS 官方下載鏈接: 。

Linux 目前在企業中廣泛的應用於服務器系統,無論是寫好的代碼,還是使用的第三方的開源的產品,絕大多數都是部署在 Linux 上面運行的。

可能很多同學一提到 Linux 就慫了,黒糊糊的一篇,連個界面都沒有,滿屏幕都是神秘代碼,沒有一個看得懂的。

表怕,本文就帶你入門 Linux 。

Linux 有不同的發行版本,而我們在企業中一般使用的是 CentOS ,目前比較常用的版本已經到了 7.x 。

由於 Linux 是開源的,所以不同廠商之間提供的發行版會有非常多,比較常見的有 Ubuntu( 基於Debian的桌面版 ) 、Debian( 國際化組織的開源操作系統 ) 、 RedHat( 紅帽企業系統 ) 、 Fedora( 最初由紅帽公司發起的桌面版系統套件 ) 等等。

因為在企業中使用比較多的還是 CentOS ,所以我們還是拿 CentOS 來介紹。

在 win 系統下的安裝可以使用第三方廠商提供的 VMware 或者 win 自帶的 Hyper-V 構建一個虛擬機進行安裝,也可以使用雲服務廠商提供的入門版的雲服務器(1H1G1M),一般新用戶首年價格都在100元以內。

安裝的過程我就不介紹了,百度一下大把。

安裝完成后,設置好 Linux root 用戶的密碼后,可以使用 ssh 工具進行連接,這裏的工具可以選擇 xshell (個人使用免費,就是官網屬實有點慢),打開 xshell 輸入 ip 、用戶名(root)、密碼后,應該可以看到如下界面:

小編這裏使用的是京東雲的服務器,打碼部分涉及 IP 信息,所以隱藏掉了,屬實怕大神搞我。

因為我們的目標不是 Linux 運維工程師,只需要能正常使用,一些簡單常用指令足夠我們日常操作 Linux 了。

首先介紹一下 Linux 的目錄,因為是使用 root 賬號登錄的,所以我們登錄后的目錄是在 /root ,查詢當前所在目錄可以使用命令 pwd ,如下:

輸入命令 cd / ,進入根目錄,再輸出命令 ls ,查看根目錄下都有什麼目錄:

大致介紹下每個目錄放的都是什麼東西:

目錄 簡介
/bin 常用命令一般在這個目錄。
/boot 存放用於系統引導時使用的各種文件。
/dev 用於存放設備文件。
/etc 一般用於存放系統的管理和配置文件。
/home 存放所有用戶文件的根目錄,是用戶主目錄的基點,比如用戶user的主目錄就是/home/user,可以用~user表示。
/lib 存放跟文件系統中的程序運行所需要的共享庫及內核模塊。共享庫又叫動態鏈接共享庫,作用類似windows里的.dll文件,存放了根文件系統程序運行所需的共享文件。
/usr 用於存放系統應用程序,比較重要的目錄/usr/local 本地系統管理員軟件安裝目錄(安裝系統級的應用)。這是最龐大的目錄,要用到的應用程序和文件幾乎都在這個目錄。
/opt 額外安裝的可選應用程序包所放置的位置。
/root 超級用戶(系統管理員)的主目錄。
/var 用於存放運行時需要改變數據的文件,也是某些大文件的溢出區,比方說各種服務的日誌文件(系統啟動日誌等)等。

很多都是系統使用的目錄,我們無需關注,一般會使用到的目錄有 /etc (修改一些系統配置,如改host文件,系統環境變量等), /usr (這裡會安裝一些應用程序),/opt (這裏其實也是安裝一些應用程序)。

簡單介紹幾個命令,有了這幾個命令,基本上我們就可以愉快的操作起來了:

  1. cd:這個不用多講了吧,就是切換目錄。
  2. ls:這個是查看目錄內容。
  3. pwd:显示當前工作目錄 。
  4. mkdir:創建目錄。
  5. vi:編輯文檔,這個命令稍微複雜一點
    1. vi 文件名 :進入一般模式(不能輸入)
    2. 按下 i 從一般模式,進入到插入模式,這時可以修改文檔
    3. 按下esc從插入模式,退出到一般模式 ,這時無法修改文檔
    4. 在一般模式下,輸入:wq ,保存退出編輯;或者還可以輸入 !q 不保存編輯內容退出。
  6. ps: 查看任務管理器: ps -ef ,例如查看 mysql 的進程,ps -ef | grep mysql 。
  7. kill:這個就是殺進程,常用格式 kill -9 pid(進程編號),配合上面的 ps 命令一起使用,殺掉你想殺的進程。
  8. tar:壓縮與解壓,常用解壓命令 tar -xvzf [需解壓的文件名] ,常用壓縮命令 tar -cvzf [壓縮后的文件名] [被壓縮的文件名] 。
  9. reboot:重啟
  10. halt:關機
  11. rm:刪除命令,常用核彈級命令 rm -rf / ;此命令禁止在任何地方嘗試,一旦執行,將無法逆轉,含義是將跟目錄直接刪除。

下面我們來演示下如何在 CentOS 上安裝 Python3 。

因為 CentOS 本身自帶 Python ,但是版本是 Python2.7 :

這裏我們不去管它,首先去 Python 官網找到 Python 的下載地址:

Python 官網下載鏈接:

小編這裏選擇的是截止目前最新發布的 3.8.0 版本。

這時我們切換到 xshell 的操作界面開始操作起來,首先切換至 /opt 目錄:

cd /opt

然後下載 Python3.8 的安裝包:

wget https://www.python.org/ftp/python/3.8.0/Python-3.8.0.tgz

這裏遇到新的命令 wget ,這個命令如果 CentOS 未提供,需要先進行安裝:

yum install wget

簡單介紹一下, yum 是在 Linux 中的一個包管理工具,可以進行簡單的安裝操作。

等待進度條下載完,下載完成后直接解壓:

tar -xvzf Python-3.8.0.tgz

解壓后編譯安裝:

# 創建安裝目錄
mkdir /usr/local/python3
cd Python-3.8.0
# 檢查配置
./configure --prefix=/usr/local/python3
# 編譯、安裝
make && make install
# 創建軟連接
ln -s /usr/local/python3/bin/python3 /usr/bin/python3
ln -s /usr/local/python3/bin/pip3 /usr/bin/pip3

測試安裝結果:

# 輸入
python3 -V
# 輸出
Python 3.8.0
# 輸入
pip3 -V
# 輸出
pip 19.2.3 from /usr/local/python3/lib/python3.8/site-packages/pip (python 3.8)

因為 Linux 部分功能也是依賴 Python 的,我們不覆蓋當前的 Python 命令的版本,直接創建一個新的 Python 命令 python3 。以及新的 pip 包管理命令 pip3

希望各位同學可以自己使用虛擬機安裝一個 CentOS 試試看,後續的部分內容將會涉及 Linux 。

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理【其他文章推薦】

網頁設計公司推薦更多不同的設計風格,搶佔消費者視覺第一線

※廣告預算用在刀口上,網站設計公司幫您達到更多曝光效益

※自行創業 缺乏曝光? 下一步"網站設計"幫您第一時間規劃公司的門面形象

【NServiceBus】什麼是Saga,Saga能做什麼

前言

          Saga單詞翻譯過來是指尤指古代挪威或冰島講述冒險經歷和英雄業績的長篇故事,對,這裏強調長篇故事。許多系統都存在長時間運行的業務流程,NServiceBus使用基於事件驅動的體繫結構將容錯性和可伸縮性融入這些業務處理過程中。
          當然一個單一接口調用則算不上一個長時間運行的業務場景,那麼如果在給定的用例中有兩個或多個調用,則應該考慮數據一致性的問題,這裡有可能第一個接口調用成功,第二次調用則可能失敗或者超時,Saga的設計以簡單而健壯的方式處理這樣的業務用例。

認識Saga

         先來通過一段代碼簡單認識一下Saga,在NServiceBus里,使用Saga的話則需要實現抽象類Saga ,SqlSaga ,這裏的T的是Saga業務實體,封裝數據,用來在長時間運行過程中封裝業務數據。

public class Saga:Saga<State>,
        IAmStartedByMessages<StartOrder>,
        IHandleMessages<CompleteOrder>
    {
        protected override void ConfigureHowToFindSaga(SagaPropertyMapper<State> mapper)
        {
            mapper.ConfigureMapping<StartOrder>(message=>message.OrderId).ToSaga(saga=>saga.OrderId);
            mapper.ConfigureMapping<CompleteOrder>(message=>message.OrderId).ToSaga(saga=>saga.OrderId);
        }

        public Task Handle(StartOrder message, IMessageHandlerContext context)
        {
            return Task.CompletedTask;
        }

        public Task Handle(CompleteOrder message, IMessageHandlerContext context)
        {
            MarkAsComplete();
            return Task.CompletedTask;
        }
    }

臨時狀態

     長時間運行則意味着有狀態,任何涉及多個網絡調用的進程都需要一個臨時狀態,這個臨時狀態可以存儲在內存中,序列化在磁盤中,也可以存儲在分佈式緩存中。在NServiceBus中我們定義實體,繼承抽象類ContainSagaData即可,默認情況下,所有公開訪問的屬性都會被持久化。

public class State:ContainSagaData
{
    public Guid OrderId { get; set; }
}

添加行為

      在NServiceBus里,處理消息的有兩種接口:IHandlerMessages 、IAmStartedByMessages 。

開啟一個Saga

       在前面的代碼片段里,我們看到已經實現了接口IAmStartedByMessages ,這個接口告訴NServiceBus,如果收到了StartOrder 消息,則創建一個Saga實例(Saga Instance),當然Saga長流程處理的實體至少有一個需要開啟Saga流程。

處理無序消息

       如果你的業務用例中確實存在無序消息的情況,則還需要業務流程正常輪轉,那麼則需要多個messaeg都要事先接口IAmStartedByMessages接口,也就是說多個message都可以創建Saga實例。

依賴可恢復性

      在處理無序消息和多個消息類型的時候,就存在消息丟失的可能,必須在你的Saga狀態完成以後,這個Saga實例又收到一條消息,但這時Saga狀態已經是完結狀態,這條消息則仍然需要處理,這裏則實現NServiceBus的IHandleSagaNotFound接口。

 public class SagaNotFoundHandler:IHandleSagaNotFound
 {
    public Task Handle(object message, IMessageProcessingContext context)
    {
        return context.Reply(new SagaNotFoundMessage());
    }
 }
  
 public class SagaNotFoundMessage
 {
        
 }

結束Saga

      當你的業務用例不再需要Saga實例時,則調用MarkComplete()來結束Saga實例。這個方法在前面的代碼片段中也可以看到,其實本質也就是設置Saga.Complete屬性,這是個bool值,你在業務用例中也可以用此值來判斷Saga流程是否結束。

namespace NServiceBus
{
    using System;
    using System.Threading.Tasks;
    using Extensibility;

    public abstract class Saga
    {
        /// <summary>
        /// The saga's typed data.
        /// </summary>
        public IContainSagaData Entity { get; set; }

        
        public bool Completed { get; private set; }

        internal protected abstract void ConfigureHowToFindSaga(IConfigureHowToFindSagaWithMessage sagaMessageFindingConfiguration);

       
        protected Task RequestTimeout<TTimeoutMessageType>(IMessageHandlerContext context, DateTime at) where TTimeoutMessageType : new()
        {
            return RequestTimeout(context, at, new TTimeoutMessageType());
        }

        
        protected Task RequestTimeout<TTimeoutMessageType>(IMessageHandlerContext context, DateTime at, TTimeoutMessageType timeoutMessage)
        {
            if (at.Kind == DateTimeKind.Unspecified)
            {
                throw new InvalidOperationException("Kind property of DateTime 'at' must be specified.");
            }

            VerifySagaCanHandleTimeout(timeoutMessage);

            var options = new SendOptions();

            options.DoNotDeliverBefore(at);
            options.RouteToThisEndpoint();

            SetTimeoutHeaders(options);

            return context.Send(timeoutMessage, options);
        }

        
        protected Task RequestTimeout<TTimeoutMessageType>(IMessageHandlerContext context, TimeSpan within) where TTimeoutMessageType : new()
        {
            return RequestTimeout(context, within, new TTimeoutMessageType());
        }

        
        protected Task RequestTimeout<TTimeoutMessageType>(IMessageHandlerContext context, TimeSpan within, TTimeoutMessageType timeoutMessage)
        {
            VerifySagaCanHandleTimeout(timeoutMessage);

            var sendOptions = new SendOptions();

            sendOptions.DelayDeliveryWith(within);
            sendOptions.RouteToThisEndpoint();

            SetTimeoutHeaders(sendOptions);

            return context.Send(timeoutMessage, sendOptions);
        }

        
        protected Task ReplyToOriginator(IMessageHandlerContext context, object message)
        {
            if (string.IsNullOrEmpty(Entity.Originator))
            {
                throw new Exception("Entity.Originator cannot be null. Perhaps the sender is a SendOnly endpoint.");
            }

            var options = new ReplyOptions();

            options.SetDestination(Entity.Originator);
            context.Extensions.Set(new AttachCorrelationIdBehavior.State { CustomCorrelationId = Entity.OriginalMessageId });

            
            options.Context.Set(new PopulateAutoCorrelationHeadersForRepliesBehavior.State
            {
                SagaTypeToUse = null,
                SagaIdToUse = null
            });

            return context.Reply(message, options);
        }

        //這個方法結束saga流程,標記Completed屬性
        protected void MarkAsComplete()
        {
            Completed = true;
        }

        void VerifySagaCanHandleTimeout<TTimeoutMessageType>(TTimeoutMessageType timeoutMessage)
        {
            var canHandleTimeoutMessage = this is IHandleTimeouts<TTimeoutMessageType>;
            if (!canHandleTimeoutMessage)
            {
                var message = $"The type '{GetType().Name}' cannot request timeouts for '{timeoutMessage}' because it does not implement 'IHandleTimeouts<{typeof(TTimeoutMessageType).FullName}>'";
                throw new Exception(message);
            }
        }

        void SetTimeoutHeaders(ExtendableOptions options)
        {
            options.SetHeader(Headers.SagaId, Entity.Id.ToString());
            options.SetHeader(Headers.IsSagaTimeoutMessage, bool.TrueString);
            options.SetHeader(Headers.SagaType, GetType().AssemblyQualifiedName);
        }
    }
}

    

Saga持久化

      本機開發環境我們使用LearningPersistence,但是投產的話則需要使用數據庫持久化,這裏我們基於MySQL,SQL持久化需要引入NServiceBus.Persistence.Sql。SQL Persistence會生成幾種關係型數據庫的sql scripts,然後會根據你的斷言配置選擇所需數據庫,比如SQL Server、MySQL、PostgreSQL、Oracle。
     持久化Saga自動創建所需表結構,你只需手動配置即可,配置后編譯成功後項目執行目錄下會生成sql腳本,文件夾名稱是NServiceBus.Persistence.Sql,下面會有Saga子目錄。


/* TableNameVariable */

set @tableNameQuoted = concat('`', @tablePrefix, 'Saga`');
set @tableNameNonQuoted = concat(@tablePrefix, 'Saga');


/* Initialize */

drop procedure if exists sqlpersistence_raiseerror;
create procedure sqlpersistence_raiseerror(message varchar(256))
begin
signal sqlstate
    'ERROR'
set
    message_text = message,
    mysql_errno = '45000';
end;

/* CreateTable */

set @createTable = concat('
    create table if not exists ', @tableNameQuoted, '(
        Id varchar(38) not null,
        Metadata json not null,
        Data json not null,
        PersistenceVersion varchar(23) not null,
        SagaTypeVersion varchar(23) not null,
        Concurrency int not null,
        primary key (Id)
    ) default charset=ascii;
');
prepare script from @createTable;
execute script;
deallocate prepare script;

/* AddProperty OrderId */

select count(*)
into @exist
from information_schema.columns
where table_schema = database() and
      column_name = 'Correlation_OrderId' and
      table_name = @tableNameNonQuoted;

set @query = IF(
    @exist <= 0,
    concat('alter table ', @tableNameQuoted, ' add column Correlation_OrderId varchar(38) character set ascii'), 'select \'Column Exists\' status');

prepare script from @query;
execute script;
deallocate prepare script;

/* VerifyColumnType Guid */

set @column_type_OrderId = (
  select concat(column_type,' character set ', character_set_name)
  from information_schema.columns
  where
    table_schema = database() and
    table_name = @tableNameNonQuoted and
    column_name = 'Correlation_OrderId'
);

set @query = IF(
    @column_type_OrderId <> 'varchar(38) character set ascii',
    'call sqlpersistence_raiseerror(concat(\'Incorrect data type for Correlation_OrderId. Expected varchar(38) character set ascii got \', @column_type_OrderId, \'.\'));',
    'select \'Column Type OK\' status');

prepare script from @query;
execute script;
deallocate prepare script;

/* WriteCreateIndex OrderId */

select count(*)
into @exist
from information_schema.statistics
where
    table_schema = database() and
    index_name = 'Index_Correlation_OrderId' and
    table_name = @tableNameNonQuoted;

set @query = IF(
    @exist <= 0,
    concat('create unique index Index_Correlation_OrderId on ', @tableNameQuoted, '(Correlation_OrderId)'), 'select \'Index Exists\' status');

prepare script from @query;
execute script;
deallocate prepare script;

/* PurgeObsoleteIndex */

select concat('drop index ', index_name, ' on ', @tableNameQuoted, ';')
from information_schema.statistics
where
    table_schema = database() and
    table_name = @tableNameNonQuoted and
    index_name like 'Index_Correlation_%' and
    index_name <> 'Index_Correlation_OrderId' and
    table_schema = database()
into @dropIndexQuery;
select if (
    @dropIndexQuery is not null,
    @dropIndexQuery,
    'select ''no index to delete'';')
    into @dropIndexQuery;

prepare script from @dropIndexQuery;
execute script;
deallocate prepare script;

/* PurgeObsoleteProperties */

select concat('alter table ', table_name, ' drop column ', column_name, ';')
from information_schema.columns
where
    table_schema = database() and
    table_name = @tableNameNonQuoted and
    column_name like 'Correlation_%' and
    column_name <> 'Correlation_OrderId'
into @dropPropertiesQuery;

select if (
    @dropPropertiesQuery is not null,
    @dropPropertiesQuery,
    'select ''no property to delete'';')
    into @dropPropertiesQuery;

prepare script from @dropPropertiesQuery;
execute script;
deallocate prepare script;

/* CompleteSagaScript */

生成的表結構:

持久化配置

      Saga持久化需要依賴NServiceBus.Persistence.Sql。引入后需要實現SqlSaga抽象類,抽象類需要重寫ConfigureMapping,配置Saga工作流程業務主鍵。

public class Saga:SqlSaga<State>,
        IAmStartedByMessages<StartOrder>
{
   protected override void ConfigureMapping(IMessagePropertyMapper mapper)
   {
      mapper.ConfigureMapping<StartOrder>(message=>message.OrderId);
   }

   protected override string CorrelationPropertyName => nameof(StartOrder.OrderId);

   public Task Handle(StartOrder message, IMessageHandlerContext context)
   {
       Console.WriteLine($"Receive message with OrderId:{message.OrderId}");

       MarkAsComplete();
       return Task.CompletedTask;
    }
 }
    
 static async Task MainAsync()
 {
     Console.Title = "Client-UI";

     var configuration = new EndpointConfiguration("Client-UI");
     //這個方法開啟自動建表、自動創建RabbitMQ隊列
     configuration.EnableInstallers(); 
     configuration.UseSerialization<NewtonsoftSerializer>();
     configuration.UseTransport<LearningTransport>();

     string connectionString = "server=127.0.0.1;uid=root;pwd=000000;database=nservicebus;port=3306;AllowUserVariables=True;AutoEnlist=false";
     var persistence = configuration.UsePersistence<SqlPersistence>();
     persistence.SqlDialect<SqlDialect.MySql>();
     //配置mysql連接串
     persistence.ConnectionBuilder(()=>new MySqlConnection(connectionString));

     var instance = await Endpoint.Start(configuration).ConfigureAwait(false);

     var command = new StartOrder()
     {
         OrderId = Guid.NewGuid()
     };

     await instance.SendLocal(command).ConfigureAwait(false);

     Console.ReadKey();

     await instance.Stop().ConfigureAwait(false);
 }

     

Saga Timeouts

     在消息驅動類型的環境中,雖然傳遞的無連接特性可以防止在線等待過程中消耗資源,但是畢竟等待時間需要有一個上線。在NServiceBus里已經提供了Timeout方法,我們只需訂閱即可,可以在你的Handle方法中根據需要訂閱Timeout,可參考如下代碼:

public class Saga:Saga<State>,
        IAmStartedByMessages<StartOrder>,
        IHandleMessages<CompleteOrder>,
        IHandleTimeouts<TimeOutMessage>
    {
        
        public Task Handle(StartOrder message, IMessageHandlerContext context)
        {
            var model=new TimeOutMessage();
            
            //訂閱超時消息
            return RequestTimeout(context,TimeSpan.FromMinutes(10));
        }

        public Task Handle(CompleteOrder message, IMessageHandlerContext context)
        {
            MarkAsComplete();
            return Task.CompletedTask;
        }

        protected override string CorrelationPropertyName => nameof(StartOrder.OrderId);


        public Task Timeout(TimeOutMessage state, IMessageHandlerContext context)
        {
            //處理超時消息
        }

        protected override void ConfigureHowToFindSaga(SagaPropertyMapper<State> mapper)
        {
            mapper.ConfigureMapping<StartOrder>(message=>message.OrderId).ToSaga(saga=>saga.OrderId);
            mapper.ConfigureMapping<CompleteOrder>(message=>message.OrderId).ToSaga(saga=>saga.OrderId);
        }
    }
//從Timeout的源碼看,這個方法是通過設置SendOptions,然後再把當前這個消息發送給自己來實現 
protected Task RequestTimeout<TTimeoutMessageType>(IMessageHandlerContext context, TimeSpan within, TTimeoutMessageType timeoutMessage)
 {
     VerifySagaCanHandleTimeout(timeoutMessage);
     var sendOptions = new SendOptions();
     sendOptions.DelayDeliveryWith(within);
     sendOptions.RouteToThisEndpoint();
     SetTimeoutHeaders(sendOptions);

     return context.Send(timeoutMessage, sendOptions);
 }

總結

       NServiceBus因為是商業產品,對分佈式消息系統所涉及到的東西都做了實現,包括分佈式事務(Outbox)、DTC都有,還有心跳檢測,監控都有,全而大,目前我們用到的也只是NServiceBus里很小的一部分功能。

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理【其他文章推薦】

※如何讓商品強力曝光呢? 網頁設計公司幫您建置最吸引人的網站,提高曝光率!!

網頁設計一頭霧水??該從何著手呢? 找到專業技術的網頁設計公司,幫您輕鬆架站!

※想知道最厲害的台北網頁設計公司推薦台中網頁設計公司推薦專業設計師”嚨底家”!!

我的程序員之路——大學和2012年

  我於2007年參加高考,順利考入一所男女比例嚴重失調的師範大學,主修計算機科學與技術。其實高中的時候本來想選生物的,可惜報生物的人太少,就沒有開班,後面選修了化學。原計劃是想學高分子材料相關專業的,怎奈高考分數太低,沒有考上相關的大學。第二志願填寫的是計算機相關的學校,當初並不知道這個專業是編程,以為是修理電腦的。因為自家電腦老是這個那個的問題,所以就義無反顧的報考了計算機專業。填志願的時候沒有什麼偉大的理想,也沒有什麼周密的計劃,就是隨意的這麼一填,沒想到現在就靠這個吃飯了。

一、課程

1)疑惑

  本校的這門計算機科學與技術是偏向綜合的,既要學軟件相關的知識,又要學硬件相關的知識。剛進入大一的時候,對一些課程感到疑惑,例如數學、物理、英語、模擬電路等課程,修電腦怎麼要學這些課程,後來才知道,其實我們學的不是修電腦,而是編程。

2)為分數而學習

  由於對編程並不熱愛,因此上課其實也不是很上心,剛開始就是為了分數而學習,完全領會不到這些課程的意義。比較上心的就是C語言了,譚浩強出的那本。一開始完全沒有基礎,寫代碼很吃力,而且那時候筆記本電腦還沒普及,也很少有機會上機調試。雖然學校有機房,但離的比較遠,基本是不會去的。後面練習的多了,慢慢也就會了,應付考試是沒問題的,但寫的代碼不夠有靈性。大二學了數據結構,比較難理解,也是靠課後練習,當時純粹是應試,所以過段時間就都忘了。包括後面的網絡、離散數學、排列組合等等,都是為了考個好成績才學的。

3)學習氛圍

  大一和大二被安排在遠離繁華都市的偏僻海邊的新校區,在這裏沒有量販KTV、沒有大型超市、沒有巨型商場。此處雖然資源有限,但是卻非常適合學習。不過,大家高中時候都學累了,進入大學后就都很放鬆。很多人喜歡去網絡文化交流中心包夜,最誇張的是隔壁班的一個同學,居然一去就是幾個月。在這種環境中,想要心無雜念的深入學習計算機真的蠻難的,況且自己也不熱愛,所以經常告訴自己學這個只是為了以後能有個混口飯吃的技能。

  後面大三回到老校區,遇到了專升本的那幫人,他們的學習熱情與我們正好相反。他們真的是熱愛這個專業,有次放學路過,發現他們把任課老師圍了起來,正在向老師諮詢這個那個的問題,他們肯定是已經明白了學習的意義,所以才能這麼投入。

4)教授

  我們文科學院的教授講課都非常有趣,可以用引人入勝來形容,上他們的課相對會有勁很多,尤其是歷史課,可以聽到很多故事。反觀我們理科院的教授,就不那麼有趣了,很多時候都是蠻枯燥的,上課很容易走神。他們的水平都很高,就是講課的時候很難讓我們理解,當時有一門離散數學,讓我印象深刻,大部分人都不會。課堂氛圍比較好的是操作系統課的那位老師,他講的蠻通俗的,有時候上課還能互動互動。

5)操作課

  大二有一門網頁設計課,授課老師讓我們用Dreamwever製作一張靜態網頁,模板就是他的博客首頁,他博客的訪問量一度飆升。這門課提起了大家的興趣,都在宿舍開筆記本製作,還會對比,看誰做的相似度最高,有的人做的很不錯。看來大家還是喜歡這種能看到效果的操作課,像數據庫、數據結構那種理論課,都提不起大家的興趣。

  大三的時候,還有一門多媒體,這門課會教點PS的內容,讓我們整天P一個胖子,摳圖啥的,大家有時候還是會有點興趣。

6)ACM

  ACM是一項編程競賽,大一的時候,有學長過來做宣講,說拿到好名次能為以後找工作提供很大的便利,一下子就勾起了一大批人的興趣,大家爭相報名參加。因為名次的含金量高,所以這項編程競賽難度也很大。ACM考的是算法,並且他的題目都是英文的,有專門的刷題網站,很多人一看要做題目,興緻就降低了很多,後面又看到題目難度很大,有些題目有點奧數的味道,漸漸的越來越多的人選擇退出。我當時也堅持了一段時間,不過自己的數學建模能力實在太弱,最終也還是放棄了。隔壁班有個同學堅持了,還能拿到名次,畢業的時候直接進了騰訊。

二、實習

1)ERP系統

  大三下半年輔導員給了我一個機會,讓我和一個學長一起做個校外的項目,雖然這個項目做到一半終止了,但對我的影響是很大的。當時是用的軟件編輯器是VS2005,打算做個ERP系統,學長是在英特爾上班的,他把那套成熟的軟件開發模式帶了過來。第一次使用項目管理系統readmine,第一次使用版本控制系統SVN,第一次使用C#開發系統,第一次製作一個完整的項目,第一次採集需求等各種第一次,讓我感覺自己好像已經進入了職場。每個月還能有500塊的收入,我用第一個月的收入買了塊500G的移動硬盤。不得不說,這次實習經歷,直接改變了我未來的職業規劃,促使我踏上了程序員這條道路。

2)商城系統

  時間很快到了大四,那一年我的一個初中同學聯繫到了我,和我說他同學搞了個軟件項目,正好缺人,想讓我也加入。當時學長的那個ERP項目已經被腰斬,正好沒事,馬上就答應了。抽了一天時間,坐了很久的公交,到了他們租的一套公寓里,了解到他們想搞個商城系統,正好也是用C#開發,模仿當時的一套開源系統來做。這次的經歷讓我接觸到了前端,確切的說是JavaScript,因為CSS和HTML由團隊的另外一個成員做。與此同時,我也迷上了前端,因為在完成某個效果時,能帶給我巨大的成就感和滿足感。不過,一直到6年後,才有機會轉型成全職前端。

3)戶外旅遊網

  商城系統團隊後面由於種種客觀原因無奈解散了,當時正好有一家戶外旅遊網在我們大學里招實習生維護公司的網站。我就報名了,學校在徐匯區,而這家公司在虹口區,兩者相距蠻遠的,地鐵都要坐一個多小時,然後下地鐵再走到公司。這家公司還有另外一個同事管網站這塊,不過他只能算半個,因為他主要管旅遊產品那塊。舊網站是用ASP開發的,我過來后老闆讓我先做點邊邊角角的事情,然後讓我開發一個和開心網類似的社交網站。這次是獨立完成了這個項目,包括數據庫設計、頁面製作、產品規劃等,我的另一篇文章《》就詳細記錄了其中的開發過程。實習期間,還拿了公司傳單在學校里發,中午的時候,在人流量最多的地方發,對自己也是一種挑戰。

三、2012年

  轉眼四年過去了,我也畢業了。但一直到畢業的時候,還沒意識到大學四年是用來打基礎的,導致剛畢業那會兒軟件基礎很薄弱。

  實習的那家旅遊公司也和我簽約了,但由於個人原因,我辭掉了這份工作。經一個朋友推薦,我回到了家附近,位於上海郊區,在那裡找到了一家軟件外包公司。這家公司主要給政府做PPT和網站,規模很小,全公司只有8個人,我從那離職7年多了,另外那7人還在。

  這裏我建議剛畢業的學生有機會去規模大點的公司,還是要把握中,因為上規模的公司在組織架構、技術沉澱、規章制度等各方面都比較成熟,並且還有比較好的人脈網,社會終歸是由人組成的,你的人脈越廣,好的機會也會越多。

四、新的開始

1)面試

  2011年9月我來到了這家小外包公司面試,我記得那天是周末,過去的時候公司里一個人都沒的。公司老闆面的我,沒涉及多少技術,就說了當前開發用的是我比較熟悉的C#語言,還介紹了一下公司的業務,主要做些和政府相關的項目,然後就讓我第二天來上班了。這裏說個很巧的事情,公司周五有個同事離職,而我過來就是替代此人的,我結婚那天才發現這個同事就是我老婆關係很近的表姐,真是無巧不成書。

  這家公司很少加班,朝八晚五,基本到點就走,包中飯,老財務早上會去菜場買菜,然後在公司燒。活挺輕鬆的,不過就是工資太低,公積金也不交,試用期是2000一個月,轉正後也只有3500,就這樣我幹了一年半。

  其實當時還有另外一次面試,另一個朋友推薦的,一家大公司,在上海市中心南京西路上。我面試后感覺自己能力還不夠,並且離家太遠,當時不怎麼想太折騰,於是就婉拒了他們的複試。安心的在這家小公司鍛煉,希望能快速的成長。

2)上手

  說個題外話,剛進入這家公司的時候,我開通了個人博客,不過對自己不夠自信,怕被別人嘲諷,一直到3年後的2014年才撰寫了自己的第一篇博文。其實現在想想,平時寫點技術和項目的總結,對自己的成長會有很大的幫助。

  剛開始給我安排的都是些遺留項目,就是簡單的改改頁面中的細節,難度不大。有一次,老闆問我壓力大不大,我很爽快的回答不大。後面讓我獨立的完成一些項目,總體來說沒有什麼大難度。政府項目都比較有規律,後台的模塊大部分都能套用,前台的頁面只是換個皮膚,大框架也比較類似。不過,這段時間對CSS、HTML和JavaScript有了新的認識,公司真正意義上的開發除了我就是另外一個同事,因此很多時候做特效都得自己想辦法解決。這段時間搜索引擎發揮了巨大的作用,公司有段時間不能上百度,google又上不去,就改用了Bing,搜索質量感覺比百度要好一點。

3)挑戰

  要說這段時間比較有挑戰的項目應該就是一個重陽節登高的報名活動,就是個表單頁面,然後填手機號、姓名等信息,最後返回一個報名號給用戶。這個頁面的併發量比較高,5000個報名量基本在兩三個小時內就能全部搶完,對於我這個菜鳥來說,要處理這並不算高的併發還是有點挑戰的。我清晰的記得上線前的一天晚上輾轉反側,很擔心會出大事故,像頁面打不開、報錯等等,因為這邊沒有專業的測試,全憑自己測試,這就很難保證質量。還好,沒出大事故,但還是出現了兩個或多個領到了同一個報名號的問題,最後另外兩個同事一個個的打電話通知他們,換了新號碼給他們,這件事就算這麼過去了。

  活動上線后的第二天,和別人聊天的時候,他正好提到了這個活動,我跟他說這個活動我做的,還是蠻自豪的。

4)跑客戶

  外包公司免不了要跑到客戶那邊去,了解需求或修改BUG。有些客戶就在附近,走過去就行,有些就比較遠了,不僅如此,遇到颳風下雨烈日的天氣,還得跑出去,日晒雨淋的還是蠻苦的。

  有一次蠻坑的,跑到長寧區,基本一個下午就沒了,背個電腦過去,然後發現是他們Excel模板用的不對,只能呵呵了,再跑回來,基本已經快到五點下班時間了,一天就沒了。還有一次跑到太倉去,老闆想開發新客戶,然後當天開車來回,有時候回公司已經六七點了。我運氣比較好,遇到的客戶都是蠻客氣的,也很配合,聽說太倉那個客戶,後面有個老頭總是刁難我們。

5)離職

  我離職的主要原因還是工資的問題,實在太低,後面有個朋友找我,他那邊在創業,有個很好的項目,讓我過去幫忙,工資還開雙倍,我馬上就答應了。這個時候是2012年的12月份,馬上要過年了,雖然有點年終獎,但很少,所以也就不在意了。

  與公司同事相處的還是很融洽的,他們也都很理解我,我在離職前特地請大家去吃了頓小肥羊火鍋,算是散夥飯。今年技術有所提升,人際關係的處理上也愈加成熟。

五、兼職

  這家公司的活蠻輕鬆的,上家實習的旅遊公司又不想再去外面招人,就找到了我,讓我兼職乾著,每個月給個固定的2000元。

1)職能

  主要就是維護網站,其中最忙的是兩次改版。尤其是2011年的12月份,我晚上下班后改頁面,周末去虹口和公司的人對需求,對頁面,那段時間非常的辛苦。當時很年輕,也不覺得,只感覺自己的生活很充實。但有時候,我白天上班的時候旅遊網出了問題,就只能遠程修改一下了。有一次最嚴重,周五的時候,頁面打不開了,下午就請假,直接打了200多的車過去,然後周末就一直在那邊改代碼。現在讓我做兼職我肯定是不願意的。

 

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理【其他文章推薦】

※帶您來了解什麼是 USB CONNECTOR  ?

※自行創業 缺乏曝光? 下一步"網站設計"幫您第一時間規劃公司的門面形象

※如何讓商品強力曝光呢? 網頁設計公司幫您建置最吸引人的網站,提高曝光率!!

※綠能、環保無空污,成為電動車最新代名詞,目前市場使用率逐漸普及化

※廣告預算用在刀口上,網站設計公司幫您達到更多曝光效益

[UWP]用Win2D實現鏤空文字

1. 前言

之前用PointLight做了一個番茄鍾,效果還不錯,具體可見這篇文章:

後來試玩了Win2D,這次就用Win2D實現文字的鏤空效果,配合PointLight做一個內斂不張揚的番茄鍾。

實現鏤空文字的核心思想是使用CanvasGeometry.CreateText從TextLayout獲取一個Geometry,然後使用DrawGeometry將它畫到DrawingSurface。這篇文章介紹了具體的實現步驟。

2. 參考例子

Win2D Gallery提供了大量Win2D的Sample,這次就參考了其中的文字鏤空效果例子,地址和運行效果如下:

3. 實現步驟

Sample的代碼量雖多,其實核心並不複雜,下面講講需要用到的API:

3.1 CanvasDevice.GetSharedDevice

因為要用到Win2D,所以首先要引用 nuget包。因為我的目標不是輸出到CanvasControl上,而是想要輸出到一個SpriteVisual上,所以使用:

var canvasDevice = CanvasDevice.GetSharedDevice();

3.2 CanvasComposition.CreateCompositionGraphicsDevice

然後創建一個Compositor,並將這個Compositor和CanvasDevice關聯起來,這裏需要使用 創建 :

var compositor = ElementCompositionPreview.GetElementVisual(this).Compositor;
var graphicsDevice = CanvasComposition.CreateCompositionGraphicsDevice(compositor, canvasDevice);

3.3 CompositionGraphicsDevice.CreateDrawingSurface

然後使用創建一個對象,它是用來繪畫內容的表面:

var drawingSurface = graphicsDevice.CreateDrawingSurface(e.NewSize, DirectXPixelFormat.B8G8R8A8UIntNormalized, DirectXAlphaMode.Premultiplied);

3.4 Compositor.CreateSurfaceBrush

使用創建一個CompositionSurfaceBrush,它的作用是使用像素繪製SpriteVisual,簡單來說它就是一張位圖,然後輸出到SpriteVisual上:

var maskSurfaceBrush = compositor.CreateSurfaceBrush(drawingSurface);
spriteTextVisual.Brush = maskSurfaceBrush;

3.5 CanvasComposition.CreateDrawingSession

有了CompositionDrawingSurface就可以為所欲為了,將這個DrawingSurface作為參數,調用創建,DrawingSession提供了多個函數,可以自由地在DrawingSurface上畫文字、形狀、圖片甚至SVG。

using (var session = CanvasComposition.CreateDrawingSession(drawingSurface))
{

}

3.6 CanvasTextFormat和CanvasTextLayout

要再DrawingSurface上寫字,需要,而CanvasTextLayout中的文字大小、格式等則由定義:

using (var textFormat = new CanvasTextFormat()
{
    FontSize = (float)FontSize,
    Direction = CanvasTextDirection.LeftToRightThenTopToBottom,
    VerticalAlignment = CanvasVerticalAlignment.Center,
    HorizontalAlignment = CanvasHorizontalAlignment.Center,

})
{
    using (var textLayout = new CanvasTextLayout(session, Text, textFormat, width, height))
    {
        Color fontColor = FontColor;
        session.DrawTextLayout(textLayout, 0, 0, fontColor);
    }
}

3.7 CanvasGeometry.CreateText

因為我的目標是鏤空的文字,所以不能直接使用DrawTextLayout。這裏需要使用從TextLayout獲取一個Geometry,然後使用DrawGeometry將它畫到DrawingSurface。CanvasStrokeStyle是可選的,它控制邊框的虛線。

using (var textGeometry = CanvasGeometry.CreateText(textLayout))
{
    var dashedStroke = new CanvasStrokeStyle()
    {
        DashStyle = DashStyle
    };
    session.DrawGeometry(textGeometry, OutlineColor, (float)StrokeWidth, dashedStroke);
}

4. 封裝為控件

將上面的代碼總結一下,封裝為一個OutlineTextControl 控件,它提供了Text、OutlineColor、FontColor等屬性,在控件SizeChanged時,或者各個屬性改變時調用DrawText重新在CompositionDrawingSurface上繪製文字。代碼大致如下:

public class OutlineTextControl : Control
{
    private CompositionDrawingSurface _drawingSurface;

    public OutlineTextControl()
    {
        var compositor = ElementCompositionPreview.GetElementVisual(this).Compositor;
        var graphicsDevice = CanvasComposition.CreateCompositionGraphicsDevice(compositor, CanvasDevice.GetSharedDevice());
        var spriteTextVisual = compositor.CreateSpriteVisual();

        ElementCompositionPreview.SetElementChildVisual(this, spriteTextVisual);
        SizeChanged += (s, e) =>
        {
            _drawingSurface = graphicsDevice.CreateDrawingSurface(e.NewSize, DirectXPixelFormat.B8G8R8A8UIntNormalized, DirectXAlphaMode.Premultiplied);
            DrawText();
            var maskSurfaceBrush = compositor.CreateSurfaceBrush(_drawingSurface);
            spriteTextVisual.Brush = maskSurfaceBrush;
            spriteTextVisual.Size = e.NewSize.ToVector2();
        };
        RegisterPropertyChangedCallback(FontSizeProperty, new DependencyPropertyChangedCallback((s, e) =>
        {
            DrawText();
        }));
    }


    private void DrawText()
    {
        if (ActualHeight == 0 || ActualWidth == 0 || string.IsNullOrWhiteSpace(Text) || _drawingSurface == null)
            return;

        var width = (float)ActualWidth;
        var height = (float)ActualHeight;
        using (var session = CanvasComposition.CreateDrawingSession(_drawingSurface))
        {
            session.Clear(Colors.Transparent);
            using (var textFormat = new CanvasTextFormat()
            {
                FontSize = (float)FontSize,
                Direction = CanvasTextDirection.LeftToRightThenTopToBottom,
                VerticalAlignment = CanvasVerticalAlignment.Center,
                HorizontalAlignment = CanvasHorizontalAlignment.Center,

            })
            {
                using (var textLayout = new CanvasTextLayout(session, Text, textFormat, width, height))
                {
                    if (ShowNonOutlineText)
                    {
                        session.DrawTextLayout(textLayout, 0, 0, FontColor);
                    }

                    using (var textGeometry = CanvasGeometry.CreateText(textLayout))
                    {
                        var dashedStroke = new CanvasStrokeStyle()
                        {
                            DashStyle = DashStyle
                        };
                        session.DrawGeometry(textGeometry, OutlineColor, (float)StrokeWidth, dashedStroke);
                    }
                }
            }
        }
    }

//SOME CODE AND PROPERTIES

}

5. 結語

文章開頭的那個番茄鍾源碼可以在這裏查看:

也可以安裝我的番茄鍾應用試玩一下,安裝地址:

6. 參考

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理【其他文章推薦】

※為什麼 USB CONNECTOR 是電子產業重要的元件?

網頁設計一頭霧水??該從何著手呢? 找到專業技術的網頁設計公司,幫您輕鬆架站!

※想要讓你的商品成為最夯、最多人討論的話題?網頁設計公司讓你強力曝光

※想知道最厲害的台北網頁設計公司推薦台中網頁設計公司推薦專業設計師”嚨底家”!!

神奇的 SQL 之 MySQL 性能分析神器 → EXPLAIN,SQL 起飛的基石!

前言

  開心一刻

    某人養了一頭豬,煩了想放生,可是豬認識回家的路,放生幾次它都自己回來了。一日,這個人想了個狠辦法,開車帶着豬轉了好多路進山區放生,放生后又各種打轉,然後掏出電話給家裡人打了個電話,問道:“豬回去了嗎?”,家裡人:“早回來了,你在哪了,怎麼還沒回來?”,他大怒道:“讓它來接我,我特么迷路了!!!”

還不如我了

背景

  某一天,樓主打完上班卡,坐在工位逛園子的時候,右下角的 QQ 閃了起來,而且還是個美女頭像!我又驚又喜,腦中閃過我所認識的可能聯繫我的女性,得出個結論:她們這會不可能聯繫我呀,圖像也沒映象,到底是誰了?打開聊天窗口聊了起來

  她:您好,我是公司客服某某某,請問 xxx後台 是您負責的嗎?

  我:您好,是我負責的,有什麼問題嗎?

  她:我發現 xxx 頁面點查詢后,一直是 加載中… ,數據一直出不來,能幫忙看看嗎?

  我:是不是您的姿勢不對?

  她:我就 xxx,然後點查詢

  我:騷等下,我試試,確實有點慢,很長時間才能出來

  她:是的,太慢了,出不來,都急死我了,能快點嗎?

  我:肯定能、必須能!您覺得什麼速度讓您覺得最舒服?

  她:越快越好吧

  我:呃…,是嗎,我先看看是什麼問題,處理好了告訴您,保證讓您覺得舒服!

  她:好的,謝謝!

  公司沒有專門的搜索服務,都是直接從 MySQL 查詢,做簡單的數據處理后返回給頁面,慢的原因肯定就是 SQL 查詢了。找到對應的查詢 SQL ,就是兩個表的聯表查詢,連接鍵也有索引,WHERE 條件也能走索引,怎麼會慢了?然後我用 EXPLAIN 看了下這條 SQL 的執行計劃,找到了慢的原因,具體原因後面揭曉(誰讓你不是豬腳!)

EXPLAIN 是什麼

  它是 MySQL 的一個命令,用來查看 SQL 的執行計劃(SQL 如何執行),根據其輸出結果,我們能夠知道以下信息:表的讀取順序,數據讀取類型,哪些索引可以使用,哪些索引實際使用了,表之間的連接類型,每張表有多少行被優化器查詢等信息,根據這些信息,我們可以找出 SQL 慢的原因,並做針對性的優化

  MySQL 5.6 之前的版本,EXPLAIN 只能用於查看 SELECT 的執行計劃,而從 MySQL 5.6 開始,可以查看 SELECT 、 DELETE 、 INSERT 、 REPLACE 和 UPDATE 的執行計劃,這可不是我瞎掰,不信的可以去 MySQL 的官網查看:

  EXPLAIN 使用方式非常簡單,簡單的你都不敢相信,就是在我們常寫的 SELECT 、 DELETE 、 INSERT 、 REPLACE 和 UPDATE 語句之前加上 EXPLAIN 即可

EXPLAIN SELECT * FROM mysql.`user`;

EXPLAIN DELETE FROM t_user WHERE user_name = '123';

  莫看 EXPLAIN 短,但它胖呀

雖然有點嬰兒肥,但也掩不住我逼人的帥氣!

  雖然 EXPLAIN 使用起來非常簡單,但它的輸出結果中信息量非常大,雖然我胖,但我肚中有貨呀!

環境和數據準備

  MySQL 版本是 5.7.2 ,存儲引擎是 InnoDB 

-- 查看 MySQL 版本
SELECT VERSION();

-- MySQL 提供什麼存儲引擎
SHOW ENGINES;

-- 查看默認存儲引擎
SHOW VARIABLES LIKE '%storage_engine%';

  準備兩張表:用戶表 tbl_user 和用戶登錄記錄表 tbl_user_login_log ,並初始化部分部分數據

-- 表創建與數據初始化
DROP TABLE IF EXISTS tbl_user;
CREATE TABLE tbl_user (
  id INT(11) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '自增主鍵',
  user_name VARCHAR(50) NOT NULL COMMENT '用戶名',
  sex TINYINT(1) NOT NULL COMMENT '性別, 1:男,0:女',
  create_time datetime NOT NULL COMMENT '創建時間',
  update_time datetime NOT NULL COMMENT '更新時間',
    remark VARCHAR(255) NOT NULL DEFAULT '' COMMENT '備註',
  PRIMARY KEY (id)
) COMMENT='用戶表';

DROP TABLE IF EXISTS tbl_user_login_log;
CREATE TABLE tbl_user_login_log (
  id INT(11) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '自增主鍵',
  user_name VARCHAR(50) NOT NULL COMMENT '用戶名',
  ip VARCHAR(15) NOT NULL COMMENT '登錄IP',
  client TINYINT(1) NOT NULL COMMENT '登錄端, 1:android, 2:ios, 3:PC, 4:H5',
  create_time datetime NOT NULL COMMENT '創建時間',
  PRIMARY KEY (id)
) COMMENT='登錄日誌';
INSERT INTO tbl_user(user_name,sex,create_time,update_time,remark) VALUES
('何天香',1,NOW(), NOW(),'朗眉星目,一表人材'),
('薛沉香',0,NOW(), NOW(),'天星樓的總樓主薛搖紅的女兒,也是天星樓的少總樓主,體態豐盈,烏髮飄逸,指若春蔥,袖臂如玉,風姿卓然,高貴典雅,人稱“天星絕香”的武林第一大美女'),
('慕容蘭娟',0,NOW(), NOW(),'武林東南西北四大世家之北世家慕容長明的獨生女兒,生得玲瓏剔透,粉雕玉琢,脾氣卻是剛烈無比,又喜着火紅,所以人送綽號“火鳳凰”,是除天星樓薛沉香之外的武林第二大美女'),
('萇婷',0,NOW(), NOW(),'當今皇上最寵愛的侄女,北王府的郡主,腰肢纖細,遍體羅綺,眉若墨畫,唇點櫻紅;雖無沉香之雅重,蘭娟之熱烈,卻別現出一種空靈'),
('柳含姻',0,NOW(), NOW(),'武林四絕之一的添愁仙子董婉婉的徒弟,體態窈窕,姿容秀麗,真箇是秋水為神玉為骨,芙蓉如面柳如腰,眉若墨畫,唇若點櫻,不弱西子半分,更勝玉環一籌; 搖紅樓、聽雨軒,琵琶一曲值千金!'),
('李凝雪',0,NOW(), NOW(),'李相國的女兒,神采奕奕,英姿颯爽,愛憎分明'),
('周遺夢',0,NOW(), NOW(),'音神傳人,湘妃竹琴的擁有者,雲髻高盤,穿了一身黑色蟬翼紗衫,愈覺得冰肌玉骨,粉面櫻唇,格外嬌艷動人'),
('恭弘=叶 恭弘留痕',0,NOW(), NOW(),'聖域聖女,膚白如雪,白衣飄飄,宛如仙女一般,微笑中帶着說不出的柔和之美'),
('郭疏影',0,NOW(), NOW(),'揚灰右使的徒弟,秀髮細眉,玉肌豐滑,嬌潤脫俗'),
('鍾鈞天',0,NOW(), NOW(),'天界,玄天九部 - 鈞天部的部主,超凡脫俗,仙氣逼人'),
('王雁雲',0,NOW(), NOW(),'塵緣山莊二小姐,刁蠻任性'),
('許侍霜',0,NOW(), NOW(),'藥王穀穀主女兒,醫術高明'),
('馮黯凝',0,NOW(), NOW(),'桃花門門主,嬌艷如火,千嬌百媚');
INSERT INTO tbl_user_login_log(user_name, ip, client, create_time) VALUES
('薛沉香', '10.53.56.78',2, '2019-10-12 12:23:45'),
('萇婷', '10.53.56.78',2, '2019-10-12 22:23:45'),
('慕容蘭娟', '10.53.56.12',1, '2018-08-12 22:23:45'),
('何天香', '10.53.56.12',1, '2019-10-19 10:23:45'),
('柳含姻', '198.11.132.198',2, '2018-05-12 22:23:45'),
('馮黯凝', '198.11.132.198',2, '2018-11-11 22:23:45'),
('周遺夢', '198.11.132.198',2, '2019-06-18 22:23:45'),
('郭疏影', '220.181.38.148',3, '2019-10-21 09:45:56'),
('薛沉香', '220.181.38.148',3, '2019-10-26 22:23:45'),
('萇婷', '104.69.160.60',4, '2019-10-12 10:23:45'),
('王雁雲', '104.69.160.61',4, '2019-10-16 20:23:45'),
('李凝雪', '104.69.160.62',4, '2019-10-17 20:23:45'),
('許侍霜', '104.69.160.63',4, '2019-10-18 20:23:45'),
('恭弘=叶 恭弘留痕', '104.69.160.64',4, '2019-10-19 20:23:45'),
('王雁雲', '104.69.160.65',4, '2019-10-20 20:23:45'),
('恭弘=叶 恭弘留痕', '104.69.160.66',4, '2019-10-21 20:23:45');

SELECT * FROM tbl_user;
SELECT * FROM tbl_user_login_log;

View Code

EXPLAIN 輸出格式概覽

  樓主再不講重點,估計有些看官老爺找他的 2 米長的大砍刀去了

  這麼滴,我們先來看看 EXPLAIN 輸出結果的大概,是不是長得滿臉麻子,讓我們望而生畏 ?

  白白凈凈的,挺好,關鍵長啊! 解釋如下

EXPLAIN 輸出格式詳解

  EXPLAIN 的輸出字段雖然有點多,但常關注的就那麼幾個,但樓主秉着負責的態度,都給大家講一下,需要重點關注的字段,樓主也會標明滴

  EXPLAIN 支持的 SQL 語句有好幾種,但工作中用的最多的還是 SELECT ,所以樓主就偷個懶,以 SELECT 來講解 EXPLAIN,有興趣的老爺去試試其他的

  id

    輸出的是整數,用來標識整個 SQL 的執行順序。id 如果相同,從上往下依次執行id不同;id 值越大,執行優先級越高,越先被執行;如果行引用其他行的並集結果,則該值可以為NULL

    不重要,有所了解就好(其實非常簡單,看一遍基本就能記住了)

  select_type

    查詢的類型,說明如下

    簡單幫大家翻譯一下(有能力的去讀官網,畢竟那是原配,最具權威性)

    SIMPLE:簡單的 SELECT 查詢,沒有 UNION 或者子查詢,包括單表查詢或者多表 JOIN 查詢

    PRIMARY: 最外層的 select 查詢,常見於子查詢或 UNION 查詢 ,最外層的查詢被標識為 PRIMARY

    UNION:UNION 操作的第二個或之後的 SELECT,不依賴於外部查詢的結果集(外部查詢指的就是 PRIMARY 對應的 SELECT

    DEPENDENT UNION:UNION 操作的第二個或之後的 SELECT,依賴於外部查詢的結果集

    UNION RESULT:UNION 的結果(如果是 UNION ALL 則無此結果)

    SUBQUERY:子查詢中的第一個 SELECT 查詢,不依賴於外部查詢的結果集

    DEPENDENT SUBQUERY:子查詢中的第一個select查詢,依賴於外部查詢的結果集

    DERIVED:派生表(臨時表),常見於 FROM 子句中有子查詢的情況

      注意:MySQL5.7 中對 Derived table 做了一個新特性,該特性允許將符合條件的 Derived table 中的子表與父查詢的表合併進行直接JOIN,從而簡化簡化了執行計劃,同時也提高了執行效率;默認情況下,MySQL5.7 中這個特性是開啟的,所以默認情況下,上面的 SQL 的執行計劃應該是這樣的

      可通過 SET SESSION optimizer_switch=derived_merge=on|off 來開啟或關閉當前 SESSION 的該特性。貌似扯的有點遠了(樓主你是不是在隨性發揮?),更多詳情可以去查閱

    MATERIALIZED:被物化的子查詢,MySQL5.6 引入的一種新的 select_type,主要是優化 FROM 或 IN 子句中的子查詢,更多詳情請查看:

    UNCACHEABLE SUBQUERY:對於外層的主表,子查詢不可被緩存,每次都需要計算

    UNCACHEABLE UNION:類似於 UNCACHEABLE SUBQUERY,只是出現在 UNION 操作中

    SIMPLLE、PRIMARY、SUBQUERY、DERIVED 這 4 個在實際工作中碰到的會比較多,看得懂這 4 個就行了,至於其他的,碰到了再去查資料就好了(我也想全部記住,但用的少,太容易忘記了,我也很無賴呀)

  table

    显示了對應行正在訪問哪個表(有別名就显示別名),還會有 <union2,3> 、 <subquery2> 、 <derived2> (這裏的 2,3、2、2 指的是 id 列的值)類似的值,具體可以往上看,這裏就不演示了(再演示就太長了,你們都看不下去了,那我不是白忙乎了 ?)

  partitions

    查詢進行匹配的分區,對於非分區表,該值為NULL。大多數情況下用不到分區,所以這一列我們無需關注

  type

    關聯類型或者訪問類型,它指明了 MySQL 決定如何查找表中符合條件的行,這是我們判斷查詢是否高效的重要依據(type 之於 EXPLAIN,就好比三圍之於女人!),完整介紹請看:

    其值有多種,我們以性能好到性能差的順序一個一個來看     

    system

      該表只有一行(=系統表),是 const 類型的特例
    const

      確定只有一行匹配的時候,mysql 優化器會在查詢前讀取它並且只讀取一次,速度非常快。用於 primary key 或 unique 索引中有常亮值比較的情形

    eq_ref

      對於每個來自於前面的表的行,從該表最多只返回一條符合條件的記錄。當連接使用的索引是 PRIMARY KEY 或 UNIQUE NOT NULL 索引時使用,非常高效

    ref

      索引訪問,也稱索引查找,它返回所有匹配某個單個值的行。此類型通常出現在多表的 JOIN 查詢, 針對於非 UNIQUE 或非 PRIMARY KEY, 或者是使用了最左前綴規則索引的查詢,換句話說,如果 JOIN 不能基於關鍵字選擇單個行的話,則使用ref

    fulltext

      當使用全文索引時會用到,這種索引一般用不到,會用專門的搜索服務(solr、elasticsearch等)來替代
    ref_or_null

      類似ref,但是添加了可以專門搜索 NULL 的行

      這個是有前提條件的,前提為 weapon 列有索引,且 weapon 列存在  NULL 

    index_merge

      該訪問類型使用了索引合併優化方法

      這個同樣也是有條件的, id 列和 weapon 列都有單列索引。如果出現 index_merge,並且這類 SQL 後期使用較頻繁,可以考慮把單列索引換為組合索引,這樣效率更高

    unique_subquery

      類似於兩表連接中被驅動表的 eq_ref 訪問方式,unique_subquery 是針對在一些包含 IN 子查詢的查詢語句中,如果查詢優化器決定將 IN 子查詢轉換為 EXISTS 子查詢,而且子查詢可以使用到主鍵或者唯一索引進行等值匹配時,則會使用 unique_subquery

    index_subquery

      index_subquery 與 unique_subquery類似,只不過訪問子查詢中的表時使用的是普通的索引

    range

      使用索引來檢索給定範圍的行,當使用 =、<>、>、>=、<、<=、IS NULL、<=>、BETWEEN 或者 IN 操作符,用常量比較關鍵字列時,則會使用 rang

      前提是必須基於索引,也就是 id 上必須有索引

    index

      當我們可以使用索引覆蓋,但需要掃描全部的索引記錄時,則會使用 index;進行統計時非常常見

    ALL

      我們熟悉的全表掃描

  possible_keys

    展示在這個 SQL 中,可能用到的索引有哪些,但不一定在查詢時使用。若為空則表示沒有可以使用的索引,此時可以通過檢查 WHERE 語句看是否可以引用某些列或者新建索引來提高性能

  key

    展示這個 SQL 實際使用的索引,如果沒有選擇索引,則此列為null,要想強制 MySQL 使用或忽視 possible_keys 列中的索引,在查詢中使用 FORCE INDEX、USE INDEX 或者I GNORE INDEX

  key_len

    展示 MySQL 決定使用的鍵長度(字節數)。如果 key 是 NULL,則長度為 NULL。在不損失精確性的情況下,長度越短越好

  ref

    展示的是與索引列作等值匹配的東東是個啥,比如只是一個常數或者是某個列。它显示的列的名字(或const),此列多數時候為 Null

  rows

    展示的是 mysql 解析器認為執行此 SQL 時預計需要掃描的行數。此數值為一個預估值,不是具體值,通常比實際值小

  filtered

    展示的是返回結果的行數所佔需要讀到的行(rows 的值)的比例,當然是越小越好啦

  extra

    表示不在其他列但也很重要的額外信息。取值有很多,我們挑一些比較常見的過一下

    using index

      表示 SQL 使用了使用覆蓋索引,而不用回表去查詢數據,性能非常不錯

    using where

      表示存儲引擎搜到記錄後進行了後過濾(POST-FILTER),如果查詢未能使用索引,using where 的作用只是提醒我們 mysql 要用 where 條件過濾結果集

    using temporary

      表示 mysql 需要使用臨時表來存儲結果集,常見於排序和分組查詢

    using filesort

      表示 mysql 無法利用索引直接完成排序(排序的字段不是索引字段),此時會用到緩衝空間(內存或者磁盤)來進行排序;一般出現該值,則表示 SQL 要進行優化了,它對 CPU 的消耗是比較大的

    impossible where

      查詢語句的WHERE子句永遠為 FALSE 時將會提示該額外信息

 

 

     當然還有其他的,不常見,等碰到了大家再去查吧(現在凌晨 1 點,我實在是太困了!)

總結

  1、背景疑問

    還記得客服小姐姐的問題嗎,她嫌我們太慢,具體原因下篇再詳細介紹,這裏就提一下:連表查詢的 連接鍵 類型不一致,一個 INT 類型,一個 VARCHAR 類型,導致 type 是 ALL(這誰設計的呀,坑死人呀! 難道是我 ?)

  2、思維導圖

    本來是想自己畫個思維導圖的,可上網一搜,發現了一個人家畫好了的思維導圖,我就偷個懶借用下:,裏面描述的很詳細,同時也包括了各種示例,真香!

  3、肚中精華

    EXPLAIN 的輸出內容很多,我們沒必要全部掌握,重點我已經幫大家划好

    type,就像 RMB 一樣重要

    key,也像 RMB 一樣重要

    extra,還像 RMB 一樣重要

    說白了還是 RMB 最重要,不是,我的意思是 type、key、extra 都很重要,其他的用到了再去買吧

  4、示例代碼

    

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理【其他文章推薦】

USB CONNECTOR掌控什麼技術要點? 帶您認識其相關發展及效能

※評比前十大台北網頁設計台北網站設計公司知名案例作品心得分享

※智慧手機時代的來臨,RWD網頁設計已成為網頁設計推薦首選