摘要:在領域,有兩大主流的安全框架,和。角色角色是一組權限的集合。安全框架的實現注解的實現本套安全框架一共定義了四個注解。該注解用來告訴安全框架,本項目中所有類所在的包,從而能夠幫助安全框架快速找到類,避免了所有類的掃描。
寫在最前
本文是《手把手項目實戰系列》的第三篇文章,預告一下,整個系列會介紹如下內容:
《手把手0基礎項目實戰(一)——教你搭建一套可自動化構建的微服務框架(SpringBoot+Dubbo+Docker+Jenkins)》
《手把手0基礎項目實戰(二)——微服務架構下的數據庫分庫分表實戰》
《手把手0基礎項目實戰(三)——教你開發一套安全框架》
《手把手0基礎項目實戰(四)——電商訂單系統架構設計與實戰(分布式事務一致性保證)》
《手把手0基礎項目實戰(五)——電商系統的緩存策略》
《手把手0基礎項目實戰(六)——基于配置中心實現集群配置的集中管理和熔斷機制》
《手把手0基礎項目實戰(七)——電商系統的日志監控方案》
《手把手0基礎項目實戰(八)——基于JMeter的系統性能測試》
幾乎所有的Web系統都需要登錄、權限管理、角色管理等功能,而且這些功能往往具有較大的普適性,與系統具體的業務關聯性較小。因此,這些功能完全可以被封裝成一個可配置、可插拔的框架,當開發一個新系統的時候直接將其引入、并作簡單配置即可,無需再從頭開發,極大節約了人力成本、時間成本。
在Java Web領域,有兩大主流的安全框架,Spring Security和Apache Shiro。他們都能實現用戶鑒權、權限管理、角色管理、防止Web攻擊等功能,而且這兩套開源框架都已經過大量項目的驗證,趨于穩定成熟,可以很好地為我們的項目服務。
本文將帶領大家從頭開始實現一套安全框架,該框架與Spring Boot深度融合,從而能夠幫助大家加深對Spring Boot的理解。這套框架中將涉及到如下內容:
Spring Boot AOP
Spring Boot 全局異常處理
Spring Boot CommandLineRunner
Java 反射機制
分布式系統中Session的集中式管理
本文將從安全框架的設計與實現兩個角度帶領大家完成安全框架的開發,廢話不多說,現在開始吧~
項目完整源碼下載https://github.com/bz51/Sprin...
1. 安全框架的設計 1.1 開發目標在所有事情開始之前,我們首先要搞清楚,我們究竟要實現哪些功能?
用戶登錄
所有系統都需要登錄功能,這毫無疑問,也不必多說。
角色管理
每個用戶都有且僅有一種角色,比如:系統管理員、普通用戶、企業用戶等等。管理員可以添加、刪除、查詢、修改角色信息。
權限管理
每種角色可以擁有不同的權限,管理員可以創建、修改、查詢、刪除權限,也可以為某一種角色添加、刪除權限。
權限檢測
用戶調用每一個接口,都需要校驗該用戶是否具備調用該接口的權限。
當我們明確了開發目標之后,下面就需要基于這些目標,設計我們的系統。我們首先要做的就是要搞清楚“用戶”、“角色”、“權限”的定義以及他們之間的關系。這在領域驅動設計中被稱為“領域模型”。
1.2 領域模型
權限:
權限表示某一用戶是否具有操作某一資源的能力。
權限一般用“資源名稱:操作名稱”來表示。比如:創建用戶的權限可以用“user:create”來表示,刪除用戶的權限可以用“user:delete”來表示。
在Web系統中,權限和接口呈一一對應關系,比如:“user:create”對應著創建用戶的接口,“user:delete”對應著刪除用戶的接口。因此,權限也可以理解成一個用戶是否具備操作某一個接口的能力。
角色:
角色是一組權限的集合。角色規定了某一類用戶共同具備的權限集合。
比如:超級管理員這種角色擁有“user:create”、“user:delete”等權限,而普通用戶只有“user:create”權限。
從領域模型中可知,角色和權限之間呈多對多的聚合關系,即一種角色可以包含多個權限,一個權限也可以屬于多種角色,并且權限可以脫離于角色而多帶帶存在,因此他們之間是一種弱依賴關系——聚合關系。
用戶:
用戶和角色之間呈多對一的聚合關系,即一個用戶只能屬于一種角色,而一種角色卻可以包含多個用戶。并且角色可以脫離于用戶多帶帶存在,因此他們之間是一種弱依賴關系——聚合關系。
1.3 數據結構設計當我們捋清楚了“權限”、“用戶”、“角色”的定義和他們之間的關系后,下面我們就可以基于這個領域模型設計出具體的數據存儲結構。
為了能夠方便地給每一個接口標注權限,我們需要自定義三個注解@Login、@Role和@Permission。
@Login:用于標識當前接口是否需要登錄。當接口使用了這個注解后,用戶只有在登錄后才能訪問。
@Role("角色名"):用于標識允許調用當前接口的角色。當接口使用了這個注解后,只有指定角色的用戶才能調用本接口。
@Permission("權限名"):用于標識允許調用當前接口的權限。當接口使用了這個注解后,只有具備指定權限的用戶才能調用本接口。
1.4 接口權限信息初始化流程要使得這個安全框架運行起來,首先就需要在系統初始化完成前,初始化所有接口的權限、角色等信息,這個過程即為“接口權限信息初始化流程”;然后在系統運行期間,如果有用戶請求接口,就可以根據這些權限信息判斷該用戶是否有權限訪問接口。
這一小節主要介紹接口權限信息初始化流程,不涉及任何實現細節,實現的細節將在本文的實現部分介紹。
當Spring完成上下文的初始化后,需要掃描本項目中所有Controller類;
再依次掃描Controller類中的所有方法,獲取方法上的@GetMapping、@PostMapping、@PutMapping和@DeleteMapping,通過這些注解獲取接口的URL、請求方式等信息;
同時,獲取方法上的@Login、@Role和@Permission,通過這些注解,獲取該接口是否需要登錄、允許訪問的角色以及允許訪問的權限信息;
將每個接口的權限信息、URL、請求方式存儲在Redis中,供用戶調用接口是鑒權使用。
1.5 用戶鑒權流程所有的用戶請求在被執行前都會被系統攔截,從請求中獲取請求的URL和請求方式;
然后從Redis中查詢該接口對應的權限信息;
若該接口需要登錄,并且當前用戶尚未登錄,則直接拒絕;
若該接口需要登錄,并且擁有已經登錄,那么需要從請求頭中解析出SessionID,并到Redis中查詢該用戶的權限信息,然后拿著用戶的權限信息、角色信息和該接口的權限信息、角色信息進行比對。若通過鑒權,則執行該接口;若未通過鑒權,則直接拒絕請求。
2. 安全框架的實現 2.1 注解的實現本套安全框架一共定義了四個注解:@AuthScan、@Login、@Role、@Permission。
2.1.1 @AuthScan該注解用來告訴安全框架,本項目中所有Controller類所在的包,從而能夠幫助安全框架快速找到Controller類,避免了所有類的掃描。
它有且僅有一個參數,用來指定Controller所在的包:@AuthScan("com.gaoxi.controller")。它的代碼實現如下:
</>復制代碼
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface AuthScan {
public String value();
}
注解顧名思義,它是用來在代碼中進行標注,它本身不承載任何邏輯,通過注解
@Retention
它解釋說明了這個注解的的存活時間。它的取值如下:
RetentionPolicy.SOURCE 注解只在源碼階段保留,在編譯器進行編譯時它將被丟棄忽視。
RetentionPolicy.CLASS 注解只被保留到編譯進行的時候,它并不會被加載到 JVM 中。
RetentionPolicy.RUNTIME 注解可以保留到程序運行的時候,它會被加載進入到 JVM 中,所以在程序運行時可以獲取到它們。
@Documented
顧名思義,這個元注解肯定是和文檔有關。它的作用是能夠將注解中的元素包含到 Javadoc 中去。
@Target
當一個注解被 @Target 注解時,這個注解就被限定了運用的場景。
ElementType.ANNOTATION_TYPE:可以給一個注解進行注解
ElementType.CONSTRUCTOR:可以給構造方法進行注解
ElementType.FIELD:可以給屬性進行注解
ElementType.LOCAL_VARIABLE:可以給局部變量進行注解
ElementType.METHOD:可以給方法進行注解
ElementType.PACKAGE:可以給一個包進行注解
ElementType.PARAMETER:可以給一個方法內的參數進行注解
ElementType.TYPE:可以給一個類型進行注解,比如類、接口、枚舉
2.1.2 @Login這個注解用于標識指定接口是否需要登錄后才能訪問,它有一個默認的boolean類型的值,用于表示是否需要登錄,其代碼如下:
</>復制代碼
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Login {
// 是否需要登錄(默認為true)
public boolean value() default true;
}
2.1.3 @Role
該注解用于指定允許訪問當前接口的角色,其代碼如下:
</>復制代碼
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Role {
public String value();
}
2.1.4 @Permission
該注解用于指定允許訪問當前接口的權限,其代碼如下:
</>復制代碼
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Permission {
public String value();
}
2.2 權限信息初始化過程
上文中提到,注解本身不含任何業務邏輯,它只是在代碼中起一個標識的作用,那么怎么才能讓注解“活”起來?這就需要通過反射機制來獲取注解。
2.2.1 在接口上聲明權限信息當完成這些注解的定義后,接下來就需要使用他們,如下面代碼所示:
</>復制代碼
public interface ProductController {
/**
* 創建產品
* @param prodInsertReq 產品詳情
* @return 是否創建成功
*/
@PostMapping("product")
@Login
@Permission("product:create")
public Result createProduct(ProdInsertReq prodInsertReq);
}
ProductController是一個Controller類,它提供了處理產品的各種接口。簡單起見,這里只列出了一個創建產品的接口。
@PostMapping是SpringMVC提供的注解,用于標識該接口的訪問路徑和訪問方式。
@Login聲明了該接口需要登錄后才能訪問。
@Permission聲明了用戶只有擁有product:create權限才能訪問該接口。
當系統初始化的時候,需要加載接口上的這些權限信息,存儲在Redis中。在系統運行期間,當有用戶請求接口的時候,系統會根據接口的權限信息判斷用戶是否有訪問接口的權限。權限信息初始化過程的代碼如下:
</>復制代碼
/**
* @author 大閑人柴毛毛
* @date 2017/11/1 上午10:04
*
* @description 初始化權限信息
*/
@AuthScan("com.gaoxi.controller")
@Component
public class InitAuth implements CommandLineRunner {
@Override
public void run(String... strings) throws Exception {
// 加載接口訪問權限
loadAccessAuth();
}
……
}
上述代碼定義了一個InitAuth類,該類實現了CommandLineRunner接口,該接口中含有run()方法,當Spring的上下文初始化完成后,就會調用run(),從而完成權限信息的初始化過程。
該類使用了@AuthScan("com.gaoxi.controller")注解,用于標識當前項目Controller類所在的包名,從而避免掃描所有類,一定程度上加速系統初始化的速度。
@Component注解會在Spring容器初始化完成后,創建本類的對象,并加入IoC容器中。
下面來看一下loadAccessAuth()方法的具體實現:
</>復制代碼
/**
* 加載接口訪問權限
*/
private void loadAccessAuth() throws IOException {
// 獲取待掃描的包名
AuthScan authScan = AnnotationUtil.getAnnotationValueByClass(this.getClass(), AuthScan.class);
String pkgName = authScan.value();
// 獲取包下所有類
List> classes = ClassUtil.getClasses(pkgName);
if (CollectionUtils.isEmpty(classes)) {
return;
}
// 遍歷類
for (Class clazz : classes) {
Method[] methods = clazz.getMethods();
if (methods==null || methods.length==0) {
continue;
}
// 遍歷函數
for (Method method : methods) {
AccessAuthEntity accessAuthEntity = buildAccessAuthEntity(method);
if (accessAuthEntity!=null) {
// 生成key
String key = generateKey(accessAuthEntity);
// 存至本地Map
accessAuthMap.put(key, accessAuthEntity);
logger.debug("",accessAuthEntity);
}
}
}
// 存至Redis
redisService.setMap(RedisPrefixUtil.Access_Auth_Prefix, accessAuthMap, null);
logger.info("接口訪問權限已加載完畢!"+accessAuthMap);
}
首先會讀取本類上的@AuthScan注解,并獲取注解中聲明了Controller類所在的包pkgName;
pkgName是一個字符串,因此需要使用Java反射機制將字符串解析成Class對象。其解析過程通過工具包ClassUtil.getClasses(pkgName)完成,具體解析過程這里就不做詳細介紹了,感興趣的同學可以參閱本項目源碼。
ClassUtil.getClasses(pkgName)解析之后,該包下的所有Controller類將會被解析成List
然后依次獲取每個Class對象中的Method對象,并依次遍歷Method對象,通過buildAccessAuthEntity(method)方法將一個個Method對象解析成AccessAuthEntity對象(具體解析過程在稍后介紹);
最后將AccessAuthEntity對象存儲在Redis中,供用戶訪問接口時使用。
這就是整個權限信息初始化的過程,下面詳細介紹buildAccessAuthEntity(method)方法的解析過程,它究竟是如何將一個Mehtod對象解析成AccessAuthEntity對象?并且AccessAuthEntity對象的結構究竟是怎樣的?
首先來看一下AccessAuthEntity的數據結構:
</>復制代碼
/**
* @author 大閑人柴毛毛
* @date 2017/11/1 上午11:05
* @description 接口訪問權限的實體類
*/
public class AccessAuthEntity implements Serializable {
/** 請求 URL */
private String url;
/** 接口方法名 */
private String methodName;
/** HTTP 請求方式 */
private HttpMethodEnum httpMethodEnum;
/** 當前接口是否需要登錄 */
private boolean isLogin;
/** 當前接口的訪問權限 */
private String permission;
// setter/getter省略
}
AccessAuthEntity用于存儲一個接口的訪問路徑、訪問方式和權限信息。在系統初始化的時候,Controller類中的每個Mehtod對象都會被buildAccessAuthEntity()方法解析成AccessAuthEntity對象。buildAccessAuthEntity()方法的代碼如下所示:
</>復制代碼
/**
* 構造AccessAuthEntity對象
* @param method
* @return
*/
private AccessAuthEntity buildAccessAuthEntity(Method method) {
GetMapping getMapping = AnnotationUtil.getAnnotationValueByMethod(method, GetMapping.class);
PostMapping postMapping = AnnotationUtil.getAnnotationValueByMethod(method, PostMapping.class);
PutMapping putMapping= AnnotationUtil.getAnnotationValueByMethod(method, PutMapping.class);
DeleteMapping deleteMapping = AnnotationUtil.getAnnotationValueByMethod(method, DeleteMapping.class);
AccessAuthEntity accessAuthEntity = null;
if (getMapping!=null
&& getMapping.value()!=null
&& getMapping.value().length==1
&& StringUtils.isNotEmpty(getMapping.value()[0])) {
accessAuthEntity = new AccessAuthEntity();
accessAuthEntity.setHttpMethodEnum(HttpMethodEnum.GET);
accessAuthEntity.setUrl(trimUrl(getMapping.value()[0]));
}
else if (postMapping!=null
&& postMapping.value()!=null
&& postMapping.value().length==1
&& StringUtils.isNotEmpty(postMapping.value()[0])) {
accessAuthEntity = new AccessAuthEntity();
accessAuthEntity.setHttpMethodEnum(HttpMethodEnum.POST);
accessAuthEntity.setUrl(trimUrl(postMapping.value()[0]));
}
else if (putMapping!=null
&& putMapping.value()!=null
&& putMapping.value().length==1
&& StringUtils.isNotEmpty(putMapping.value()[0])) {
accessAuthEntity = new AccessAuthEntity();
accessAuthEntity.setHttpMethodEnum(HttpMethodEnum.PUT);
accessAuthEntity.setUrl(trimUrl(putMapping.value()[0]));
}
else if (deleteMapping!=null
&& deleteMapping.value()!=null
&& deleteMapping.value().length==1
&& StringUtils.isNotEmpty(deleteMapping.value()[0])) {
accessAuthEntity = new AccessAuthEntity();
accessAuthEntity.setHttpMethodEnum(HttpMethodEnum.DELETE);
accessAuthEntity.setUrl(trimUrl(deleteMapping.value()[0]));
}
// 解析@Login 和 @Permission
if (accessAuthEntity!=null) {
accessAuthEntity = getLoginAndPermission(method, accessAuthEntity);
accessAuthEntity.setMethodName(method.getName());
}
return accessAuthEntity;
}
該方法首先會獲取當前Method上的XXXMapping四個注解,通過解析這些注解能夠獲取到當前接口的訪問路徑和請求方式,并將這兩者存儲在AccessAuthEntity對象中。
然后通過getLoginAndPermission方法,解析當前Method對象中的@Login 和@Permission信息,其代碼如下所示:
</>復制代碼
/**
* 獲取指定方法上的@Login的值和@Permission的值
* @param method 目標方法
* @param accessAuthEntity
* @return
*/
private AccessAuthEntity getLoginAndPermission(Method method, AccessAuthEntity accessAuthEntity) {
// 獲取@Permission的值
Permission permission = AnnotationUtil.getAnnotationValueByMethod(method, Permission.class);
if (permission!=null && StringUtils.isNotEmpty(permission.value())) {
accessAuthEntity.setPermission(permission.value());
accessAuthEntity.setLogin(true);
return accessAuthEntity;
}
// 獲取@Login的值
Login login = AnnotationUtil.getAnnotationValueByMethod(method, Login.class);
if (login!=null) {
accessAuthEntity.setLogin(true);
}
accessAuthEntity.setLogin(false);
return accessAuthEntity;
}
該注解的解析過程由注解工具包AnnotationUtil.getAnnotationValueByMethod完成,具體的解析過程這里就不再贅述,感興趣的同學請參閱項目源碼。
到此為止,接口的訪問路徑、請求方式、是否需要登錄、權限信息都已經解析成一個個AccessAuthEntity對象,并以“請求方式+訪問路徑”作為key,存儲在Redis中。接口權限信息的初始化過程也就完成了!
2.2.3 用戶鑒權當用戶請求所有接口前,系統都應該攔截這些請求,只有在權限校驗通過的情況下才運行調用接口,否則直接拒絕請求。
基于上述需求,我們需要給Controller中所有方法執行前增加切面,并將用于權限校驗的代碼織入到該切面中,從而在方法執行前完成權限校驗。下面就詳細介紹在SpringBoot中AOP的使用。
首先,我們需要在項目的pom中引入AOP的依賴:
</>復制代碼
org.springframework.boot
spring-boot-starter-aop
創建切面類:
在類上必須添加@Aspect注解,用于標識當前類是一個AOP切面類
該類也必須添加@Component注解,讓Spring初始化完成后創建本類的對象,并加入IoC容器中
然后需要使用@Pointcut注解定義切點;切點描述了哪些類中的哪些方法需要織入權限校驗代碼。我們這里將所有Controller類中的所有方法作為切點。
當完成切點的定義后,我們需要使用@Before注解聲明切面織入的時機;由于我們需要在方法執行前攔截所有的請求,因此使用@Before注解。
當完成上述設置之后,所有Controller類中的函數在被調用前,都會執行權限校驗代碼。權限校驗的詳細過程在authentication()方法中完成。
</>復制代碼
/**
* @author 大閑人柴毛毛
* @date 2017/11/2 下午7:06
*
* @description 訪問權限處理類(所有請求都要經過此類)
*/
@Aspect
@Component
public class AccessAuthHandle {
/** 定義切點 */
@Pointcut("execution(public * com.gaoxi.controller..*.*(..))")
public void accessAuth(){}
/**
* 攔截所有請求
*/
@Before("accessAuth()")
public void doBefore() {
// 訪問鑒權
authentication();
}
}
權限校驗過程
該方法首先會獲取當前請求的訪問路徑和請求方法;
然后獲取HTTP請求頭中的SessionID,并從Redis中獲取該SessionID對應的用戶信息;
然后根據接口訪問路徑和訪問方法,從Redis中獲取該接口的權限信息;到此為止,權限校驗前的準備工作都已完成,下面就要進入權限校驗過程了;
</>復制代碼
/**
* 檢查當前用戶是否允許訪問該接口
*/
private void authentication() {
// 獲取 HttpServletRequest
HttpServletRequest request = getHttpServletRequest();
// 獲取 method 和 url
String method = request.getMethod();
String url = request.getServletPath();
// 獲取 SessionID
String sessionID = getSessionID(request);
// 獲取SessionID對應的用戶信息
UserEntity userEntity = getUserEntity(sessionID);
// 獲取接口權限信息
AccessAuthEntity accessAuthEntity = getAccessAuthEntity(method, url);
// 檢查權限
authentication(userEntity, accessAuthEntity);
}
authentication():
首先判斷當前接口是否需要登錄后才允許訪問,如果無需登錄,那么直接允許訪問;
若當前接口需要登錄后才能訪問,那么判斷當前用戶是否已經登錄;若尚未登錄,則直接拒絕請求(通過拋出throw new CommonBizException(ExpCodeEnum.NO_PERMISSION)異常來拒絕請求,這由SpringBoot統一異常處理機制來完成,稍后會詳細介紹);若已經登錄,則開始檢查權限信息;
權限檢查由checkPermission()方法完成,它會將用戶所具備的權限和接口要求的權限進行比對;如果用戶所具備的權限包含接口要求的權限,那么權限校驗通過;反之,則通過拋異常的方式拒絕請求。
</>復制代碼
/**
* 檢查權限
* @param userEntity 當前用戶的信息
* @param accessAuthEntity 當前接口的訪問權限
*/
private void authentication(UserEntity userEntity, AccessAuthEntity accessAuthEntity) {
// 無需登錄
if (!accessAuthEntity.isLogin()) {
return;
}
// 檢查是否登錄
checkLogin(userEntity, accessAuthEntity);
// 檢查是否擁有權限
checkPermission(userEntity, accessAuthEntity);
}
/**
* 檢查當前用戶是否擁有訪問該接口的權限
* @param userEntity 用戶信息
* @param accessAuthEntity 接口權限信息
*/
private void checkPermission(UserEntity userEntity, AccessAuthEntity accessAuthEntity) {
// 獲取接口權限
String accessPermission = accessAuthEntity.getPermission();
// 獲取用戶權限
List userPermissionList = userEntity.getRoleEntity().getPermissionList();
// 判斷用戶是否包含接口權限
if (CollectionUtils.isNotEmpty(userPermissionList)) {
for (PermissionEntity permissionEntity : userPermissionList) {
if (permissionEntity.getPermission().equals(accessPermission)) {
return;
}
}
}
// 沒有權限
throw new CommonBizException(ExpCodeEnum.NO_PERMISSION);
}
/**
* 檢查當前接口是否需要登錄
* @param userEntity 用戶信息
* @param accessAuthEntity 接口訪問權限
*/
private void checkLogin(UserEntity userEntity, AccessAuthEntity accessAuthEntity) {
// 尚未登錄
if (accessAuthEntity.isLogin() && userEntity==null) {
throw new CommonBizException(ExpCodeEnum.UNLOGIN);
}
}
全局異常處理
為了是得代碼具備良好的可讀性,這里使用了SpringBoot提供的全局異常處理機制。我們只需拋出異常即可,這些異常會被我們預先設置的全局異常處理類捕獲并處理。全局異常處理本質上借助于AOP完成。
我們需要定義全局異常處理類,它只是一個普通類,我們只要用@ControllerAdvice注解聲明即可
我們還需要在這個類上增加@ResponseBody注解,它能夠幫助我們當處理完異常后,直接向用戶返回JSON格式的錯誤信息,而無需我們手動處理。
在這個類中,我們根據異常類型不同,定義了兩個異常處理函數,分別用于捕獲業務異常、系統異常。并且需要使用@ExceptionHandler注解告訴Spring,該方法用于處理什么類型的異常。
當我們完成上述配置后,只要項目中任何地方拋出異常,都會被這個全局異常處理類捕獲,并根據拋出異常的類型選擇相應的異常處理函數。
</>復制代碼
/**
* @Author 大閑人柴毛毛
* @Date 2017/10/27 下午11:02
* REST接口的通用異常處理
*/
@ControllerAdvice
@ResponseBody
public class ExceptionHandle {
private final Logger logger = LoggerFactory.getLogger(this.getClass());
/**
* 業務異常處理
* @param exception
* @param
* @return
*/
@ExceptionHandler(CommonBizException.class)
public Result exceptionHandler(CommonBizException exception) {
return Result.newFailureResult(exception);
}
/**
* 系統異常處理
* @param exception
* @return
*/
@ExceptionHandler(Exception.class)
public Result sysExpHandler(Exception exception) {
logger.error("系統異常 ",exception);
return Result.newFailureResult();
}
}
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/68807.html
摘要:淺談秒殺系統架構設計后端掘金秒殺是電子商務網站常見的一種營銷手段。這兩個項目白話網站架構演進后端掘金這是白話系列的文章。 淺談秒殺系統架構設計 - 后端 - 掘金秒殺是電子商務網站常見的一種營銷手段。 不要整個系統宕機。 即使系統故障,也不要將錯誤數據展示出來。 盡量保持公平公正。 實現效果 秒殺開始前,搶購按鈕為活動未開始。 秒殺開始時,搶購按鈕可以點擊下單。 秒殺結束后,按鈕按鈕變...
摘要:系列教程手把手教你寫電商爬蟲第一課找個軟柿子捏捏手把手教你寫電商爬蟲第二課實戰尚妝網分頁商品采集爬蟲看完兩篇,相信大家已經從開始的小菜鳥晉升為中級菜鳥了,好了,那我們就繼續我們的爬蟲課程。 系列教程: 手把手教你寫電商爬蟲-第一課 找個軟柿子捏捏手把手教你寫電商爬蟲-第二課 實戰尚妝網分頁商品采集爬蟲 看完兩篇,相信大家已經從開始的小菜鳥晉升為中級菜鳥了,好了,那我們就繼續我們的爬蟲課...
摘要:系列教程手把手教你寫電商爬蟲第一課找個軟柿子捏捏手把手教你寫電商爬蟲第二課實戰尚妝網分頁商品采集爬蟲看完兩篇,相信大家已經從開始的小菜鳥晉升為中級菜鳥了,好了,那我們就繼續我們的爬蟲課程。 系列教程: 手把手教你寫電商爬蟲-第一課 找個軟柿子捏捏手把手教你寫電商爬蟲-第二課 實戰尚妝網分頁商品采集爬蟲 看完兩篇,相信大家已經從開始的小菜鳥晉升為中級菜鳥了,好了,那我們就繼續我們的爬蟲課...
摘要:剩下的同學,我們繼續了可以看出,作為一個完善的電商網站,尚妝網有著普通電商網站所擁有的主要的元素,包括分類,分頁,主題等等。 系列教程 手把手教你寫電商爬蟲-第一課 找個軟柿子捏捏 如果沒有看過第一課的朋友,請先移步第一課,第一課講了一些基礎性的東西,通過軟柿子切糕王子這個電商網站好好的練了一次手,相信大家都應該對寫爬蟲的流程有了一個大概的了解,那么這課咱們就話不多說,正式上戰場,對壘...
摘要:剩下的同學,我們繼續了可以看出,作為一個完善的電商網站,尚妝網有著普通電商網站所擁有的主要的元素,包括分類,分頁,主題等等。 系列教程 手把手教你寫電商爬蟲-第一課 找個軟柿子捏捏 如果沒有看過第一課的朋友,請先移步第一課,第一課講了一些基礎性的東西,通過軟柿子切糕王子這個電商網站好好的練了一次手,相信大家都應該對寫爬蟲的流程有了一個大概的了解,那么這課咱們就話不多說,正式上戰場,對壘...
閱讀 1827·2023-04-26 02:51
閱讀 2859·2021-09-10 10:50
閱讀 3056·2021-09-01 10:48
閱讀 3615·2019-08-30 15:53
閱讀 1823·2019-08-29 18:40
閱讀 410·2019-08-29 16:16
閱讀 2033·2019-08-29 13:21
閱讀 1822·2019-08-29 11:07