mybatis精講(三)–標籤及TypeHandler使用

目錄

話引

  • 前兩張我們分別介紹了Mybatis環境搭建及其組件的生命周期。這些都是我們Mybatis入門必備技能。有了前兩篇的鋪墊我們今天就來深入下Mybatis, 也為了填下之前埋下的坑。

XML配置標籤

概覽


<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
        PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-config.dtd">

<configuration> 
    <!--引入外部配置文件-->
    <properties resource=""/>
    <!--設置-->
    <settings/>
    <!--定義別名-->
    <typeAliases>
        <package name=""/>
    </typeAliases>
    <!--類型處理器-->
    <typeHandlers/>
    <!--對象工廠-->
    <objectFactory/>
    <!--插件-->
    <plugins/>
    <!--定義數據庫信息,默認使用development數據庫構建環境-->
    <environments default="development">
        <environment id="development">
            <!--jdbc事物管理-->
            <transactionManager type="JDBC"/>
            <!--配置數據庫連接信息-->
            <dataSource type="POOLED"/>
        </environment>
    </environments>
    <!--數據庫廠商標識-->
    <databaseIdProvider/>
    <mappers/>
</configuration>

  • 上面模板列出了所有xml可以配置的屬性。這裏plugins是一個讓人哭笑不得的東西。用的好是利器,用的不好就是埋坑。接下來我們來看看各個屬性的作用

properties

  • 該標籤的作用就是引入變量。和maven的properties一樣。在這裏定義的變量或者引入的變量,在下面我們是可以童工${}使用的。

子標籤property


<properties>
  <property name="zxhtom" value="jdbc:mysql://localhost:3306/mybatis"/>
</properties>

<dataSource type="POOLED">
<property name="driver" value="${zxhtom}"/>
<dataSource>
  • 上述的配置就可以直接使用zxhtom這個變量。

resource

  • 除了上述方法我們還可以通過引入其他properties文件,就可以使用文件里的配置變量了。

<properties resource="mybatis.properties"/>

程序注入

  • 最後還有一種我們在構建SqlSessionFactory的時候重新載入我們的Properties對象就可以了。另外三者的優先級是從低到高

settings


configuration.setAutoMappingBehavior(AutoMappingBehavior.valueOf(props.getProperty("autoMappingBehavior", "PARTIAL")));
configuration.setAutoMappingUnknownColumnBehavior(AutoMappingUnknownColumnBehavior.valueOf(props.getProperty("autoMappingUnknownColumnBehavior", "NONE")));
configuration.setCacheEnabled(booleanValueOf(props.getProperty("cacheEnabled"), true));
configuration.setProxyFactory((ProxyFactory) createInstance(props.getProperty("proxyFactory")));
configuration.setLazyLoadingEnabled(booleanValueOf(props.getProperty("lazyLoadingEnabled"), false));
configuration.setAggressiveLazyLoading(booleanValueOf(props.getProperty("aggressiveLazyLoading"), false));
configuration.setMultipleResultSetsEnabled(booleanValueOf(props.getProperty("multipleResultSetsEnabled"), true));
configuration.setUseColumnLabel(booleanValueOf(props.getProperty("useColumnLabel"), true));
configuration.setUseGeneratedKeys(booleanValueOf(props.getProperty("useGeneratedKeys"), false));
configuration.setDefaultExecutorType(ExecutorType.valueOf(props.getProperty("defaultExecutorType", "SIMPLE")));
configuration.setDefaultStatementTimeout(integerValueOf(props.getProperty("defaultStatementTimeout"), null));
configuration.setDefaultFetchSize(integerValueOf(props.getProperty("defaultFetchSize"), null));
configuration.setMapUnderscoreToCamelCase(booleanValueOf(props.getProperty("mapUnderscoreToCamelCase"), false));
configuration.setSafeRowBoundsEnabled(booleanValueOf(props.getProperty("safeRowBoundsEnabled"), false));
configuration.setLocalCacheScope(LocalCacheScope.valueOf(props.getProperty("localCacheScope", "SESSION")));
configuration.setJdbcTypeForNull(JdbcType.valueOf(props.getProperty("jdbcTypeForNull", "OTHER")));
configuration.setLazyLoadTriggerMethods(stringSetValueOf(props.getProperty("lazyLoadTriggerMethods"), "equals,clone,hashCode,toString"));
configuration.setSafeResultHandlerEnabled(booleanValueOf(props.getProperty("safeResultHandlerEnabled"), true));
configuration.setDefaultScriptingLanguage(resolveClass(props.getProperty("defaultScriptingLanguage")));
configuration.setDefaultEnumTypeHandler(resolveClass(props.getProperty("defaultEnumTypeHandler")));
configuration.setCallSettersOnNulls(booleanValueOf(props.getProperty("callSettersOnNulls"), false));
configuration.setUseActualParamName(booleanValueOf(props.getProperty("useActualParamName"), true));
configuration.setReturnInstanceForEmptyRow(booleanValueOf(props.getProperty("returnInstanceForEmptyRow"), false));
configuration.setLogPrefix(props.getProperty("logPrefix"));
configuration.setConfigurationFactory(resolveClass(props.getProperty("configurationFactory")));
  • 上面代碼是我們在XMLConfigBuilder解析settings標籤的代碼。從這段代碼中我們了解到settings子標籤。
參數 功能 可選值 默認值
autoMappingBehavior 指定Mybatis應如何自動映射列到字段上。
NONE : 表示取消自動映射
PARTIAL:只會自動映射沒有定義嵌套結果集映射的結果集
FULL : 自動映射任意複雜的結果集
NONE、PARTIAL、FULL PARTIAL
autoMappingUnknownColumnBehavior 指定識別到位置列或屬性的時間
NONE : 什麼都不做
WARNING:日誌會報警(前提是日誌設置了显示權限)
FAILING : 拋出異常。
NONE, WARNING, FAILING NONE
cacheEnabled 該配置影響的所有映射器中配置的緩存的全局開關 true|false true
proxyFactory 指定Mybatis創建具有延遲加載能力的對象所用到的代理工具未指定時將自動查找 SLF4J | LOG4J | LOG4J2 | JDK_LOGGING | COMMONS_LOGGING | STDOUT_LOGGING | NO_LOGGING not set
lazyLoadingEnabled 延時加載全局開關
開啟時:級聯對象會延時加載;級聯標籤中可以通過fetchType來定製覆蓋此選項
true|false false
aggressiveLazyLoading 啟用時:對任意延遲屬性的調用會使帶有延遲加載屬性的對象分層性質完整加載,反之按需加載 true|false true
multipleResultSetsEnabled 是否允許單一語句返回多結果集 true|false true
useColumnLabel 確切的說當映射找不到參數時會使用列標籤(數據庫列名)代替別名去映射 true|false true
useGeneratedKeys 允許 JDBC 支持自動生成主鍵,需要驅動兼容。 如果設置為 true 則這個設置強制使用自動生成主鍵,儘管一些驅動不能兼容但仍可正常工作(比如 Derby) true|false false
defaultExecutorType 配置默認的執行器。SIMPLE 就是普通的執行器;REUSE 執行器會重用預處理語句(prepared statements); BATCH 執行器將重用語句並執行批量更新 SIMPLE REUSE BATCH SIMPLE
defaultStatementTimeout 設置超時時間,決定驅動等待數據庫響應的秒數 整數 null
defaultFetchSize 設置數據庫resultSet讀取數據方式,默認全部加載進內存,設置該屬性可以設置一次性讀多少條數據進內存 整數 null
mapUnderscoreToCamelCase 是否開啟自動駝峰命名規則(camel case)映射,即從經典數據庫列名 A_COLUMN 到經典 Java 屬性名 aColumn 的類似映射。 true|false false
safeRowBoundsEnabled -允許在嵌套語句中使用分頁 true|false false
localCacheScope 一級緩存。mybatis默認對同一個sqlsession中數據是共享的。一個sqlsession調用兩次相同查詢實際只會查詢一次。就是因為該屬性為SESSION , STATEMENT則針對的是每一條sql SESSION|STATEMENT SESSION
jdbcTypeForNull 當沒有為參數提供特定的jdbc類型時,為空值則指定JDBC類型。在新增時我們沒有設置參數,這個時候就會根據此參數天長。加入設置VARCHAR,那麼我們新增的數據沒傳參數則為空字符 NULL|VARCHAR|OTHER OTHER
lazyLoadTriggerMethods 指定具體方法延時加載 方法 equals,clone,hashCode,toString
safeResultHandlerEnabled 允許在嵌套語句中使用分頁 true|false true
defaultScriptingLanguage 動態SQL生成的默認語言 org.apache.ibatis.scripting.xmltags.XMLDynamicLanguageDriver
defaultEnumTypeHandler mybatis默認的枚舉處理類
callSettersOnNulls 指定當結果集中值為null的時候是否調用映射對象的setter(put)方法。
useActualParamName 允許使用他們的編譯后名稱來映射,3.4.2后默認true.在xml中#{0}則報錯。設置為false,則#{0}代表第一個參數#{n}第n個 true|false true
returnInstanceForEmptyRow 當返回行的所有列都是空時,MyBatis默認返回 null。 當開啟這個設置時,MyBatis會返回一個空實例。 請注意,它也適用於嵌套的結果集 (如集合或關聯)。(新增於 3.4.2) true|false false
logPrefix 指定 MyBatis 增加到日誌名稱的前綴。

別名

  • 別名是mybatis為我們項目中類起的一個名字,類名往往會很長所以別名就方便我們平時的開發。Mybatis為我們內置了一些類的別名:byte、short、int、long、float、double、boolean、char等基礎類型的別名。還有其的封裝類型、String,Object,Map,List等等常用的類。
    org.apache.ibatis.type.TypeAliasRegistry這個類中幫我們內置了別名。可以看下。自定義別名也是通過這個類進行註冊的。我們可以通過settings中typeAliases配置的方式結合@Alias。或者掃描包也可以的。

TypeHandler

  • 這個接口就四個方法

public interface TypeHandler<T> {

  /**
   * 設置參數是用到的方法
   */
  void setParameter(PreparedStatement ps, int i, T parameter, JdbcType jdbcType) throws SQLException;

  T getResult(ResultSet rs, String columnName) throws SQLException;

  T getResult(ResultSet rs, int columnIndex) throws SQLException;

  T getResult(CallableStatement cs, int columnIndex) throws SQLException;

}
  • 可以理解成攔截器。它主要攔截的是設置參數和獲取結果的兩個節點。這個類的作用就是將Java對象和jdbcType進行相互轉換的一個功能。同樣的在org.apache.ibatis.type.TypeHandlerRegistry這個類中mybatis為我們提供了內置的TypeHandler。基本上是對於基本數據和分裝對象的轉換。
  • 下面我們隨便看一個TypeHandler處理細節
public class BooleanTypeHandler extends BaseTypeHandler<Boolean> {

  @Override
  public void setNonNullParameter(PreparedStatement ps, int i, Boolean parameter, JdbcType jdbcType)
      throws SQLException {
    ps.setBoolean(i, parameter);
  }

  @Override
  public Boolean getNullableResult(ResultSet rs, String columnName)
      throws SQLException {
    boolean result = rs.getBoolean(columnName);
    return !result && rs.wasNull() ? null : result;
  }

  @Override
  public Boolean getNullableResult(ResultSet rs, int columnIndex)
      throws SQLException {
    boolean result = rs.getBoolean(columnIndex);
    return !result && rs.wasNull() ? null : result;
  }

  @Override
  public Boolean getNullableResult(CallableStatement cs, int columnIndex)
      throws SQLException {
    boolean result = cs.getBoolean(columnIndex);
    return !result && cs.wasNull() ? null : result;
  }
}
  • setParameter是PreparedStatement進行設置成boolean類型。getResult分別通過三種不同方式獲取。在這些方法里我們可以根據自己也無需求進行控制。常見的控制是枚舉的轉換。傳遞參數過程可能是枚舉的name,但是傳遞到數據庫中要枚舉的index.這種需求我們就可以在TypeHandler中實現。我們書寫的typeHandler之後並不能被識別,還需要我們在resultMap中的result標籤中通過typeHandler指定我們的自定義Handler.

自定義TypeHandler

  • 承接上文我們說道枚舉的轉換。下面我們還是已學生類為例。學生中性別之前是boolean類型。現在我們採用枚舉類型。但是數據庫中存的還是數據,01.

EnumTypeHandler

  • 在TypeHandlerRegister類中申明了默認的枚舉類處理器是private Class<? extends TypeHandler> defaultEnumTypeHandler = EnumTypeHandler.class;
@Override
public E getNullableResult(ResultSet rs, String columnName) throws SQLException {
  String s = rs.getString(columnName);
  return s == null ? null : Enum.valueOf(type, s);
}
  • 我們通過這個方法可以看出,這個枚舉處理器適合已枚舉名稱存儲的方式

EnumOrdinalTypeHandler

  • 在Enum中還有一個屬性oridinal。這個表示枚舉中的索引。然後我們通過查看Mybatis提供的處理器發現有個叫EnumOrdinalTypeHandler。我們很容易聯想到的就是這個處理器是通過枚舉的所以作為數據庫內容的。在SexEnum中MALE存儲到數據庫中則為0.注意這個0不是我們的index.而是MALE的索引。如果將MALE和FEMAEL調換。那麼MALE索引則為1.

  • 因為默認的是EnumTypeHandler。所以想用EnumOrdinalTypeHandler的話我們要麼在resultMap中sex字段指定該處理器。要不就通過配置文件typeHandlers註冊進來。(將處理器與Java類進行綁定。mybatis遇到這個Java對象的時候就知道用什麼處理器處理)


<typeHandlers>
    <typeHandler handler="org.apache.ibatis.type.EnumOrdinalTypeHandler" javaType="com.github.zxhtom.enums.SexEnum"/>
</typeHandlers>

SexTypeHandler

  • 上面的不管是通過名稱存儲還是通過索引存儲都不太滿足我們的需求。我們想通過我們的index存儲。那麼這時候我們就得自定義處理邏輯了。Mybatis處理器都是繼承BaseTypeHandler。因為BaseTypeHandler實現了TypeHandler.所以我們這裏也就繼承BaseTypeHandler。

public class SexTypeHandler extends BaseTypeHandler<SexEnum> {


    @Override
    public void setNonNullParameter(PreparedStatement ps, int i, SexEnum parameter, JdbcType jdbcType) throws SQLException {
        ps.setInt(i,parameter.getIndex());
    }

    @Override
    public SexEnum getNullableResult(ResultSet rs, String columnName) throws SQLException {
        int i = rs.getInt(columnName);
        return SexEnum.getSexEnum(i);
    }

    @Override
    public SexEnum getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
        int i = rs.getInt(columnIndex);
        return SexEnum.getSexEnum(i);
    }

    @Override
    public SexEnum getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
        int i = cs.getInt(columnIndex);
        return SexEnum.getSexEnum(i);
    }
}

typeHandler注意點

  • 在編寫自定義處理器的時候我們得之處Javatype、jdbctype。兩者不是必填。但至少得有一個。正常我們默認javatype是必填的。
  • 填寫的方式有三種
    • 通過MappedTypes、MappedJdbcTypes分別指定javatype、jdbctype
    • 通過在mybatis-config.xml中配置typeHandlers進行註解。裏面也有這兩個屬性的配置。
    • 通過在mapper.xml的resultmap中再次指定某個字段的typehandler.
  • TypeHandler為我們提供了Java到jdbc數據的轉換橋樑。極大的方便了我們平時的開發。讓我們開發期間忽略數據的轉換這麼糟心的事情。

# 加入戰隊

微信公眾號

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

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

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

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

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

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

Spring Boot 2.X(十八):集成 Spring Security-登錄認證和權限控制

前言

在企業項目開發中,對系統的安全和權限控制往往是必需的,常見的安全框架有 Spring Security、Apache Shiro 等。本文主要簡單介紹一下 Spring Security,再通過 Spring Boot 集成開一個簡單的示例。

Spring Security

什麼是 Spring Security?

Spring Security 是一種基於 Spring AOP 和 Servlet 過濾器 Filter 的安全框架,它提供了全面的安全解決方案,提供在 Web 請求和方法調用級別的用戶鑒權和權限控制。

Web 應用的安全性通常包括兩方面:用戶認證(Authentication)和用戶授權(Authorization)。

用戶認證指的是驗證某個用戶是否為系統合法用戶,也就是說用戶能否訪問該系統。用戶認證一般要求用戶提供用戶名和密碼,系統通過校驗用戶名和密碼來完成認證。

用戶授權指的是驗證某個用戶是否有權限執行某個操作。

2.原理

Spring Security 功能的實現主要是靠一系列的過濾器鏈相互配合來完成的。以下是項目啟動時打印的默認安全過濾器鏈(集成5.2.0):

[
    org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter@5054e546,
    org.springframework.security.web.context.SecurityContextPersistenceFilter@7b0c69a6,
    org.springframework.security.web.header.HeaderWriterFilter@4fefa770,
    org.springframework.security.web.csrf.CsrfFilter@6346aba8,
    org.springframework.security.web.authentication.logout.LogoutFilter@677ac054,
    org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter@51430781,
    org.springframework.security.web.savedrequest.RequestCacheAwareFilter@4203d678,
    org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter@625e20e6,
    org.springframework.security.web.authentication.AnonymousAuthenticationFilter@19628fc2,
    org.springframework.security.web.session.SessionManagementFilter@471f8a70,
    org.springframework.security.web.access.ExceptionTranslationFilter@3e1eb569,
    org.springframework.security.web.access.intercept.FilterSecurityInterceptor@3089ab62
]
  • WebAsyncManagerIntegrationFilter
  • SecurityContextPersistenceFilter
  • HeaderWriterFilter
  • CsrfFilter
  • LogoutFilter
  • UsernamePasswordAuthenticationFilter
  • RequestCacheAwareFilter
  • SecurityContextHolderAwareRequestFilter
  • AnonymousAuthenticationFilter
  • SessionManagementFilter
  • ExceptionTranslationFilter
  • FilterSecurityInterceptor

詳細解讀可以參考:https://blog.csdn.net/dushiwodecuo/article/details/78913113

3.核心組件

SecurityContextHolder

用於存儲應用程序安全上下文(Spring Context)的詳細信息,如當前操作的用戶對象信息、認證狀態、角色權限信息等。默認情況下,SecurityContextHolder 會使用 ThreadLocal 來存儲這些信息,意味着安全上下文始終可用於同一執行線程中的方法。

獲取有關當前用戶的信息

因為身份信息與線程是綁定的,所以可以在程序的任何地方使用靜態方法獲取用戶信息。例如獲取當前經過身份驗證的用戶的名稱,代碼如下:

Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal();
if (principal instanceof UserDetails) {
    String username = ((UserDetails)principal).getUsername();
} else {
    String username = principal.toString();
}

其中,getAuthentication() 返回認證信息,getPrincipal() 返回身份信息,UserDetails 是對用戶信息的封裝類。

Authentication

認證信息接口,集成了 Principal 類。該接口中方法如下:

接口方法 功能說明
getAuthorities() 獲取權限信息列表,默認是 GrantedAuthority 接口的一些實現類,通常是代表權限信息的一系列字符串
getCredentials() 獲取用戶提交的密碼憑證,用戶輸入的密碼字符竄,在認證過後通常會被移除,用於保障安全
getDetails() 獲取用戶詳細信息,用於記錄 ip、sessionid、證書序列號等值
getPrincipal() 獲取用戶身份信息,大部分情況下返回的是 UserDetails 接口的實現類,是框架中最常用的接口之一

AuthenticationManager

認證管理器,負責驗證。認證成功后,AuthenticationManager 返回一個填充了用戶認證信息(包括權限信息、身份信息、詳細信息等,但密碼通常會被移除)的 Authentication 實例。然後再將 Authentication 設置到 SecurityContextHolder 容器中。

AuthenticationManager 接口是認證相關的核心接口,也是發起認證的入口。但它一般不直接認證,其常用實現類 ProviderManager 內部會維護一個 List<AuthenticationProvider> 列表,存放里多種認證方式,默認情況下,只需要通過一個 AuthenticationProvider 的認證,就可被認為是登錄成功。

UserDetailsService

負責從特定的地方加載用戶信息,通常是通過JdbcDaoImpl從數據庫加載實現,也可以通過內存映射InMemoryDaoImpl實現。

UserDetails

該接口代表了最詳細的用戶信息。該接口中方法如下:

接口方法 功能說明
getAuthorities() 獲取授予用戶的權限
getPassword() 獲取用戶正確的密碼,這個密碼在驗證時會和 Authentication 中的 getCredentials() 做比對
getUsername() 獲取用於驗證的用戶名
isAccountNonExpired() 指示用戶的帳戶是否已過期,無法驗證過期的用戶
isAccountNonLocked() 指示用戶的賬號是否被鎖定,無法驗證被鎖定的用戶
isCredentialsNonExpired() 指示用戶的憑據(密碼)是否已過期,無法驗證憑證過期的用戶
isEnabled() 指示用戶是否被啟用,無法驗證被禁用的用戶

Spring Security 實戰

1.系統設計

本文主要使用 Spring Security 來實現系統頁面的權限控制和安全認證,本示例不做詳細的數據增刪改查,sql 可以在完整代碼里下載,主要是基於數據庫對頁面 和 ajax 請求做權限控制。

1.1 技術棧

  • 編程語言:Java
  • 編程框架:Spring、Spring MVC、Spring Boot
  • ORM 框架:MyBatis
  • 視圖模板引擎:Thymeleaf
  • 安全框架:Spring Security(5.2.0)
  • 數據庫:MySQL
  • 前端:Layui、JQuery

1.2 功能設計

  1. 實現登錄、退出
  2. 實現菜單 url 跳轉的權限控制
  3. 實現按鈕 ajax 請求的權限控制
  4. 防止跨站請求偽造(CSRF)攻擊

1.3 數據庫層設計

t_user 用戶表

字段 類型 長度 是否為空 說明
id int 8 主鍵,自增長
username varchar 20 用戶名
password varchar 255 密碼

t_role 角色表

字段 類型 長度 是否為空 說明
id int 8 主鍵,自增長
role_name varchar 20 角色名稱

t_menu 菜單表

字段 類型 長度 是否為空 說明
id int 8 主鍵,自增長
menu_name varchar 20 菜單名稱
menu_url varchar 50 菜單url(Controller 請求路徑)

t_user_roles 用戶權限表

字段 類型 長度 是否為空 說明
id int 8 主鍵,自增長
user_id int 8 用戶表id
role_id int 8 角色表id

t_role_menus 權限菜單表

字段 類型 長度 是否為空 說明
id int 8 主鍵,自增長
role_id int 8 角色表id
menu_id int 8 菜單表id

實體類這裏不詳細列了。

2.代碼實現

2.0 相關依賴

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

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
            <exclusions>
                <exclusion>
                    <groupId>org.junit.vintage</groupId>
                    <artifactId>junit-vintage-engine</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-jdbc</artifactId>
        </dependency>

        <!-- 熱部署模塊 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <optional>true</optional> <!-- 這個需要為 true 熱部署才有效 -->
        </dependency>
        
            <!-- mysql 數據庫驅動. -->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
        </dependency>

        <!-- mybaits -->
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>2.1.0</version>
        </dependency>
        
        <!-- thymeleaf -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>
        
        <!-- alibaba fastjson -->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.47</version>
        </dependency>
        <!-- spring security -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
    </dependencies>

2.1 繼承 WebSecurityConfigurerAdapter 自定義 Spring Security 配置

/**
prePostEnabled :決定Spring Security的前註解是否可用 [@PreAuthorize,@PostAuthorize,..]
secureEnabled : 決定是否Spring Security的保障註解 [@Secured] 是否可用
jsr250Enabled :決定 JSR-250 annotations 註解[@RolesAllowed..] 是否可用.
 */
@Configurable
@EnableWebSecurity
//開啟 Spring Security 方法級安全註解 @EnableGlobalMethodSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true,securedEnabled = true,jsr250Enabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter{

    @Autowired
    private CustomAccessDeniedHandler customAccessDeniedHandler;
    @Autowired
    private UserDetailsService userDetailsService;
    
    /**
     * 靜態資源設置
     */
    @Override
    public void configure(WebSecurity webSecurity) {
        //不攔截靜態資源,所有用戶均可訪問的資源
        webSecurity.ignoring().antMatchers(
                "/",
                "/css/**",
                "/js/**",
                "/images/**",
                "/layui/**"
                );
    }
    /**
     * http請求設置
     */
    @Override
    public void configure(HttpSecurity http) throws Exception {
        //http.csrf().disable(); //註釋就是使用 csrf 功能       
        http.headers().frameOptions().disable();//解決 in a frame because it set 'X-Frame-Options' to 'DENY' 問題           
        //http.anonymous().disable();
        http.authorizeRequests()
            .antMatchers("/login/**","/initUserData","/main")//不攔截登錄相關方法        
            .permitAll()        
            //.antMatchers("/user").hasRole("ADMIN")  // user接口只有ADMIN角色的可以訪問
//          .anyRequest()
//          .authenticated()// 任何尚未匹配的URL只需要驗證用戶即可訪問
            .anyRequest()
            .access("@rbacPermission.hasPermission(request, authentication)")//根據賬號權限訪問         
            .and()
            .formLogin()
            .loginPage("/")
            .loginPage("/login")   //登錄請求頁
            .loginProcessingUrl("/login")  //登錄POST請求路徑
            .usernameParameter("username") //登錄用戶名參數
            .passwordParameter("password") //登錄密碼參數
            .defaultSuccessUrl("/main")   //默認登錄成功頁面
            .and()
            .exceptionHandling()
            .accessDeniedHandler(customAccessDeniedHandler) //無權限處理器
            .and()
            .logout()
            .logoutSuccessUrl("/login?logout");  //退出登錄成功URL
            
    }
    /**
     * 自定義獲取用戶信息接口
     */
    @Override
    public void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
    }
    
    /**
     * 密碼加密算法
     * @return
     */
    @Bean
    public BCryptPasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
 
    }
}

2.2 自定義實現 UserDetails 接口,擴展屬性

public class UserEntity implements UserDetails {

    /**
     * 
     */
    private static final long serialVersionUID = -9005214545793249372L;

    private Long id;// 用戶id
    private String username;// 用戶名
    private String password;// 密碼
    private List<Role> userRoles;// 用戶權限集合
    private List<Menu> roleMenus;// 角色菜單集合

    private Collection<? extends GrantedAuthority> authorities;
    public UserEntity() {
        
    }
    
    public UserEntity(String username, String password, Collection<? extends GrantedAuthority> authorities,
            List<Menu> roleMenus) {
        this.username = username;
        this.password = password;
        this.authorities = authorities;
        this.roleMenus = roleMenus;
    }

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    public List<Role> getUserRoles() {
        return userRoles;
    }

    public void setUserRoles(List<Role> userRoles) {
        this.userRoles = userRoles;
    }

    public List<Menu> getRoleMenus() {
        return roleMenus;
    }

    public void setRoleMenus(List<Menu> roleMenus) {
        this.roleMenus = roleMenus;
    }

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return this.authorities;
    }

    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    @Override
    public boolean isEnabled() {
        return true;
    }

}

2.3 自定義實現 UserDetailsService 接口

/**
 * 獲取用戶相關信息
 * @author charlie
 *
 */
@Service
public class UserDetailServiceImpl implements UserDetailsService {
    private Logger log = LoggerFactory.getLogger(UserDetailServiceImpl.class);

    @Autowired
    private UserDao userDao;

    @Autowired
    private RoleDao roleDao;
    @Autowired
    private MenuDao menuDao;

    @Override
    public UserEntity loadUserByUsername(String username) throws UsernameNotFoundException {
        // 根據用戶名查找用戶
        UserEntity user = userDao.getUserByUsername(username);
        System.out.println(user);
        if (user != null) {
            System.out.println("UserDetailsService");
            //根據用戶id獲取用戶角色
            List<Role> roles = roleDao.getUserRoleByUserId(user.getId());
            // 填充權限
            Collection<SimpleGrantedAuthority> authorities = new HashSet<SimpleGrantedAuthority>();
            for (Role role : roles) {
                authorities.add(new SimpleGrantedAuthority(role.getRoleName()));
            }
            //填充權限菜單
            List<Menu> menus=menuDao.getRoleMenuByRoles(roles);
            return new UserEntity(username,user.getPassword(),authorities,menus);
        } else {
            System.out.println(username +" not found");
            throw new UsernameNotFoundException(username +" not found");
        }       
    }

}

2.4 自定義實現 URL 權限控制

/**
 * RBAC數據模型控制權限
 * @author charlie
 *
 */
@Component("rbacPermission")
public class RbacPermission{

    private AntPathMatcher antPathMatcher = new AntPathMatcher();

    public boolean hasPermission(HttpServletRequest request, Authentication authentication) {
        Object principal = authentication.getPrincipal();
        boolean hasPermission = false;
        // 讀取用戶所擁有的權限菜單
        List<Menu> menus = ((UserEntity) principal).getRoleMenus();
        System.out.println(menus.size());
        for (Menu menu : menus) {
            if (antPathMatcher.match(menu.getMenuUrl(), request.getRequestURI())) {
                hasPermission = true;
                break;
            }
        }
        return hasPermission;
    }
}

2.5 實現 AccessDeniedHandler

自定義處理無權請求

/**
 * 處理無權請求
 * @author charlie
 *
 */
@Component
public class CustomAccessDeniedHandler implements AccessDeniedHandler {

    private Logger log = LoggerFactory.getLogger(CustomAccessDeniedHandler.class);

    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response,
            AccessDeniedException accessDeniedException) throws IOException, ServletException {
        boolean isAjax = ControllerTools.isAjaxRequest(request);
        System.out.println("CustomAccessDeniedHandler handle");
        if (!response.isCommitted()) {
            if (isAjax) {
                String msg = accessDeniedException.getMessage();
                log.info("accessDeniedException.message:" + msg);
                String accessDenyMsg = "{\"code\":\"403\",\"msg\":\"沒有權限\"}";
                ControllerTools.print(response, accessDenyMsg);
            } else {
                request.setAttribute(WebAttributes.ACCESS_DENIED_403, accessDeniedException);
                response.setStatus(HttpStatus.FORBIDDEN.value());
                RequestDispatcher dispatcher = request.getRequestDispatcher("/403");
                dispatcher.forward(request, response);
            }
        }

    }

    public static class ControllerTools {
        public static boolean isAjaxRequest(HttpServletRequest request) {
            return "XMLHttpRequest".equals(request.getHeader("X-Requested-With"));
        }

        public static void print(HttpServletResponse response, String msg) throws IOException {
            response.setCharacterEncoding("UTF-8");
            response.setContentType("application/json; charset=utf-8");
            PrintWriter writer = response.getWriter();
            writer.write(msg);
            writer.flush();
            writer.close();
        }
    }

}

2.6 相關 Controller

登錄/退出跳轉

/**
 * 登錄/退出跳轉
 * @author charlie
 *
 */
@Controller
public class LoginController {
    @GetMapping("/login")
    public ModelAndView login(@RequestParam(value = "error", required = false) String error,
            @RequestParam(value = "logout", required = false) String logout) {
        ModelAndView mav = new ModelAndView();
        if (error != null) {
            mav.addObject("error", "用戶名或者密碼不正確");
        }
        if (logout != null) {
            mav.addObject("msg", "退出成功");
        }
        mav.setViewName("login");
        return mav;
    }
}

登錄成功跳轉

@Controller
public class MainController {

    @GetMapping("/main")
    public ModelAndView toMainPage() {
        //獲取登錄的用戶名
        Object principal= SecurityContextHolder.getContext().getAuthentication().getPrincipal();
        String username=null;
        if(principal instanceof UserDetails) {
            username=((UserDetails)principal).getUsername();
        }else {
            username=principal.toString();
        }
        ModelAndView mav = new ModelAndView();
        mav.setViewName("main");
        mav.addObject("username", username);
        return mav;
    }
    
}

用於不同權限頁面訪問測試

/**
 * 用於不同權限頁面訪問測試
 * @author charlie
 *
 */
@Controller
public class ResourceController {

    @GetMapping("/publicResource")
    public String toPublicResource() {
        return "resource/public";
    }
    
    @GetMapping("/vipResource")
    public String toVipResource() {
        return "resource/vip";
    }
}

用於不同權限ajax請求測試

/**
 * 用於不同權限ajax請求測試
 * @author charlie
 *
 */
@RestController
@RequestMapping("/test")
public class HttptestController {

    @PostMapping("/public")
    public JSONObject doPublicHandler(Long id) {
        JSONObject json = new JSONObject();
        json.put("code", 200);
        json.put("msg", "請求成功" + id);
        return json;
    }

    @PostMapping("/vip")
    public JSONObject doVipHandler(Long id) {
        JSONObject json = new JSONObject();
        json.put("code", 200);
        json.put("msg", "請求成功" + id);
        return json;
    }
}

2.7 相關 html 頁面

登錄頁面

<form class="layui-form" action="/login" method="post">
            <div class="layui-input-inline">
                <input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}"/>
                <input type="text" name="username" required
                    placeholder="用戶名" autocomplete="off" class="layui-input">
            </div>
            <div class="layui-input-inline">
                <input type="password" name="password" required  placeholder="密碼" autocomplete="off"
                    class="layui-input">
            </div>
            <div class="layui-input-inline login-btn">
                <button id="btnLogin" lay-submit lay-filter="*" class="layui-btn">登錄</button>
            </div>
            <div class="form-message">
                <label th:text="${error}"></label>
                <label th:text="${msg}"></label>
            </div>
        </form>

防止跨站請求偽造(CSRF)攻擊

退出系統

<form id="logoutForm" action="/logout" method="post"
                                style="display: none;">
                                <input type="hidden" th:name="${_csrf.parameterName}"
                                    th:value="${_csrf.token}">
                            </form>
                            <a
                                href="javascript:document.getElementById('logoutForm').submit();">退出系統</a>

ajax 請求頁面

<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}" id="hidCSRF">
<button class="layui-btn" id="btnPublic">公共權限請求按鈕</button>
<br>
<br>
<button class="layui-btn" id="btnVip">VIP權限請求按鈕</button>
<script type="text/javascript" th:src="@{/js/jquery-1.8.3.min.js}"></script>
<script type="text/javascript" th:src="@{/layui/layui.js}"></script>
<script type="text/javascript">
        layui.use('form', function() {
            var form = layui.form;
            $("#btnPublic").click(function(){
                $.ajax({
                    url:"/test/public",
                    type:"POST",
                    data:{id:1},
                    beforeSend:function(xhr){
                        xhr.setRequestHeader('X-CSRF-TOKEN',$("#hidCSRF").val());   
                    },
                    success:function(res){
                        alert(res.code+":"+res.msg);
                
                    }   
                });
            });
            $("#btnVip").click(function(){
                $.ajax({
                    url:"/test/vip",
                    type:"POST",
                    data:{id:2},
                    beforeSend:function(xhr){
                        xhr.setRequestHeader('X-CSRF-TOKEN',$("#hidCSRF").val());   
                    },
                    success:function(res){
                        alert(res.code+":"+res.msg);
                        
                    }
                });
            });
        });
    </script>

2.8 測試

測試提供兩個賬號:user 和 admin (密碼與賬號一樣)

由於 admin 作為管理員權限,設置了全部的訪問權限,這裏只展示 user 的測試結果。

完整代碼

非特殊說明,本文版權歸 所有,轉載請註明出處.

原文標題:Spring Boot 2.X(十八):集成 Spring Security-登錄認證和權限控制

原文地址:

如果文章有不足的地方,歡迎提點,後續會完善。

如果文章對您有幫助,請給我點個贊,請掃碼關注下我的公眾號,文章持續更新中…

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

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

收購3c,收購IPHONE,收購蘋果電腦-詳細收購流程一覽表

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

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

※高價收購3C產品,價格不怕你比較

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

碼農當自強

碼農當自強

導航

  • 初出茅廬
  • 跳槽才能漲薪
  • 力拔山兮氣蓋世
  • 止步中層
  • 契約精神?聯盟
  • 技工?匠人
  • 碼農當自強

  有人的地方就有江湖。有江湖必有俠客。IT人的江湖水生草闊,從來都盛產俠客和隱士。很多人離開這片江湖,沒有留下自己的故事,而那些有故事的終究成了傳說。

初出茅廬

  本文的主人公木木君,2011年畢業於一個普通二本大學的計算機專業。那年六月,他懷揣夢想,來到西部的一座准一線城市。

  “天高任鳥飛,海闊憑魚游”。同大多數應屆畢業生一樣,木木君懷揣夢想,滿腔熱心,對未來充滿希冀,希望能夠在這座大城市打拚出自己的一片天地。“求突破,求提高,求發展”,這是他給自己設定的未來五年計劃,分三個步驟執行。

  IT行業的門檻向來很高,大多數民企很少招生應屆生,特別是非985,211大學的童鞋,容易碰壁。木木君面試了20家大大小小的公司,花了一個多月,總算找了個正規公司。這是一家生產機頂盒的工廠,幾千人的公司,僅有不到50人的研發團隊。木木君在這裏開啟了自己的IT職業生涯。團隊不比正規的軟件公司,但同事和睦相處,也能夠接觸到實際的開發項目,總算有所突破。

  “笨鳥先飛”。木木君憑藉自己的努力,半年之內就把部門內部的項目都摸了一遍,並在原有基礎上增加了很多功能。

跳槽才能漲薪

  IT江湖潛規則之跳槽才能漲薪。

  有一天,木木君和往常一樣正在配合運維同事改進新的OA系統。“滴滴滴~”,一波QQ消息來襲。木木君點開消息,是師兄。木木君和這個師兄其實不是一個專業,師兄比他高一個年級,因為大學被同一個老師帶着一起做過項目,經常串寢室,比較熟悉。師兄告訴他,他換工作了,這是他兩年來第三次換工作,在軟件園,月薪8K。聊天完畢,木木君沉思良久,開始對高大上的軟件園產生嚮往。

  在第一家公司待了11個月,木木君選擇了離開,換到了一家軟件園的公司,並有機會和華為的工程師一起合作開發項目。值得一提的是,這次跳槽工資幾乎翻了一倍。

  新公司是行業里排名靠前的,公司制度規範,開發流程標準。團隊里研發同事嚴謹,當然壓力也很大。

  剛進公司的前兩個月,壓力挺大,見識了以前沒有接觸過的框架和組件,以及很多聽不懂的術語。每晚和同事加班到8點半,有時甚至11點才從公司離開,儘管很累,但是能夠感受的技術和經驗上的進步。

  在這家公司待了兩年。見識 了一些牛人,甚至有些一個人頂一個小團隊的人。華為的狼性文化在木木君心中打下烙印,深入血液,變成做事風格的一部分。在以後的工作中,也是秉持了這種文化。

力拔山兮氣蓋世

  大多數碼農,在工作三年左右能夠迎來自己的一個技術上的小高峰。

  木木君在第二家公司待了兩年感覺就像到山上跟了一個武林高手學了一身本領,瞬間有了自信。離職的時候,我和我同事還開玩笑說,出去之後至少都是8K…

  “移動互聯網時代已經來臨,站在風口上豬也能飛”。那一年,手機APP大行其道,獨領風騷,是個公司都想做APP。y也是在那一年小米火了,華為剛開始邁入手機領域。進入第三家公司,是一個不到一百人的軟件公司,做旅遊APP的,期望在這裡能夠接觸到移動端。

  “圈子不同,不硬融”。木木君在這家公司真正見識了什麼是公司內耗。小幫派林立,你再努力也無法融入。一年不到,領導換了幾茬,還玩的是家天下。

止步中層

  對大多數人而言,職場的中層就是天花板。

  “天下之大,竟無用武之地”。就像一個武林俠客,讓他困惑不是練武的孤獨和寂寞,而是竟然沒有用武之地。

  “此處不留爺,自有留爺處”。很快,木木君便進入一件互聯網產品公司,主營電商相關Saas軟件。這裏部門分工明確,需求,產品研發,安全,運維,DBA,測試一應俱全。公司產品成熟,客戶穩定,可以學習真正的大數據和自動化運維。

  在這裏團隊從零開始搭建自動化平台,並逐步迭代,一路摸爬滾打,基本能夠滿足公司100多台服務器的自動化運維。

  木木君一腔熱情,終於受到領導器重,升入中層。團隊不大,但是業務不少,支撐多個部門的系統研發和運維,幾乎人人都掛了2+項目。身為leader的木木君,更是不在話下。特殊時期,幾乎一人承擔一個團隊開發任務。甚至非常時期通宵支持…

  四年過去,木木君也進入了而立之年。2018年,經濟不景氣的一年…公司漲薪已經渺茫…陸續身邊逐漸有人跳槽。不到半年,身邊已經有三個人跳槽,其中一個老領導走了留下一句“待了六年,已經沒有上升空間”。

契約精神?聯盟

  我們是一個團隊,不是一個家庭。

  企業跟員工應該是一種什麼樣的關係?

  領英的執行總裁在《聯盟》這本書中開頭就寫道:“我們是一個團隊,不是一個家庭”。

  近期屢屢爆出的HR被辭退事件和員工因患病被辭退事件,引起了輿論共鳴。但是希望大家認識到員工和公司的關係是合作和聯盟關係。未來更是如此…

技工?匠人

  碼農,一群靠技術謀生的人。只是在互聯網的光環加持下,變得“高精尖”。但是,放在歷史的長河中來看,也不過是特定時代的勞動者。他們和上一個時代的磚瓦工,木匠其實沒有太大差別。是社會生產力發展到一定階段,一種工種對另一種工種的替代。

  互聯網給技術人帶來紅利,容易讓技術人感到天生的優越感,再加上身處大廠就容易自我感覺良好。

  吳軍博士對的碼農層級的分類,可以看出,技術人的發展方向不只是在技術本身,還要具備綜合能力。

  • 第五級:能獨立解決問題,完成工程工作。

  • 第四級:能指導和帶領其他人一同完成更有影響力的工作。

  • 第三級:能獨立設計和實現產品,並且在市場上獲得成功。

  • 第二級:能設計和實現別人不能做出的產品,也就是說他的作用很難取代。

  • 第一級:開創一個產業。

  試問諸君在第幾層?

  說到底,技術人應該秉持匠人精神

碼農當自強

  生活不止眼前的苟且,還有詩和遠方。

  2019年各大公司的財報,都反應很多公司效益差強人意。很多互聯網公司也在尋找新的風口和增長點。而立之年的木木君,雖然躊躇滿志,但再次陷入迷茫。深處IT行業多年,幾乎五年一個風口,技術行業更新快,玩的是創新和顛覆。移動互聯網時代是如何革傳統行業的命,木木君歷歷在目…

  同時,各大媒體充斥着中年危機的推文,一時間人人自危,催生了各種打着知識旗號販賣焦慮的二道販子。不禁讓木木君想起了之前有個大神的文章《屌絲的出路》。那個大神也是一段傳奇。

  很多碼農都在焦慮,但是又不知道如何做?技術人有一個通病,純粹。這不是缺點,但是生活是多元的,要多主動接觸技術之外的世界。

  • 副業

  同時,可以多一些副業嘗試。比如,和朋友一起接一些項目。創建自己的博客。口才好的,可以錄一些技術教學視頻放到網上。

  • 健身

  身體是革命的本錢,這裏的健身不是一定要去健身房,而是要學會鍛煉身體和合理作息。

  • 投資

  年輕的時候,做一點投資和理財。投資房產也是不錯的選擇。投資可以為未來增加一筆資產。

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

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

※高價3c回收,收購空拍機,收購鏡頭,收購 MACBOOK-更多收購平台討論專區

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

收購3c瘋!各款手機、筆電、相機、平板,歡迎來詢價!

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

原創|我是如何從零學習開發一款跨平台桌面軟件的(Markdown編輯器)

原始衝動

最近一直在學習 Electron 開發桌面應用程序,目的是想做一個桌面編輯器,雖然一直在使用Typora這款神器,但無奈Typora太過國際化,在國內水土不服,無法滿足我的一些需求。

比如實現本地圖片上傳到雲端(mac版可以藉助iPic),無法幫我把本地圖片和文章一起發布到博客園、CSDN、SegmentFault、掘金等國內知名博客平台,要麼使用一些免費或付費的圖床,藉助類似iPic的工具,把圖片一鍵上傳到雲端。

我個人也嘗試過七牛雲的免費10G存儲空間,但是說實話,這些免費的空間到最後一定是為了讓你成為付費用戶,各種限制各種吐槽在網上很容易可以搜索到。

免費的圖床如新浪微博等,還算是比較好的圖床工具,相比一些網絡上的壓根不知道啥公司甚至是歸屬個人的免費圖床,新浪應該是比較靠譜的,相對來說可以保證圖片的存活時間,我個人用過一些免費的圖床網站,記得印象深刻的就是服務器出問題,網站掛個公告,曾經的圖片再去訪問就是默認的404。

雖然新浪家大業大不是說倒閉就倒閉的,圖片相對穩定可靠,不過新浪的圖片服務器會檢測訪問來源Referer來防止外部網站引用,造成訪問403。

總結起來就是一句話,圖片還是隨着文章一鍵發布到博客平台比較好。要丟一起丟~

心理掙扎

緣起這個動機,但是下定決心依舊是困難重重。

我個人是一個Java工程師,雖說搞過Andorid、HTML前端,但對前端深感不適的我果斷放棄了。對於桌面程序開發,我連Swing都不會,造一個Markdown編輯器有點難,何況還要加上這些定製功能。

猶猶豫豫,還是決定去嘗試一下。於是調研寫跨平台的一些途徑。

先嘗試Swing,不過Swing不好實現我期望的一些功能,改成JavaFX倒是可以,不過說實話,寫起來很累,太過繁瑣,就放棄了。最後把目光瞄向electron,就它了,HTML+Js+Css,聽起來就很簡單,事實證明,無論是測試還是打包都很方便。

決定之後,便開始進行 Electron 的系統學習。

邁出第一步

第一步就是安裝 Electron 的本地開發環境,這也是大多數應用開發的第一步。

你需要安裝 Node.js 在你的本地電腦,Electron 也是依賴於 Node.js 的環境,嚴格來說, Electron 通過將 Chromium 和 Node.js 合併到同一個運行時環境中,並將其打包為Mac,Windows和Linux系統下的應用來實現這一目的。

關於 Electron 的具體開發流程,這裏不再贅述,你完全可以在開發中使用Web前端開發的思維,除了在處理多個窗口之間交互的時候,就不得不了解Eelctron的進程機制。

主進程和渲染進程

Electron 運行 package.json 的 main 腳本的進程被稱為主進程。 在主進程中運行的腳本通過創建web頁面來展示用戶界面。 一個 Electron 應用總是有且只有一個主進程。

由於 Electron 使用了 Chromium 來展示 web 頁面,所以 Chromium 的多進程架構也被使用到。 每個 Electron 中的 web 頁面運行在它自己的渲染進程中。

在普通的瀏覽器中,web頁面通常在沙盒環境中運行,並且無法訪問操作系統的原生資源。 然而 Electron 的用戶在 Node.js 的 API 支持下可以在頁面中和操作系統進行一些底層交互。

主進程與渲染進程的區別

主進程使用 BrowserWindow 實例創建頁面。 每個 BrowserWindow 實例都在自己的渲染進程里運行頁面。 當一個 BrowserWindow 實例被銷毀后,相應的渲染進程也會被終止。

主進程管理所有的web頁面和它們對應的渲染進程。 每個渲染進程都是獨立的,它只關心它所運行的 web 頁面。

在頁面中調用與 GUI 相關的原生 API 是不被允許的,因為在 web 頁面里操作原生的 GUI 資源是非常危險的,而且容易造成資源泄露。 如果你想在 web 頁面里使用 GUI 操作,其對應的渲染進程必須與主進程進行通訊,請求主進程進行相關的 GUI 操作。

主進程與渲染進程通信

那麼進程間如何通訊?

Electron為主進程( main process)和渲染器進程(renderer processes)通信提供了多種實現方式,如可以使用ipcRenderer 和 ipcMain模塊發送消息,使用 remote模塊進行RPC方式通信。

你還可以用 Electron 內的 IPC 機制實現。將數據存在主進程的某個全局變量中,然後在多個渲染進程中使用 remote 模塊來訪問它。

示例代碼:

// 在主進程中
global.sharedObject = {
  someProperty: 'default value'
}
// 在第一個頁面中
require('electron').remote.getGlobal('sharedObject').someProperty = 'new value'
// 在第二個頁面中
console.log(require('electron').remote.getGlobal('sharedObject').someProperty)

使用Electron的API

Electron在主進程和渲染進程中提供了大量API去幫助開發桌面應用程序, 在主進程和渲染進程中,你可以通過require的方式將其包含在模塊中以此,獲取Electron的API

const electron = require('electron')

所有Electron的API都被指派給一種進程類型。 許多API只能被用於主進程或渲染進程中,但其中一些API可以同時在上述兩種進程中使用。 每一個API的文檔都將聲明你可以在哪種進程中使用該API。

Electron中的窗口是使用BrowserWindow類型創建的一個實例, 它只能在主進程中使用。

// 這樣寫在主進程會有用,但是在渲染進程中會提示'未定義'
const { BrowserWindow } = require('electron')

const win = new BrowserWindow()

因為進程之間的通信是被允許的, 所以渲染進程可以調用主進程來執行任務。 Electron通過remote模塊暴露一些通常只能在主進程中獲取到的API。 為了在渲染進程中創建一個BrowserWindow的實例,通常使用remote模塊為中間件:

// 這樣寫在渲染進程中時行得通的,但是在主進程中是'未定義'
const { remote } = require('electron')
const { BrowserWindow } = remote

const win = new BrowserWindow()

使用Node.js的API

Electron同時在主進程和渲染進程中對Node.js 暴露了所有的接口。 這裡有兩個重要的定義:

1)所有在Node.js可以使用的API,在Electron中同樣可以使用。 在Electron中調用如下代碼是有用的:

const fs = require('fs')

const root = fs.readdirSync('/')

// 這會打印出磁盤根級別的所有文件
// 同時包含'/'和'C:\'。
console.log(root)

2)你可以在你的應用程序中使用Node.js的模塊。 選擇您最喜歡的 npm 模塊。 npm 提供了目前世界上最大的開源代碼庫,那裡包含良好的維護、經過測試的代碼,提供給服務器應用程序的特色功能也提供給Electron。

例如,在你的應用程序中要使用官方的AWS SDK,你需要首先安裝它的依賴:

npm install --save aws-sdk

然後在你的Electron應用中,通過require引入並使用該模塊,就像構建Node.js應用程序那樣:

// 準備好被使用的S3 client模塊
const S3 = require('aws-sdk/clients/s3')

有一個非常重要的提示: 原生Node.js模塊 (即指,需要編譯源碼過後才能被使用的模塊) 需要在編譯后才能和Electron一起使用。

最終產品殺青落地

終於搞明白了 Electron 的應用架構,那麼接着就要進入產品的開發階段。比較慶幸的是,ELectron 的UI完全由CSS+HTML組成,這部分可用的框架太多了,我選擇了又老又知名的 BootStarp 框架搭建界面UI,還引用了JS框架JQuery。選擇了 electron-store 作為本地存儲文件,至於最關鍵的Markdown語法解析,對比了一番主流解析框架,最終選擇了 markdown-it。貼一下效果圖:

這款軟件我給他起名為 JustWrite,意思就是現在就寫,也是在督促自己吧,畢竟猶豫徘徊,等於白來。

現在軟件的功能除了包含一鍵發布本地文章加本地圖片到博客園、CSDN、SegmentFault、掘金、開源中國等平台,我還打算將他打造為一個體驗不錯的Markdown寫作軟件。現在你閱讀的這篇文章,就是我使用 JustWrite 書寫的,使用的字體是我個人喜歡的幼圓體,除此之外,還有六款風格迥異的字體可以切換使用。字號也是可以動態放大或者縮小,還可以關閉右側預覽,專註於寫作,如下圖所示:

這些截圖是我截屏后使用快捷鍵Ctrl+V一鍵粘貼的,圖片會自動放到當前md文件所在目錄下的picture文件夾內。

關於 JustWrite 從構思到實踐的心路歷程大致就以上這些了,這次開發 JustWrite 也讓我過了一把產品經理的癮,基本已經滿足了我的日常需求。如果你有更好的想法和創意也可以告訴我,說不定第二天就會實現了。

Github:

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

※公開收購3c價格,不怕被賤賣!

※想知道網站建置網站改版該如何進行嗎?將由專業工程師為您規劃客製化網頁設計後台網頁設計

※不管是台北網頁設計公司台中網頁設計公司,全省皆有專員為您服務

※Google地圖已可更新顯示潭子電動車充電站設置地點!!

※帶您來看台北網站建置台北網頁設計,各種案例分享

abp(net core)+easyui+efcore實現倉儲管理系統——ABP WebAPI與EasyUI結合增刪改查之二(二十八)




 










   

       在上一篇 文章中我們學習了TreeGrid的一些基礎知識,接下我們來創建我們開發組織管理功能用到的一些類。關於如何創建類我們之前的文章中已經寫了很多了,這裡會有些簡略。

 

四、定義應用服務接口需要用到的DTO類

      為了在進行查詢時使用, PagedOrgResultRequestDto被用來將模塊數據傳遞到基礎設施層.

       1. 在Visual Studio 2017的“解決方案資源管理器”中,右鍵單擊“ABP.TPLMS.Application”項目,在彈出菜單中選擇“添加” > “新建文件夾”,並重命名為“Orgs”

      2. 使用鼠標右鍵單擊我們剛才創建的“Orgs”文件夾,在彈出菜單中選擇“添加” > “新建文件夾”,並重命名為“Dto”。

      3.右鍵單擊“Dto”文件夾,然後選擇“添加” > “類”。 將類命名為 Paged OrgResultRequestDto,然後選擇“添加”。代碼如下。

using Abp.Application.Services.Dto;
using Abp.Application.Services.Dto;
using System;
using System.Collections.Generic;
using System.Text;
 

namespace ABP.TPLMS.Orgs.Dto
{

public class PagedOrgResultRequestDto : PagedResultRequestDto
    {
        public string Keyword { get; set; }
    }
}

      4.右鍵單擊“Dto”文件夾,然後選擇“添加” > “類”。 將類命名為 OrgDto,然後選擇“添加”。代碼如下。

using Abp.Application.Services.Dto;
using Abp.AutoMapper;
using ABP.TPLMS.Entitys;
using System;
using System.Collections.Generic;
using System.Text;
 

namespace ABP.TPLMS.Orgs.Dto
{

    [AutoMapFrom(typeof(Org))]
    public class OrgDto : EntityDto<int>
    {

        int m_parentId = 0;
        public string Name { get; set; }       

        public string HotKey { get; set; }
        public int ParentId { get { return m_parentId; } set { m_parentId = value; } }       

        public string ParentName { get; set; }
        public bool IsLeaf { get; set; }
        public bool IsAutoExpand { get; set; }       

        public string IconName { get; set; }
        public int Status { get; set; }
        public int Type { get; set; }      
        public string BizCode { get; set; }       

        public string CustomCode { get; set; }
        public DateTime CreationTime { get; set; }
        public DateTime UpdateTime { get; set; }

        public int CreateId { get; set; }
        public int SortNo { get; set; }
        public int _parentId {
            get { return m_parentId; }          

        }
    }
}

 

      5.右鍵單擊“Dto”文件夾,然後選擇“添加” > “類”。 將類命名為 CreateUpdateOrgDto,然後選擇“添加”。代碼如下。

using Abp.Application.Services.Dto;
using Abp.AutoMapper;
using ABP.TPLMS.Entitys;
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Text;

 
namespace ABP.TPLMS.Orgs.Dto
{

    [AutoMapTo(typeof(Org))]
    public class CreateUpdateOrgDto : EntityDto<int>
    {

        public string Name { get; set; }
        [StringLength(255)]
        public string HotKey { get; set; }

        public int ParentId { get; set; }

        [Required]
        [StringLength(255)]
        public string ParentName { get; set; }

        public bool IsLeaf { get; set; }
        public bool IsAutoExpand { get; set; }

        [StringLength(255)]
        public string IconName { get; set; }

        public int Status { get; set; }
        public int Type { get; set; }

        [StringLength(255)]
        public string BizCode { get; set; }

        [StringLength(100)]
        public string CustomCode { get; set; }
        public DateTime CreationTime { get; set; }
        public DateTime UpdateTime { get; set; }

        public int CreateId { get; set; }
        public int SortNo { get; set; } 

    }
}

 

 

 

 

五、定義IOrgAppService接口

        6. 在Visual Studio 2017的“解決方案資源管理器”中,鼠標右鍵單擊“Org”文件夾,然後選擇“添加” > “新建項”,在彈出對話框中選擇“接口”。為應用服務定義一個名為 IOrgAppService 的接口。代碼如下。

using System;
using System.Collections.Generic;
using System.Text;
using Abp.Application.Services;
using ABP.TPLMS.Orgs.Dto; 

 

namespace ABP.TPLMS.Orgs
{
  public  interface IOrgAppService : IAsyncCrudAppService<//定義了CRUD方法

             OrgDto, //用來展示組織信息
             int, //Org實體的主鍵
             PagedOrgResultRequestDto, //獲取組織信息的時候用於分頁
             CreateUpdateOrgDto, //用於創建組織信息
             CreateUpdateOrgDto> //用於更新組織信息

    {
    }
}

 

      六、實現IOrgAppService

       7.在Visual Studio 2017的“解決方案資源管理器”中,右鍵單擊“Org”文件夾,然後選擇“添加” > “新建項”,在彈出對話框中選擇“類”。為應用服務定義一個名為 OrgAppService 的服務類。代碼如下。

using Abp.Application.Services;
using Abp.Domain.Repositories;
using ABP.TPLMS.Entitys;
using ABP.TPLMS.Orgs.Dto;
using System;
using System.Collections.Generic;
using System.Text;
 

namespace ABP.TPLMS.Orgs
{

    public class OrgAppService : AsyncCrudAppService<Org, OrgDto, int, PagedOrgResultRequestDto,
                            CreateUpdateOrgDto, CreateUpdateOrgDto>, IOrgAppService 

    {
        public OrgAppService(IRepository<Org, int> repository)

            : base(repository)

        { 

        }
    }
}

七 創建OrgController繼承自TPLMSControllerBase

      1. 在Visual Studio 2017的“解決方案資源管理器”中,右鍵單擊在領域層“ABP.TPLMS.Web.Mvc”項目中的Controller目錄。 選擇“添加” > “新建項…”。如下圖。

 

     2. 在彈出對話框“添加新項-ABP.TPLMS.Web.Mvc”中選擇“控制器類”,然後在名稱輸入框中輸入“OrgsController”,然後點擊“添加”按鈕。

     3.在OrgsController.cs文件中輸入如下代碼,通過構造函數注入對應用服務的依賴。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Abp.AspNetCore.Mvc.Authorization;
using Abp.Web.Models;
using ABP.TPLMS.Controllers;
using ABP.TPLMS.Orgs;
using ABP.TPLMS.Orgs.Dto;
using ABP.TPLMS.Web.Models.Orgs;
using Microsoft.AspNetCore.Mvc;


namespace ABP.TPLMS.Web.Controllers
{
    [AbpMvcAuthorize]
    public class OrgsController : TPLMSControllerBase
    {
        private readonly IOrgAppService _orgAppService;
        private const int MAX_COUNT= 1000;
        
        public OrgsController(IOrgAppService orgAppService)
        {
            _orgAppService = orgAppService;
        }
        [HttpGet]
        // GET: /<controller>/
        public IActionResult Index()
        {
            return View();
        }
        [DontWrapResult]
        [HttpPost]
        public string List()
        {         
            PagedOrgResultRequestDto paged = new PagedOrgResultRequestDto();
            paged.MaxResultCount = MAX_COUNT;
           var userList = _orgAppService.GetAll(paged).GetAwaiter().GetResult().Items;
            int total = userList.Count;
            var json = JsonEasyUI(userList, total);
            return json;
        }       
    }
}

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

收購3c,收購IPHONE,收購蘋果電腦-詳細收購流程一覽表

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

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

※公開收購3c價格,不怕被賤賣!

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

靚仔靚女如何用瀏覽器自拍和保存

一、前言

1.核心技術

  • Web Real-Time Communication:網頁即時通信,可以在瀏覽器進行實時語音或者視頻對話的API
  • Canvas:HTML5中的新元素,可以用來來繪製圖形、圖標、以及其它任何視覺性圖像

2.音頻採集的基本概念

  • 攝像頭:用於採集圖像和視頻
  • 麥克風:採集音頻數據
  • 幀率:一秒鐘採集圖像的次數。幀率越高,越平滑流暢
  • 軌:借鑒了多媒體的概念,每條軌數據都是獨立的,如MP4中的音頻軌、視頻軌,是分別被存儲的
  • 流:可以理解為容器。在WebRTC中,流可以分為媒體流(MediaStream)和數據流(DataStream)。
  • 分辨率:2K、1080P、720P等,越清晰,佔用帶寬越多

3.音視頻設備的基本原理

  • 音頻設備
    音頻輸入設備主要是採集音數據,而採集音頻數據的本質是模擬信號轉成数字信號,
    採集到的數據經過量化、編碼,最終開成数字信號,這就是音頻設備要完成的工作。
    人的聽覺範圍的頻率是20Hz~20kHz之間,日常語音交流8kHz就哆了。
    為了追求高品質、高保真,需要將音頻輸入設備採樣率設置在40kHz上才能完整保留原始信號

  • 視頻設備
    當實物光通過鏡頭進行攝像機后,它會通過視頻設備的模數轉換(A/D)模塊,即光學傳感器,將光轉換成数字信號,即RGB數據,獲得RGB數據后,再通過DSP進行優化處理,如自動增強、白平衡、色彩飽和等,等到24位的真彩色圖片

模數轉換使用的採集定理稱為奈奎斯特定理:

在進行模擬 / 数字信號的轉換過程中,當採樣率大於信號中最高頻率的 2 倍時,採樣之後的数字信號就完整地保留原始信號中的信息。

talk is cheap, 上代碼,以下示例運行的時候會請求攝像頭權限,同意即可,接下來就是見證奇迹的時刻!

二、示例

1.示例1-打開攝像頭

這就是照像的核心功能,以後可以用來化妝,擠痘痘,整理髮型

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>打開攝像頭</title>
</head>
<body>
<h1>打開攝像頭</h1>
<video autoplay playsinline></video>
</body>
</html>

<script>
    const mediaStreamContrains = {
        video: {
            frameRate: {min: 20},
            width: {min: 640, ideal: 1280},
            height: {min: 360, ideal: 720},
            aspectRatio: 16 / 9
        },
        audio: {
            echoCancellation: true,
            noiseSuppression: true,
            autoGainControl: true
        }
    };

    const localVideo = document.querySelector('video');

    function gotLocalMediaStream(mediaStream) {
        localVideo.srcObject = mediaStream;
    }

    function handleLocalMediaStreamError(error) {
        console.log('navigator.getUserMedia error: ', error);
    }

    navigator.mediaDevices.getUserMedia(mediaStreamContrains).then(
        gotLocalMediaStream
    ).catch(
        handleLocalMediaStreamError
    );
</script>

運行結果如下

示例2-拍照保存

這裏展示了兩個按鈕,拍照和保存,yes,就是自拍的核心功能!

<html>
<head>
    <meta charset="UTF-8">
    <title>拍照一分鐘,P圖兩小時</title>
</head>

<body>
<section>
    <div>
        <video autoplay playsinline id="player"></video>
    </div>

</section>
<section>
    <div>
        <button id="snapshot">拍照</button>
        <button id="download">下載</button>
    </div>
    <div>
        <canvas id="picture"></canvas>
    </div>
</section>
</body>
</html>


<script>
    'use strict';

    var videoplay = document.querySelector('video#player');

    function gotMediaStream(stream) {
        window.stream = stream;
        videoplay.srcObject = stream;
    }

    function handleError(err) {
        console.log('getUserMedia error:', err);
    }

    function start() {
        var constraints = {
            video: {
                width: 1280,
                height: 720,
                frameRate: 15,
                facingMode: 'enviroment'
            },
            audio: false
        }

        navigator.mediaDevices.getUserMedia(constraints)
            .then(gotMediaStream)
            .catch(handleError);
    }


    //拍照
    var snapshot = document.querySelector('button#snapshot');
    snapshot.onclick = function () {
        var picture = document.querySelector('canvas#picture');
        picture.width = 1280;
        picture.height = 720;
        picture.getContext('2d').drawImage(videoplay, 0, 0, picture.width, picture.height);
    };


    //下載
    function downLoad(url) {
        var oA = document.createElement("a");
        oA.download = 'photo';// 設置下載的文件名,默認是'下載'
        oA.href = url;
        document.body.appendChild(oA);
        oA.click();
        oA.remove(); // 下載之後把創建的元素刪除
    }

    document.querySelector("button#download").onclick = function () {
        downLoad(picture.toDataURL("image/jpeg"));
    };

    start();

</script>

運行結果如下

就是這麼簡單!

重點方法和參數解釋

  • 1.方法:avigator.mediaDevices.getUserMedia(constraints);
    返回一個promise對象,調用成功,可以通過promise對象獲取MediaStream對象,

  • 2.參數:mediaStreamContrains
    傳入的constraints參數類型為 MediaStreamConstraints,可以指定 MediaStream 中包含哪些類型的媒體軌(音頻軌、視頻軌),並且可為這些媒體軌設置一些限制。
    視頻的幀率最小 20 幀每秒;
    寬度最小是 640,理想的寬度是 1280;
    高度最小是 360,最理想高度是 720;
    寬高比是 16:9;
    對於音頻則是開啟迴音消除、降噪以及自動增益功能。

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

※高價收購3C產品,價格不怕你比較

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

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

3c收購,鏡頭 收購有可能以全新價回收嗎?

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

不只是換殼上市!EC-05 與 Gogoro S2 實測比較

EC-05 由台灣山葉(YAMAHA)和 Gogoro 聯手打造,動力系統採用與 Gogoro S2 相同的 G2 鋁合金水冷永磁同步馬達(S-Version),許多人不禁疑問 EC-05 和 Gogoro S2 到底有什麼差別?難道只是換了一個車殼?更別說 EC-05 的價格還比 Gogoro S2 貴了 2,820 元。為了比較其中的差別,《科技新報》試駕了 EC-05 和 2020 年式的 Gogoro S2,帶來兩台車的第一手觀察。

EC-05 配備嵌合式環型頭燈,能在夜間騎乘時避免光線發散,確保前方視野的明亮程度。Gogoro S2 同樣配備環型頭燈,不過形狀略有不同,和一般機車一樣完全外露。EC-05 的後照鏡為菱形設計,視野較為寬廣,Gogoro S2 的後照鏡是圓形,視野較狹窄,而且騎乘者容易被自己的身體擋住部分視線。

Gogoro S2 前方配備了多功能置物箱,可以擺放手機和手套等小型物品,也能做為飲料置杯架使用,EC-05 就沒有這項裝置。Gogoro S2 的前方置物箱內附 USB 充電插槽,EC-05 的 USB 插孔則位於後座的置物箱。EC-05 使用無線智慧鑰匙,靠近車輛後按壓鑰匙的灰色部分進行解鎖。Gogoro S2 則使用 iQ system 智慧鑰匙卡,只要輕觸 S 標誌就能解鎖。

EC-05(左)配備嵌合式環型頭燈,Gogoro S2 的頭燈則外露。

EC-05 後照鏡呈菱形,視野較寬廣。

Gogoro S2 的後照鏡為圓形。

Gogoro S2 配備多功能置物箱,並附有 USB 充電插槽。

EC-05 的 USB 充電插槽位於後置物箱內側。

EC-05 以無線智慧鑰匙解鎖。

試乘時騎行陽明山的山路,EC-05 的穩定度和避震能力優於 Gogoro S2,在路面凹凸不平時較不容易顛簸,雙載騎乘時的差距更加明顯。EC-05 的座墊將後座的後方略為墊高,讓座墊呈現「山」字型的結構,側邊則採切削設計,讓騎乘者的腿部更為舒適。Gogoro S2 的後座較為平滑,中間使用皮質材質並標有「S」字樣。雖然乍看之下差異不大,但實際騎乘時後座乘客的感受明顯不同。

EC-05 的乘客在機車減速和加速時都更容易維持穩定,比較不會有往後飛出去或往前貼到前方騎乘者身上的狀況,即使長時間乘坐也不會滑動。Gogoro S2 的後座把手較為平直,EC-05 的把手彎曲向上,對於手不夠長的乘客更容易抓握。相較於 Gogoro S2,EC-05 能給予後座乘客更安穩的乘坐體驗。

EC-05 的座墊呈現「山」字型的凸起,讓後座乘客乘坐時更穩定。

Gogoro S2 的座墊較為平滑。

EC-05 與 Gogoro S2 座墊比較。

EC-05 的後座把手彎曲向上,較容易抓握。

Gogoro S2 的後座把手平直,手沒那麼長的後座乘客較難抓握。

EC-05(右)與 Gogoro S2 的後座把手差異從車尾可以明顯看出。

由於 EC-05 與 Gogoro S2 採用相同的動力系統,因此性能上的數據相差無幾。EC-05 最大馬力為 10hp,安全極速達到時速 90 公里,靜止加速到時速 50 公里需要 3.9 秒。Gogoro S2 最大馬力為 10.18hp,安全極速達到時速 92 公里,靜止加速到時速 50 公里僅需 3.8 秒,各方面都是 Gogoro S2 稍勝。

相較於 Gogoro S2,EC-05 的儀表板略為調整傾斜角度並向前方移動,用以減少騎乘者所需的視線移動。EC-05 的腳踏板具有菱形的凸起幫助止滑,讓騎乘時腳的位置可以更穩定。Gogoro S2 的腳踏板則是完全平滑,並有著「gogoro」的字樣。

EC-05 在車及座墊高度上也有更動,車高為 1,180 mm 略高於 Gogoro S2 的 1,080 mm,座高則以 768 mm 略低於 Gogoro S2 的 780 mm。調整過後 EC-05 在龍頭和座墊間的空間增加,騎乘時較為舒適也不容易意外按壓到喇叭等按鍵。不過 EC-05 的龍頭相較 Gogoro S2 略為沉重,力氣較小的使用者操控時可能會稍微費力。

EC-05 的腳踏板具有止滑的菱形格紋。

Gogoro S2 的腳踏板較為平滑。

身高 176 公分的騎乘者與 EC-05 的比例。

身高 156 公分的騎乘者與 EC-05 的比例。

YAMAHA 的設計理念標榜「人機官能」,也就是在設計時結合機械結構和人體工學。這樣的概念讓 EC-05 看似與 Gogoro S2 僅有微幅的差異,但這些細節卻在騎乘時能發揮不成比例的效果。至於這樣的差別值不值得多花 2,820 元購買,那就要看個人是否在意這些細節帶來的感受。

EC-05 提供藍灰色、深黑色、深藍灰色和白銀色 4 種顏色讓消費者選擇,定價為台幣 99,800 元。Gogoro S2 僅有灰黑色一個款式,定價為台幣 96,980 元。

(合作媒體:。圖片來源:)

延伸閱讀:

本站聲明:網站內容來源於EnergyTrend https://www.energytrend.com.tw/ev/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※高價收購3C產品,價格不怕你比較

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

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

3c收購,鏡頭 收購有可能以全新價回收嗎?

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

中油換電站今年採競標,Gogoro累計 285 站、光陽 146 站

中油從 107 年起,依據經濟部智慧電動機車能源補充設施普及計畫(公建計畫)於加油站建置電動機車充電、換電設備,去年僅睿能 Gogoro 參與設 144 站,今年增加光陽 Ionex,兩家採公開競標方式,由光陽 Ionex 獲得 3 標共 146 站,睿能 Gogoro 得到 1 標 48 站;其中,睿能 Gogoro 扣除合約到期站數,再加上先前自建設站,在台灣中油加油站中有 285 站可為消費者服務。

經濟部工業局委託台灣銀行辦理共同供應契約設備系統招標,其中電池交換設備系統得標廠商都需符合安全規範審核,108 年換電設備系統之得標廠商共有睿能 Gogoro 及光陽 Ionex 兩家,中油考量消費者的便利性及設備使用公平性兩大原則,於採購系統設備前即採取公開方式辦理合作經營招標。

中油表示,北部地區有 52 站與睿能 Gogoro 合作的換電站,今年合約陸續到期,睿能 Gogoro 依據契約與決標結果遷移設備,為執行公建計畫,中油將此52站列入今年建置194站換電站的地點,公建換電站依各縣市站點打散,再平均分4案開標,讓每一標案站點均涵蓋各縣市,投標結果為光陽 Ionex 獲得 3 標(146 站)、睿能 Gogoro 得 1 標(48 站),原先台灣中油與睿能 Gogoro 合約到期的52站中,有38站改由光陽Ionex得標。

中油指出,上述政府電動機車充、換電站公建計畫,於 107 年已執行 161 站,其中 144 座換電站均為睿能 Gogoro 所有,另有充電 17 站,今年預計建置 216 座充換電站,其中 194 座為換電站,光陽 Ionex 有 146 站、睿能 Gogoro 48 站,另有充電 22 站,以總數來說,再加上睿能 Gogoro 之前的自建站 79 站,其仍在台灣中油有 285 站,光陽 Ionex 有 146 站為消費者服務,明年亦將配合政府預算持續建置更多站點,協助政府綠能政策推動。

(本文內容由 授權使用。首圖來源:Gogoro)

本站聲明:網站內容來源於EnergyTrend https://www.energytrend.com.tw/ev/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

收購3c,收購IPHONE,收購蘋果電腦-詳細收購流程一覽表

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

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

※公開收購3c價格,不怕被賤賣!

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

夏季電動機車市場正熱,Gogoro 掛牌數突破 20 萬

2019 年台灣電動機車市場百家爭鳴,也帶動市場熱度,越來越多消費者願意選擇電動機車。電動機車大廠 Gogoro 宣布旗下總掛牌數正式達 20 萬輛,寫下新的里程碑。

根據工研院產科國際所推估,台灣電動機車年銷量可以突破 15 萬輛,成長速度超過全球平均。2019 年夏季包括 Gogoro、光陽(Kymco)、山葉(YAMAHA)、中華汽車 eMOVING 和宏佳騰等品牌都推出新的電動機車,代表車廠也清楚了解市場對電動機車的需求。

受惠於暑假旺季提前發酵,加上受到新車效應與即將到來的開學季影響,Gogoro 從 2019 年 5 月以來已連續 4 個月單月掛牌數破萬,這也讓 Gogoro 總掛牌數正式達到 20 萬輛。在全新發表的 Gogoro S2 ABS 車款助攻之下,讓 Gogoro 在 8 月單月掛牌市占率達 16.35%,即使 8 月整體機車市場下滑 20%,仍然保持穩定成長。

Gogoro 資深行銷總監陳彥揚表示:「今年夏天對 Gogoro 來說深具意義,連續 4 個月掛牌破萬,帶動 Gogoro 總車主數超越 20 萬大關,除了非常感謝車主的熱情支持,業界的投入及政府政策的推動,也是電動機車產業發展向前邁進的關鍵要素,期盼能快速跟上全球化電動車趨勢的腳步。」

每年最具代表性的車主活動「快閃台北橋」將於 9 月 29 日舉辦,2018 年活動以 1,303 台機車創下「世界最大規模電動機車遊行」的金氏世界紀錄,Gogoro 也將擴大邀請所有電動機車車主,今年活動時集結台北橋。

8 月推出的「暢快騎 0 元起」購車方案廣受歡迎,Gogoro 為了慶祝車主數量突破 20 萬大關,宣布活動時間將延長至 9 月 30 日,購買 Gogoro 全車系即贈送 6 個月電池服務資費 299 元,無論選擇什麼方案每個月都可抵扣電池資費 299 元。即日起至 9 月 30 日購買 Gogoro 全車系並符合學生資格的車主,即可享有 12 期 0 利率分期優惠,同時再送「1 年期車碰車險」。

(合作媒體:。首圖來源:Gogoro)

本站聲明:網站內容來源於EnergyTrend https://www.energytrend.com.tw/ev/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※公開收購3c價格,不怕被賤賣!

※想知道網站建置網站改版該如何進行嗎?將由專業工程師為您規劃客製化網頁設計後台網頁設計

※不管是台北網頁設計公司台中網頁設計公司,全省皆有專員為您服務

※Google地圖已可更新顯示潭子電動車充電站設置地點!!

※帶您來看台北網站建置台北網頁設計,各種案例分享

js 關於apply和call的理解使用

  關於call和apply,以前也思考良久,很多時候都以為記住了,但是,我太難了。今天我特地寫下筆記,希望可以完全掌握這個東西,也希望可以幫助到任何想對學習這個東西的同學。

一.apply函數定義與理解,先從apply函數出發

  在MDN上,apply的定義是:

    “apply()方法調用一個具有給定this值的函,以及作為一個數組(或)提供的參數。”

  我的理解是:apply的前面有個含有this的對象,設為A,apply()的參數里,也含有一個含有this的對象設為B。則A.apply(B),表示A代碼執行調用了B,B代碼照常執行,執行后的結果作為apply的參數,然後apply把這個結果所指代表示的this替換掉A本身的this,接着執行A代碼。

  比如:

 1     var aa = {
 2         _name:111,
 3         _age:222,
 4         _f:function(){
 5             console.log(this)
 6             console.log(this._name)
 7         }
 8     }
 9     var cc = {
10         _name:0,
11         _age:0,
12         _f:function(){
13             console.log(this)
14             console.log(this._name)
15         }
16     }
17     cc._f.apply(aa)//此時aa表示的this就是aa本身
18     cc._f.apply(aa._f)//此時aa._f表示的this就是aa._f本身
19     
20     /**
21      * 此時aa._f()表示的this,就是執行后的結果本身。aa._f()執行后,沒有返回值,所以應該是undefined,而undefined作為call和apply的參數時,call和apply前面的方法 cc._f 的this會替換成全局對象window。
22      * 參考MDN:https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Function/apply 的參數說明
23      */
24     cc._f.apply(aa._f())

執行結果:

  1.參數為aa

  

  這兩行的打印的都是來自 cc._f 方法內的那兩句console 。aa的時候算是初始化,裏面的 aa._f 方法沒有執行。

  2.參數為aa.f

  

  這兩行的打印的都是來自 cc._f 方法內的那兩句console 。aa.f 的時候應該也算是初始化,或者是整個函數當參數傳但是不執行這個參數,即 aa._f 方法沒有執行。

  3.參數為aa.f()

   

  這四行的打印,前面兩行來自 aa._f() 方法執行打印的;後面兩行是來自cc._f()方法打印的。

  后兩行解析:aa._f()執行后,沒有返回值,所以是undefined,在apply執行解析后,cc._f()的this變成的window,所以打印了window。window裏面沒有_name這個屬性,所以undefined。

 二.apply與call的區分

  1.apply()

    A.apply(B, [1,2,3]) 後面的參數是arguments對象或類似數組的對象,它會被自動解析為A的參數;

    對於A.apply(B) / A.call(B) , 簡單講,B先執行,執行后根據結果去執行A。這個時候,用A去執行B的內容代碼,然後再執行自己的代碼。

  比如:

    var f1 = function(a,b){
        console.log(a+b)
    }
    var f2 = function(a,b,c){
        console.log(a,b,c)
    }
    f2.apply(f1,[1,2])//1 2 undefined

   先執行f1,f1執行后(f1是f1,不是f1()執行方法,所以console.log(a+b)這行代碼並沒有執行,相當於,初始化了代碼f1),由於沒有返回值,所以結果是undefined,f2執行的時候this指向window;參數中的 ” [1,2] “,解析后變成 f2 的參數 “ 1,2,undefined ”,執行f2方法后,打印出1,2,undefined三個值

  2.call()

    A.call(B, 1,2,3) 後面的參數都是獨立的參數對象,它們會被自動解析為A的參數;

  比如: 

    var f1 = function(a,b){
        console.log(a+b)
    }
    var f2 = function(a,b,c){
        console.log(a,b,c)
    }
    f2.call(f1,[1,2])//[1,2] undefined undefined
    f2.call(f1,1,2)//1 2 undefined

   參數中的 ” [1,2] “,因為傳入了一個數組,相當於只傳入了第一個參數,b和c參數沒有傳。解析后變成 f2 的參數 “ [1,2],undefined ,undefined ”,執行f2方法后,打印出 [1,2],undefined ,undefined 三個值。

三.apply與call帶來的便利

  1. push();

  push參數是類似(a,b,c,d,e)如此傳輸的,如果在一個數組的基礎上進行傳輸另一個數組的內容,可以如下:

    //apply用法
    var arr = new Array(1,2,3)
    var arr1 = new Array(11,21,31)
    Array.prototype.push.apply(arr,arr1)
    console.log(arr)//[1, 2, 3, 11, 21, 31]
    
    //call用法
    var arr = new Array(1,2,3)
    var arr1 = new Array(11,21,31)
    Array.prototype.push.call(arr,arr1[0],arr1[1],arr1[2])
    console.log(arr)//[1, 2, 3, 11, 21, 31]

   2. 數組利用Math求最大和最小值

  apply和call的第一個參數,如果是null或者undefined,則apply或call前面的函數會把this指向window

    //apply的用法
    var _maxNum = Math.max.apply(null,[1,3,2,4,5])
    console.log(_maxNum)//5
    var _minNum = Math.min.apply(null,[1,3,2,4,5])
    console.log(_minNum)//1
    
    //call的用法
    var _maxNum = Math.max.call(null,1,3,2,4,5)
    console.log(_maxNum)//5
    var _minNum = Math.min.call(null,1,3,2,4,5)
    console.log(_minNum)//1

 四.總結

  簡而言之,apply和call函數的第一個參數都是用來替換this指向的對象;apply的第二個參數使用arguments或者類似數組的參數進行傳參,call的第二個或以上的參數,使用獨立單位,一個一個進行傳參;執行順序是apply或call的第一個參數先執行得到結果,然後執行apply或call前面的函數,執行的時候用已經執行的結果所指代的this去執行。apply和call的使用除了參數上的使用方式不同外,功能是一模一樣的。

  以上內容純屬個人理解,有誤勿噴請指出!謝謝!

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

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

收購3c,收購IPHONE,收購蘋果電腦-詳細收購流程一覽表

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

※想要讓你的商品在網路上成為最夯、最多人討論的話題?

※高價收購3C產品,價格不怕你比較

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