开篇语
如果说接口设计是艺术,那么抽象类的设计就是工程哲学的体现。在软件设计中,有一类经典问题:"算法的骨架是固定的,但具体步骤的实现各不相同"。
OAuth认证流程就是这样一个典型场景:无论是GitHub、Google还是微信,所有平台的认证流程都遵循相同的模式------获取授权码 → 换取访问令牌 → 获取用户信息,但每个平台的API细节、参数格式、响应结构都截然不同。
JustAuth的AuthDefaultRequest
抽象类通过模板方法模式,优雅地解决了这个"一样的流程,不同的实现"问题。这个设计不仅统一平台的接入方式,更展现了模板方法模式在复杂业务场景中的威力。
本期将带你深入AuthDefaultRequest
的源码世界,掌握模板方法模式的精髓,学会设计既灵活又统一的抽象框架。
一、模板方法模式深度解析
1.1 模板方法模式的本质理解
模板方法模式的核心思想是"Don't call us, we'll call you"(好莱坞原则),即由框架控制流程,具体实现只需关注自己的职责。
经典模板方法结构
java
// 模板方法模式的经典结构
public abstract class AbstractClass {
// 模板方法:定义算法骨架
public final void templateMethod() {
primitiveOperation1(); // 基本操作1
primitiveOperation2(); // 基本操作2
hook(); // 钩子方法
}
// 抽象方法:子类必须实现
protected abstract void primitiveOperation1();
protected abstract void primitiveOperation2();
// 钩子方法:子类可选实现
protected void hook() {
// 默认实现或空实现
}
}
JustAuth中的模板方法实现映射
java
// JustAuth的模板方法模式实现
public abstract class AuthDefaultRequest implements AuthRequest {
// 模板方法:定义OAuth认证算法骨架
@Override
public AuthResponse<AuthUser> login(AuthCallback authCallback) {
try {
checkCode(authCallback); // 步骤1:参数校验
if (!config.isIgnoreCheckState()) { // 步骤2:状态校验(钩子)
AuthChecker.checkState(authCallback.getState(), source, authStateCache);
}
AuthToken authToken = this.getAccessToken(authCallback); // 步骤3:获取令牌(抽象)
AuthUser user = this.getUserInfo(authToken); // 步骤4:获取用户(抽象)
return AuthResponse.<AuthUser>builder() // 步骤5:封装响应
.code(AuthResponseStatus.SUCCESS.getCode())
.data(user)
.build();
} catch (Exception e) {
Log.error("Failed to login with oauth authorization.", e);
return this.responseError(e); // 异常处理
}
}
// 抽象方法:子类必须实现
public abstract AuthToken getAccessToken(AuthCallback authCallback);
public abstract AuthUser getUserInfo(AuthToken authToken);
// 钩子方法:子类可选覆盖
protected void checkCode(AuthCallback authCallback) {
AuthChecker.checkCode(source, authCallback);
}
}
1.2 算法骨架的抽象艺术
OAuth标准流程的抽象层次
scss
┌─────────────────────────┐
│ login(模板方法) │
└─────────────────────────┘
│
┌────────────────┼────────────────┐
│ │ │
┌─────────┐ ┌──────────┐ ┌──────────┐
│参数校验 │ │获取访问令牌│ │获取用户信息│
│checkCode│ │getAccessToken│ │getUserInfo│
└─────────┘ └──────────┘ └──────────┘
│ │ │
┌─────────┐ ┌──────────┐ ┌──────────┐
│钩子方法 │ │抽象方法 │ │抽象方法 │
│可覆盖 │ │必须实现 │ │必须实现 │
└─────────┘ └──────────┘ └──────────┘
抽象层次的设计原则
🎯 原则1:变与不变的清晰分离
java
public abstract class AuthDefaultRequest implements AuthRequest {
// 不变的部分:OAuth流程骨架
@Override
public AuthResponse<AuthUser> login(AuthCallback authCallback) {
// 固定流程:校验 → 获取令牌 → 获取用户 → 封装响应
// 这个流程所有OAuth平台都一样,不允许子类修改
}
// 变化的部分:平台特定实现
public abstract AuthToken getAccessToken(AuthCallback authCallback); // 各平台API不同
public abstract AuthUser getUserInfo(AuthToken authToken); // 各平台响应不同
// 半变化的部分:提供默认实现,允许定制
protected void checkCode(AuthCallback authCallback) {
// 大部分平台都一样,个别平台可以覆盖
AuthChecker.checkCode(source, authCallback);
}
}
🎯 原则2:职责边界的精确划分
java
// 每个抽象方法都有明确的输入输出和职责边界
public abstract class AuthDefaultRequest implements AuthRequest {
/**
* 获取访问令牌
*
* 职责边界:
* - 输入:OAuth授权回调参数
* - 输出:标准化的AuthToken对象
* - 职责:与平台API交互,获取访问令牌
* - 不负责:参数校验(模板方法已处理)
* - 不负责:异常处理(模板方法统一处理)
*/
public abstract AuthToken getAccessToken(AuthCallback authCallback);
/**
* 获取用户信息
*
* 职责边界:
* - 输入:标准化的AuthToken对象
* - 输出:标准化的AuthUser对象
* - 职责:使用令牌获取用户信息并转换为统一格式
* - 不负责:令牌有效性验证(由平台API返回错误)
* - 不负责:用户信息缓存(上层业务决定)
*/
public abstract AuthUser getUserInfo(AuthToken authToken);
}
1.3 变与不变的分离哲学
不变的核心:OAuth算法骨架
java
// OAuth认证的不变算法骨架
public final AuthResponse<AuthUser> login(AuthCallback authCallback) {
// 第1步:输入验证(所有平台都需要)
checkCode(authCallback);
// 第2步:状态校验(安全要求,所有平台都需要)
if (!config.isIgnoreCheckState()) {
AuthChecker.checkState(authCallback.getState(), source, authStateCache);
}
// 第3步:获取访问令牌(实现各不相同)
AuthToken authToken = this.getAccessToken(authCallback);
// 第4步:获取用户信息(实现各不相同)
AuthUser user = this.getUserInfo(authToken);
// 第5步:响应封装(所有平台都一样)
return AuthResponse.<AuthUser>builder()
.code(AuthResponseStatus.SUCCESS.getCode())
.data(user)
.build();
}
变化的细节:平台特定实现
java
// GitHub平台的特定实现
public class AuthGithubRequest extends AuthDefaultRequest {
@Override
public AuthToken getAccessToken(AuthCallback authCallback) {
// GitHub特有的令牌获取方式
String response = doPostAuthorizationCode(authCallback.getCode());
Map<String, String> res = GlobalAuthUtils.parseStringToMap(response);
this.checkResponse(res.containsKey("error"), res.get("error_description"));
return AuthToken.builder()
.accessToken(res.get("access_token"))
.scope(res.get("scope"))
.tokenType(res.get("token_type"))
.build();
}
@Override
public AuthUser getUserInfo(AuthToken authToken) {
// GitHub特有的用户信息获取方式
HttpHeader header = new HttpHeader();
header.add("Authorization", "token " + authToken.getAccessToken());
String response = new HttpUtils(config.getHttpConfig())
.get(UrlBuilder.fromBaseUrl(source.userInfo()).build(), null, header, false)
.getBody();
JSONObject object = JSONObject.parseObject(response);
this.checkResponse(object.containsKey("error"), object.getString("error_description"));
// GitHub API响应的特定字段映射
return AuthUser.builder()
.rawUserInfo(object)
.uuid(object.getString("id"))
.username(object.getString("login")) // GitHub用login字段
.avatar(object.getString("avatar_url"))
.blog(object.getString("blog"))
.nickname(object.getString("name"))
.company(object.getString("company"))
.location(object.getString("location"))
.email(object.getString("email"))
.remark(object.getString("bio")) // GitHub用bio字段表示个人简介
.gender(AuthUserGender.UNKNOWN) // GitHub不提供性别信息
.token(authToken)
.source(source.toString())
.build();
}
}
// 对比:微信平台的不同实现
public class AuthWeChatRequest extends AuthDefaultRequest {
@Override
public AuthToken getAccessToken(AuthCallback authCallback) {
// 微信特有的令牌获取方式(POST JSON格式)
String response = doPostAuthorizationCode(authCallback.getCode());
JSONObject accessTokenObject = JSONObject.parseObject(response);
this.checkResponse(accessTokenObject);
return AuthToken.builder()
.accessToken(accessTokenObject.getString("access_token"))
.refreshToken(accessTokenObject.getString("refresh_token"))
.expireIn(accessTokenObject.getIntValue("expires_in"))
.openId(accessTokenObject.getString("openid")) // 微信特有字段
.scope(accessTokenObject.getString("scope"))
.build();
}
@Override
public AuthUser getUserInfo(AuthToken authToken) {
// 微信特有的用户信息获取方式
String response = doGetUserInfo(authToken);
JSONObject object = JSONObject.parseObject(response);
this.checkResponse(object);
// 微信API响应的特定字段映射
return AuthUser.builder()
.rawUserInfo(object)
.uuid(object.getString("openid"))
.username(object.getString("nickname")) // 微信用nickname字段
.nickname(object.getString("nickname"))
.avatar(object.getString("headimgurl")) // 微信用headimgurl字段
.location(object.getString("country")) // 微信提供country/province/city
.gender(getRealGender(object)) // 微信提供性别信息
.token(authToken)
.source(source.toString())
.build();
}
}
1.4 钩子方法的设计技巧
钩子方法(Hook Method)是模板方法模式中的高级技巧,它在算法的关键节点提供扩展点,让子类可以影响算法的执行。
钩子方法的四种应用模式
🪝 模式1:条件控制钩子
java
public abstract class AuthDefaultRequest implements AuthRequest {
@Override
public AuthResponse<AuthUser> login(AuthCallback authCallback) {
checkCode(authCallback);
// 条件控制钩子:允许某些平台跳过状态验证
if (!config.isIgnoreCheckState()) {
AuthChecker.checkState(authCallback.getState(), source, authStateCache);
}
AuthToken authToken = this.getAccessToken(authCallback);
AuthUser user = this.getUserInfo(authToken);
return AuthResponse.<AuthUser>builder()
.code(AuthResponseStatus.SUCCESS.getCode())
.data(user)
.build();
}
// 钩子方法:子类可以通过配置控制是否跳过状态验证
protected boolean shouldCheckState() {
return !config.isIgnoreCheckState();
}
}
🪝 模式2:扩展点钩子
java
public abstract class AuthDefaultRequest implements AuthRequest {
@Override
public String authorize(String state) {
String baseUrl = UrlBuilder.fromBaseUrl(source.authorize())
.queryParam("response_type", "code")
.queryParam("client_id", config.getClientId())
.queryParam("redirect_uri", config.getRedirectUri())
.queryParam("state", getRealState(state))
.build();
// 扩展点钩子:允许子类添加额外的URL参数
return customizeAuthorizeUrl(baseUrl);
}
// 钩子方法:默认不做任何处理,子类可以覆盖
protected String customizeAuthorizeUrl(String url) {
return url;
}
}
// GitHub平台利用钩子添加scope参数
public class AuthGithubRequest extends AuthDefaultRequest {
@Override
protected String customizeAuthorizeUrl(String url) {
return UrlBuilder.fromBaseUrl(url)
.queryParam("scope", this.getScopes(" ", true,
AuthScopeUtils.getDefaultScopes(AuthGithubScope.values())))
.build();
}
}
🪝 模式3:回调通知钩子
java
public abstract class AuthDefaultRequest implements AuthRequest {
@Override
public AuthResponse<AuthUser> login(AuthCallback authCallback) {
try {
checkCode(authCallback);
// 开始登录回调
onLoginStart(authCallback);
AuthToken authToken = this.getAccessToken(authCallback);
// 获取令牌成功回调
onTokenObtained(authToken);
AuthUser user = this.getUserInfo(authToken);
// 登录成功回调
onLoginSuccess(user);
return AuthResponse.<AuthUser>builder()
.code(AuthResponseStatus.SUCCESS.getCode())
.data(user)
.build();
} catch (Exception e) {
// 登录失败回调
onLoginFailure(e);
throw e;
}
}
// 回调钩子方法:子类可以覆盖实现监控、日志等功能
protected void onLoginStart(AuthCallback authCallback) {}
protected void onTokenObtained(AuthToken authToken) {}
protected void onLoginSuccess(AuthUser user) {}
protected void onLoginFailure(Exception e) {}
}
🪝 模式4:策略选择钩子
java
public abstract class AuthDefaultRequest implements AuthRequest {
protected String accessTokenUrl(String code) {
UrlBuilder builder = UrlBuilder.fromBaseUrl(source.accessToken())
.queryParam("code", code)
.queryParam("client_id", config.getClientId())
.queryParam("client_secret", config.getClientSecret())
.queryParam("grant_type", "authorization_code")
.queryParam("redirect_uri", config.getRedirectUri());
// 策略选择钩子:允许子类选择不同的参数传递方式
return customizeAccessTokenUrl(builder).build();
}
// 钩子方法:子类可以选择不同的URL构建策略
protected UrlBuilder customizeAccessTokenUrl(UrlBuilder builder) {
return builder;
}
// 通用的令牌获取方法
protected String doPostAuthorizationCode(String code) {
return new HttpUtils(config.getHttpConfig()).post(accessTokenUrl(code)).getBody();
}
}
// 某些平台需要特殊的参数处理
public class AuthSpecialPlatformRequest extends AuthDefaultRequest {
@Override
protected UrlBuilder customizeAccessTokenUrl(UrlBuilder builder) {
// 某些平台需要额外的参数或特殊的参数格式
return builder
.queryParam("platform", "mobile") // 特殊平台标识
.queryParam("version", "2.0"); // API版本
}
}
二、AuthDefaultRequest实现深度解读
2.1 类结构与继承体系分析
继承体系图
kotlin
┌─────────────────┐
│ AuthRequest │ ← 接口:定义契约
│ (interface) │
└─────────────────┘
△
│ implements
│
┌─────────────────┐
│AuthDefaultRequest│ ← 抽象类:模板方法实现
│ (abstract class)│
└─────────────────┘
△
│ extends
┌────┼────┐
│ │ │
┌─────┐┌─────┐┌─────┐
│GitHub││WeChat││ ... │ ← 具体实现:平台特定逻辑
└─────┘└─────┘└─────┘
核心字段设计分析
java
public abstract class AuthDefaultRequest implements AuthRequest {
protected AuthConfig config; // 配置信息:客户端ID、密钥等
protected AuthSource source; // 平台信息:API地址、平台名称等
protected AuthStateCache authStateCache; // 状态缓存:CSRF防护
// 设计思考:
// 1. 为什么用protected而不是private?
// - 允许子类直接访问,提高性能
// - 避免getter方法的开销
// - 符合模板方法模式的设计原则
// 2. 为什么需要AuthStateCache?
// - OAuth的安全要求:防止CSRF攻击
// - 状态管理:跟踪授权流程的状态
// - 可扩展性:支持不同的缓存实现
}
2.2 模板方法login的精妙设计
完整的login方法解析
java
@Override
public AuthResponse<AuthUser> login(AuthCallback authCallback) {
try {
// 第1步:参数校验 - 统一的输入验证
checkCode(authCallback);
// 第2步:状态校验 - 可配置的安全检查
if (!config.isIgnoreCheckState()) {
AuthChecker.checkState(authCallback.getState(), source, authStateCache);
}
// 第3步:获取令牌 - 抽象方法,平台特定实现
AuthToken authToken = this.getAccessToken(authCallback);
// 第4步:获取用户 - 抽象方法,平台特定实现
AuthUser user = this.getUserInfo(authToken);
// 第5步:响应封装 - 统一的成功响应格式
return AuthResponse.<AuthUser>builder()
.code(AuthResponseStatus.SUCCESS.getCode())
.data(user)
.build();
} catch (Exception e) {
// 异常处理 - 统一的错误处理机制
Log.error("Failed to login with oauth authorization.", e);
return this.responseError(e);
}
}
设计精妙之处分析
🎯 精妙点1:步骤的逻辑顺序
java
// ✅ 正确的步骤顺序:先验证,再处理
checkCode(authCallback); // 1. 基础参数验证
if (!config.isIgnoreCheckState()) { // 2. 安全状态验证
AuthChecker.checkState(...);
}
AuthToken authToken = this.getAccessToken(authCallback); // 3. 获取令牌
AuthUser user = this.getUserInfo(authToken); // 4. 获取用户
// ❌ 错误的顺序示例:
AuthToken authToken = this.getAccessToken(authCallback); // 先处理业务
checkCode(authCallback); // 后验证参数(浪费资源)
🎯 精妙点2:异常处理的位置
java
// ✅ 在模板方法中统一处理异常
@Override
public AuthResponse<AuthUser> login(AuthCallback authCallback) {
try {
// 所有业务逻辑
return AuthResponse.success(user);
} catch (Exception e) {
// 统一异常处理:日志记录 + 响应转换
Log.error("Failed to login with oauth authorization.", e);
return this.responseError(e);
}
}
// ✅ 子类只需要抛出异常,无需处理
@Override
public AuthToken getAccessToken(AuthCallback authCallback) {
String response = doPostAuthorizationCode(authCallback.getCode());
if (response.contains("error")) {
throw new AuthException("Failed to get access token"); // 直接抛出
}
return parseToken(response);
}
// ❌ 错误做法:子类自己处理异常
@Override
public AuthToken getAccessToken(AuthCallback authCallback) {
try {
String response = doPostAuthorizationCode(authCallback.getCode());
return parseToken(response);
} catch (Exception e) {
// 不应该在这里处理异常,会破坏统一性
return null;
}
}
🎯 精妙点3:可配置的灵活性
java
// 灵活的配置支持
if (!config.isIgnoreCheckState()) {
AuthChecker.checkState(authCallback.getState(), source, authStateCache);
}
// 这种设计的好处:
// ✅ 生产环境:启用状态验证,确保安全
// ✅ 测试环境:可以跳过状态验证,简化测试
// ✅ 特定平台:某些平台可能不支持state参数
2.3 工具方法的设计智慧
URL构建工具方法
java
// 访问令牌URL构建
protected String accessTokenUrl(String code) {
return UrlBuilder.fromBaseUrl(source.accessToken())
.queryParam("code", code)
.queryParam("client_id", config.getClientId())
.queryParam("client_secret", config.getClientSecret())
.queryParam("grant_type", "authorization_code")
.queryParam("redirect_uri", config.getRedirectUri())
.build();
}
// 用户信息URL构建
protected String userInfoUrl(AuthToken authToken) {
return UrlBuilder.fromBaseUrl(source.userInfo())
.queryParam("access_token", authToken.getAccessToken())
.build();
}
// 刷新令牌URL构建
protected String refreshTokenUrl(String refreshToken) {
return UrlBuilder.fromBaseUrl(source.refresh())
.queryParam("client_id", config.getClientId())
.queryParam("client_secret", config.getClientSecret())
.queryParam("refresh_token", refreshToken)
.queryParam("grant_type", "refresh_token")
.queryParam("redirect_uri", config.getRedirectUri())
.build();
}
设计智慧分析
🧠 智慧1:模板化的URL构建
java
// 每个URL构建方法都遵循相同的模式:
// 1. 从AuthSource获取基础URL
// 2. 添加OAuth标准参数
// 3. 添加平台特定参数
// 4. 构建最终URL
// 这种设计的好处:
// ✅ 统一性:所有平台都用相同的方式构建URL
// ✅ 可维护性:URL构建逻辑集中管理
// ✅ 可测试性:可以独立测试URL构建逻辑
// ✅ 可扩展性:子类可以覆盖特定的URL构建方法
🧠 智慧2:HTTP请求的抽象封装
java
// 通用的POST请求方法
protected String doPostAuthorizationCode(String code) {
return new HttpUtils(config.getHttpConfig()).post(accessTokenUrl(code)).getBody();
}
// 通用的GET请求方法
protected String doGetAuthorizationCode(String code) {
return new HttpUtils(config.getHttpConfig()).get(accessTokenUrl(code)).getBody();
}
// 通用的用户信息获取方法
protected String doGetUserInfo(AuthToken authToken) {
return new HttpUtils(config.getHttpConfig()).get(userInfoUrl(authToken)).getBody();
}
// 设计分析:
// ✅ 封装了HTTP配置的传递
// ✅ 统一了请求方式的选择
// ✅ 简化了子类的实现复杂度
// ✅ 支持HTTP配置的统一管理(超时、代理等)
🧠 智慧3:状态管理的安全实现
java
protected String getRealState(String state) {
if (StringUtils.isEmpty(state)) {
state = UuidUtils.getUUID(); // 自动生成随机state
}
// 缓存state,用于后续验证
authStateCache.cache(state, state);
return state;
}
// 这个方法的安全设计:
// ✅ 自动生成:用户不提供state时自动生成
// ✅ 唯一性:使用UUID确保state的唯一性
// ✅ 缓存验证:将state存入缓存,回调时验证
// ✅ CSRF防护:防止跨站请求伪造攻击
Scope处理的巧妙设计
java
protected String getScopes(String separator, boolean encode, List<String> defaultScopes) {
List<String> scopes = config.getScopes();
// 使用默认scope
if (null == scopes || scopes.isEmpty()) {
if (null == defaultScopes || defaultScopes.isEmpty()) {
return "";
}
scopes = defaultScopes;
}
// 处理分隔符
if (null == separator) {
separator = " "; // OAuth标准默认使用空格
}
// 拼接和编码
String scopeStr = String.join(separator, scopes);
return encode ? UrlUtil.urlEncode(scopeStr) : scopeStr;
}
// 使用示例(GitHub平台):
@Override
public String authorize(String state) {
return UrlBuilder.fromBaseUrl(super.authorize(state))
.queryParam("scope", this.getScopes(" ", true,
AuthScopeUtils.getDefaultScopes(AuthGithubScope.values())))
.build();
}
设计精妙之处:
- 配置优先:优先使用用户配置的scope
- 默认回退:用户未配置时使用平台默认scope
- 格式灵活:支持不同平台的分隔符要求
- 编码处理:可选的URL编码支持
- 空值安全:优雅处理各种边界情况
三、异常处理机制深度分析
3.1 统一异常处理策略
JustAuth的异常处理体现了"集中处理,分层抛出"的设计理念,通过模板方法统一处理异常,子类只需专注业务逻辑。
异常处理的层次结构
scss
┌─────────────────────┐
│ 模板方法层 │
│ (统一异常处理) │
└─────────────────────┘
│
┌─────────┴─────────┐
│ │
┌───────────────┐ ┌───────────────┐
│ 业务逻辑层 │ │ 工具方法层 │
│ (抛出业务异常) │ │ (抛出技术异常) │
└───────────────┘ └───────────────┘
│ │
┌───────────────┐ ┌───────────────┐
│ 平台API层 │ │ HTTP请求层 │
│ (API错误) │ │ (网络错误) │
└───────────────┘ └───────────────┘
异常转换机制
java
// 模板方法中的统一异常处理
@Override
public AuthResponse<AuthUser> login(AuthCallback authCallback) {
try {
// 业务逻辑执行
checkCode(authCallback);
AuthToken authToken = this.getAccessToken(authCallback);
AuthUser user = this.getUserInfo(authToken);
return AuthResponse.success(user);
} catch (Exception e) {
Log.error("Failed to login with oauth authorization.", e);
return this.responseError(e); // 异常转换为响应对象
}
}
// 异常转换方法的精妙实现
AuthResponse<AuthUser> responseError(Exception e) {
int errorCode = AuthResponseStatus.FAILURE.getCode();
String errorMsg = e.getMessage();
// 特殊异常类型的处理
if (e instanceof AuthException) {
AuthException authException = ((AuthException) e);
errorCode = authException.getErrorCode();
if (StringUtils.isNotEmpty(authException.getErrorMsg())) {
errorMsg = authException.getErrorMsg();
}
}
return AuthResponse.<AuthUser>builder()
.code(errorCode)
.msg(errorMsg)
.build();
}
3.2 错误码设计规范
AuthResponseStatus枚举设计分析
java
@Getter
@AllArgsConstructor
public enum AuthResponseStatus {
// 成功状态
SUCCESS(2000, "Success"),
// 通用错误 (5000-5099)
FAILURE(5000, "Failure"),
NOT_IMPLEMENTED(5001, "Not Implemented"),
PARAMETER_INCOMPLETE(5002, "Parameter incomplete"),
UNSUPPORTED(5003, "Unsupported operation"),
// 配置错误 (5004-5009)
NO_AUTH_SOURCE(5004, "AuthDefaultSource cannot be null"),
UNIDENTIFIED_PLATFORM(5005, "Unidentified platform"),
ILLEGAL_REDIRECT_URI(5006, "Illegal redirect uri"),
ILLEGAL_REQUEST(5007, "Illegal request"),
// 参数错误 (5008-5019)
ILLEGAL_CODE(5008, "Illegal code"),
ILLEGAL_STATUS(5009, "Illegal state"),
REQUIRED_REFRESH_TOKEN(5010, "The refresh token is required; it must not be null"),
ILLEGAL_TOKEN(5011, "Invalid token"),
ILLEGAL_KID(5012, "Invalid key identifier(kid)"),
ILLEGAL_TEAM_ID(5013, "Invalid team id"),
ILLEGAL_CLIENT_ID(5014, "Invalid client id"),
ILLEGAL_CLIENT_SECRET(5015, "Invalid client secret"),
ILLEGAL_WECHAT_AGENT_ID(5016, "Illegal wechat agent id"),
;
private final int code;
private final String msg;
}
错误码设计的最佳实践
🎯 实践1:分类编码策略
java
// 错误码的分层设计
// 2xxx: 成功状态
// 5000-5099: 通用错误
// 5100-5199: 配置相关错误
// 5200-5299: 参数验证错误
// 5300-5399: 平台API错误
// 5400-5499: 网络请求错误
// 这种分类的好处:
// ✅ 快速定位:通过错误码快速判断错误类型
// ✅ 统计分析:可以按错误类型进行统计
// ✅ 监控告警:不同类型错误设置不同告警级别
// ✅ 扩展性:新增错误类型时有明确的编码规范
🎯 实践2:错误信息的国际化准备
java
// 当前设计已考虑国际化扩展
public enum AuthResponseStatus {
ILLEGAL_CODE(5008, "Illegal code"),
// 未来可以扩展为:
// ILLEGAL_CODE(5008, "error.illegal.code"), // 使用国际化key
private final int code;
private final String msg; // 可以是国际化key
}
// 国际化处理器
public class I18nErrorHandler {
public static String getLocalizedMessage(AuthResponseStatus status, Locale locale) {
// 根据locale获取本地化消息
return MessageSource.getMessage(status.getMsg(), locale);
}
}
3.3 AuthException设计深度分析
异常类的层次设计
java
public class AuthException extends RuntimeException {
private int errorCode; // 错误码
private String errorMsg; // 错误消息
// 多种构造方法,适应不同的异常创建场景
// 场景1:简单的错误消息
public AuthException(String errorMsg) {
this(AuthResponseStatus.FAILURE.getCode(), errorMsg);
}
// 场景2:带平台信息的错误
public AuthException(String errorMsg, AuthSource source) {
this(AuthResponseStatus.FAILURE.getCode(), errorMsg, source);
}
// 场景3:完整的错误码和消息
public AuthException(int errorCode, String errorMsg) {
super(errorMsg);
this.errorCode = errorCode;
this.errorMsg = errorMsg;
}
// 场景4:使用预定义的错误状态
public AuthException(AuthResponseStatus status) {
this(status.getCode(), status.getMsg());
}
// 场景5:带平台信息的预定义错误
public AuthException(AuthResponseStatus status, AuthSource source) {
this(status.getCode(), status.getMsg(), source);
}
// 场景6:嵌套异常支持
public AuthException(String message, Throwable cause) {
super(message, cause);
}
}
异常使用的最佳实践
🎯 实践1:语义化的异常抛出
java
// ✅ 使用预定义状态,语义清晰
throw new AuthException(AuthResponseStatus.ILLEGAL_CODE);
// ✅ 带平台信息,便于排查
throw new AuthException(AuthResponseStatus.PARAMETER_INCOMPLETE, source);
// ✅ 自定义消息,提供具体信息
throw new AuthException("GitHub API rate limit exceeded");
// ❌ 避免使用通用异常
throw new RuntimeException("Something went wrong"); // 信息不明确
🎯 实践2:异常链的保持
java
// ✅ 保持异常链,便于调试
@Override
public AuthToken getAccessToken(AuthCallback authCallback) {
try {
String response = doPostAuthorizationCode(authCallback.getCode());
return parseTokenResponse(response);
} catch (HttpException e) {
// 保持原始异常信息
throw new AuthException("Failed to get access token", e);
} catch (JsonException e) {
// 转换为业务异常,但保持异常链
throw new AuthException("Invalid token response format", e);
}
}
// ❌ 丢失异常链
@Override
public AuthToken getAccessToken(AuthCallback authCallback) {
try {
String response = doPostAuthorizationCode(authCallback.getCode());
return parseTokenResponse(response);
} catch (Exception e) {
// 丢失了原始异常信息
throw new AuthException("Failed to get access token");
}
}
🎯 实践3:分层异常处理
java
// HTTP工具层:抛出技术异常
public class HttpUtils {
public String post(String url) {
try {
// HTTP请求实现
return response.body();
} catch (IOException e) {
throw new HttpException("Network error", e); // 技术异常
}
}
}
// 业务逻辑层:转换为业务异常
@Override
public AuthToken getAccessToken(AuthCallback authCallback) {
try {
String response = doPostAuthorizationCode(authCallback.getCode());
if (response.contains("error")) {
throw new AuthException(AuthResponseStatus.ILLEGAL_CODE); // 业务异常
}
return parseToken(response);
} catch (HttpException e) {
throw new AuthException("Failed to connect to OAuth server", e);
}
}
// 模板方法层:统一处理所有异常
@Override
public AuthResponse<AuthUser> login(AuthCallback authCallback) {
try {
// 执行业务逻辑
return AuthResponse.success(user);
} catch (Exception e) {
// 统一转换为响应对象
return responseError(e);
}
}
四、实战演练
4.1 手写简化版模板方法实现
让我们从零开始,实现一个简化版的OAuth模板方法框架,加深对设计原理的理解。
第1步:定义核心接口
java
// 简化的OAuth请求接口
public interface SimpleOAuthRequest {
// 生成授权URL
String authorize(String state);
// 获取访问令牌
AccessToken getAccessToken(String code);
// 获取用户信息
UserInfo getUserInfo(AccessToken token);
// 完整登录流程
default LoginResult login(String code, String state) {
throw new UnsupportedOperationException("Not implemented");
}
}
// 简化的数据模型
public class AccessToken {
private String token;
private long expireTime;
// getters/setters...
}
public class UserInfo {
private String id;
private String name;
private String email;
// getters/setters...
}
public class LoginResult {
private boolean success;
private String errorMsg;
private UserInfo userInfo;
// getters/setters...
}
第2步:实现抽象基类
java
// 简化版的抽象基类,实现模板方法
public abstract class AbstractOAuthRequest implements SimpleOAuthRequest {
protected OAuthConfig config;
public AbstractOAuthRequest(OAuthConfig config) {
this.config = config;
}
// 模板方法:定义完整的登录流程算法骨架
@Override
public LoginResult login(String code, String state) {
try {
// 第1步:参数验证(通用逻辑)
validateParameters(code, state);
// 第2步:获取访问令牌(抽象方法,子类实现)
AccessToken token = getAccessToken(code);
// 第3步:获取用户信息(抽象方法,子类实现)
UserInfo userInfo = getUserInfo(token);
// 第4步:后置处理(钩子方法,可选覆盖)
postProcessUserInfo(userInfo);
// 第5步:构建成功响应(通用逻辑)
return buildSuccessResult(userInfo);
} catch (Exception e) {
// 统一异常处理
return buildErrorResult(e.getMessage());
}
}
// 通用逻辑:参数验证
protected void validateParameters(String code, String state) {
if (code == null || code.trim().isEmpty()) {
throw new IllegalArgumentException("Authorization code cannot be empty");
}
if (state == null || state.trim().isEmpty()) {
throw new IllegalArgumentException("State parameter cannot be empty");
}
// TODO: 验证state的有效性(防CSRF)
}
// 抽象方法:子类必须实现
public abstract AccessToken getAccessToken(String code);
public abstract UserInfo getUserInfo(AccessToken token);
// 钩子方法:子类可选覆盖
protected void postProcessUserInfo(UserInfo userInfo) {
// 默认不做任何处理
}
// 通用逻辑:构建响应
protected LoginResult buildSuccessResult(UserInfo userInfo) {
LoginResult result = new LoginResult();
result.setSuccess(true);
result.setUserInfo(userInfo);
return result;
}
protected LoginResult buildErrorResult(String errorMsg) {
LoginResult result = new LoginResult();
result.setSuccess(false);
result.setErrorMsg(errorMsg);
return result;
}
}
第3步:实现具体的平台类
java
// GitHub平台的具体实现
public class GitHubOAuthRequest extends AbstractOAuthRequest {
private static final String TOKEN_URL = "https://github.com/login/oauth/access_token";
private static final String USER_URL = "https://api.github.com/user";
public GitHubOAuthRequest(OAuthConfig config) {
super(config);
}
@Override
public String authorize(String state) {
return String.format(
"https://github.com/login/oauth/authorize?client_id=%s&redirect_uri=%s&state=%s&scope=user:email",
config.getClientId(),
config.getRedirectUri(),
state
);
}
@Override
public AccessToken getAccessToken(String code) {
// GitHub特定的令牌获取逻辑
String requestBody = String.format(
"client_id=%s&client_secret=%s&code=%s",
config.getClientId(),
config.getClientSecret(),
code
);
// 模拟HTTP请求
String response = httpPost(TOKEN_URL, requestBody);
// 解析GitHub的响应格式:access_token=xxx&scope=xxx&token_type=bearer
Map<String, String> params = parseUrlEncodedResponse(response);
AccessToken token = new AccessToken();
token.setToken(params.get("access_token"));
token.setExpireTime(System.currentTimeMillis() + 3600 * 1000); // 1小时
return token;
}
@Override
public UserInfo getUserInfo(AccessToken token) {
// GitHub特定的用户信息获取逻辑
String response = httpGet(USER_URL, "Bearer " + token.getToken());
// 模拟解析JSON响应
JsonObject json = parseJson(response);
UserInfo userInfo = new UserInfo();
userInfo.setId(json.getString("id"));
userInfo.setName(json.getString("name"));
userInfo.setEmail(json.getString("email"));
return userInfo;
}
// GitHub特有的后置处理
@Override
protected void postProcessUserInfo(UserInfo userInfo) {
// 如果name为空,使用login作为name
if (userInfo.getName() == null || userInfo.getName().isEmpty()) {
userInfo.setName("GitHub User " + userInfo.getId());
}
}
// 工具方法(简化实现)
private String httpPost(String url, String body) {
// 模拟HTTP POST请求
return "access_token=gho_xxxxxxxxxxxxxxxxxxxx&scope=user:email&token_type=bearer";
}
private String httpGet(String url, String authHeader) {
// 模拟HTTP GET请求
return "{\"id\":\"12345\",\"login\":\"testuser\",\"name\":\"Test User\",\"email\":\"test@example.com\"}";
}
private Map<String, String> parseUrlEncodedResponse(String response) {
// 简化的URL编码解析
Map<String, String> result = new HashMap<>();
String[] pairs = response.split("&");
for (String pair : pairs) {
String[] keyValue = pair.split("=");
if (keyValue.length == 2) {
result.put(keyValue[0], keyValue[1]);
}
}
return result;
}
private JsonObject parseJson(String json) {
// 模拟JSON解析(实际应使用JSON库)
return new JsonObject(json);
}
}
// 微信平台的不同实现
public class WeChatOAuthRequest extends AbstractOAuthRequest {
private static final String TOKEN_URL = "https://api.weixin.qq.com/sns/oauth2/access_token";
private static final String USER_URL = "https://api.weixin.qq.com/sns/userinfo";
public WeChatOAuthRequest(OAuthConfig config) {
super(config);
}
@Override
public String authorize(String state) {
return String.format(
"https://open.weixin.qq.com/connect/oauth2/authorize?appid=%s&redirect_uri=%s&response_type=code&scope=snsapi_userinfo&state=%s",
config.getClientId(), // 微信中叫appid
config.getRedirectUri(),
state
);
}
@Override
public AccessToken getAccessToken(String code) {
// 微信特定的令牌获取逻辑(GET请求,JSON响应)
String url = String.format(
"%s?appid=%s&secret=%s&code=%s&grant_type=authorization_code",
TOKEN_URL,
config.getClientId(),
config.getClientSecret(),
code
);
String response = httpGet(url, null);
JsonObject json = parseJson(response);
AccessToken token = new AccessToken();
token.setToken(json.getString("access_token"));
token.setExpireTime(System.currentTimeMillis() + json.getLong("expires_in") * 1000);
return token;
}
@Override
public UserInfo getUserInfo(AccessToken token) {
// 微信特定的用户信息获取逻辑
String url = String.format(
"%s?access_token=%s&openid=%s",
USER_URL,
token.getToken(),
"openid_from_token" // 实际需要从token中获取
);
String response = httpGet(url, null);
JsonObject json = parseJson(response);
UserInfo userInfo = new UserInfo();
userInfo.setId(json.getString("openid"));
userInfo.setName(json.getString("nickname")); // 微信用nickname字段
// 微信不提供email
return userInfo;
}
// 微信特有的后置处理
@Override
protected void postProcessUserInfo(UserInfo userInfo) {
// 微信用户通常没有email,设置一个默认值
if (userInfo.getEmail() == null) {
userInfo.setEmail("noemail@wechat.user");
}
}
// 省略工具方法实现...
}
第4步:使用示例
java
// 使用示例
public class OAuthExample {
public static void main(String[] args) {
// GitHub登录示例
OAuthConfig githubConfig = new OAuthConfig(
"github_client_id",
"github_client_secret",
"http://localhost:8080/callback/github"
);
SimpleOAuthRequest githubRequest = new GitHubOAuthRequest(githubConfig);
// 1. 生成授权URL
String authorizeUrl = githubRequest.authorize("random_state_123");
System.out.println("GitHub授权链接: " + authorizeUrl);
// 2. 用户授权后,使用返回的code进行登录
LoginResult result = githubRequest.login("auth_code_from_github", "random_state_123");
if (result.isSuccess()) {
UserInfo user = result.getUserInfo();
System.out.println("登录成功: " + user.getName() + " (" + user.getEmail() + ")");
} else {
System.out.println("登录失败: " + result.getErrorMsg());
}
// 微信登录示例
OAuthConfig wechatConfig = new OAuthConfig(
"wechat_app_id",
"wechat_app_secret",
"http://localhost:8080/callback/wechat"
);
SimpleOAuthRequest wechatRequest = new WeChatOAuthRequest(wechatConfig);
// 相同的使用方式,不同的内部实现
String wechatUrl = wechatRequest.authorize("random_state_456");
LoginResult wechatResult = wechatRequest.login("wechat_auth_code", "random_state_456");
// 处理结果...
}
}
4.2 分析OAuth标准流程的抽象过程
OAuth流程的抽象层次分析
通过上面的实现,我们可以清晰地看到OAuth流程的抽象过程:
🎯 第1层抽象:标准化流程骨架
java
// 所有OAuth平台都遵循的标准流程
public LoginResult login(String code, String state) {
validateParameters(code, state); // 1. 参数验证
AccessToken token = getAccessToken(code); // 2. 获取令牌
UserInfo userInfo = getUserInfo(token); // 3. 获取用户
postProcessUserInfo(userInfo); // 4. 后置处理
return buildSuccessResult(userInfo); // 5. 构建响应
}
// 这个流程是不变的,所有平台都适用
🎯 第2层抽象:平台差异的隔离
java
// 每个平台的具体实现不同,但接口统一
public abstract AccessToken getAccessToken(String code); // 令牌获取方式不同
public abstract UserInfo getUserInfo(AccessToken token); // 用户信息获取方式不同
// GitHub: POST请求,URL编码响应,Bearer认证
// 微信: GET请求,JSON响应,参数认证
// 但对外接口完全一致
🎯 第3层抽象:扩展点的设计
java
// 钩子方法允许平台特定的定制
protected void postProcessUserInfo(UserInfo userInfo) {
// GitHub: 处理空名称
// 微信: 设置默认邮箱
// 其他平台: 其他特定处理
}
抽象过程的设计原则
📐 原则1:找出不变的部分
java
// 分析所有OAuth平台,找出共同的流程步骤:
// 1. 生成授权链接 → authorize()
// 2. 用授权码换取令牌 → getAccessToken()
// 3. 用令牌获取用户信息 → getUserInfo()
// 4. 组装响应数据 → buildResult()
// 这些步骤在所有平台中都存在,顺序也相同
📐 原则2:抽象变化的部分
java
// 各平台的差异点:
// - API地址不同 → 通过配置解决
// - 请求方式不同(GET/POST) → 子类实现决定
// - 参数格式不同 → 子类实现决定
// - 响应格式不同 → 子类实现决定
// - 字段映射不同 → 子类实现决定
// 将这些差异抽象为抽象方法,由子类实现
📐 原则3:提供扩展点
java
// 预留扩展点,应对未来变化:
// - 钩子方法:允许特定平台的定制逻辑
// - 配置参数:通过配置控制行为
// - 工具方法:提供常用的辅助功能
// 这些扩展点让框架既统一又灵活
4.3 设计异常处理的最佳实践
异常处理架构设计
java
// 分层异常处理架构
public class OAuthExceptionHandler {
// 第1层:技术层异常
public static class NetworkException extends RuntimeException {
public NetworkException(String message, Throwable cause) {
super(message, cause);
}
}
public static class ParseException extends RuntimeException {
public ParseException(String message, Throwable cause) {
super(message, cause);
}
}
// 第2层:业务层异常
public static class OAuthException extends RuntimeException {
private final String errorCode;
private final String platform;
public OAuthException(String errorCode, String message, String platform) {
super(message);
this.errorCode = errorCode;
this.platform = platform;
}
public OAuthException(String errorCode, String message, String platform, Throwable cause) {
super(message, cause);
this.errorCode = errorCode;
this.platform = platform;
}
// getters...
}
}
异常处理的最佳实践
🎯 实践1:异常的分层转换
java
// HTTP工具类:抛出技术异常
public class HttpClient {
public String post(String url, String body) throws NetworkException {
try {
// 执行HTTP请求
return executeRequest(url, body);
} catch (IOException e) {
throw new NetworkException("Failed to connect to " + url, e);
} catch (TimeoutException e) {
throw new NetworkException("Request timeout for " + url, e);
}
}
}
// OAuth实现类:转换为业务异常
public class GitHubOAuthRequest extends AbstractOAuthRequest {
@Override
public AccessToken getAccessToken(String code) {
try {
String response = httpClient.post(TOKEN_URL, buildRequestBody(code));
return parseTokenResponse(response);
} catch (NetworkException e) {
// 转换为业务异常
throw new OAuthException("NETWORK_ERROR",
"Failed to get access token from GitHub", "github", e);
} catch (ParseException e) {
// 转换为业务异常
throw new OAuthException("PARSE_ERROR",
"Invalid token response from GitHub", "github", e);
}
}
}
// 模板方法:统一处理业务异常
public abstract class AbstractOAuthRequest implements SimpleOAuthRequest {
@Override
public LoginResult login(String code, String state) {
try {
return performLogin(code, state);
} catch (OAuthException e) {
// 业务异常转换为结果对象
return buildErrorResult(e.getErrorCode(), e.getMessage(), e.getPlatform());
} catch (Exception e) {
// 未预期异常的兜底处理
return buildErrorResult("UNKNOWN_ERROR", "Unexpected error: " + e.getMessage(), "unknown");
}
}
}
🎯 实践2:错误码的标准化设计
java
// 错误码枚举
public enum OAuthErrorCode {
// 网络相关错误 (1xxx)
NETWORK_ERROR(1001, "Network connection failed"),
TIMEOUT_ERROR(1002, "Request timeout"),
// 认证相关错误 (2xxx)
INVALID_CODE(2001, "Invalid authorization code"),
INVALID_TOKEN(2002, "Invalid access token"),
EXPIRED_TOKEN(2003, "Access token expired"),
// 平台API错误 (3xxx)
API_ERROR(3001, "Platform API returned error"),
RATE_LIMIT(3002, "API rate limit exceeded"),
// 数据解析错误 (4xxx)
PARSE_ERROR(4001, "Failed to parse response"),
INVALID_RESPONSE(4002, "Invalid response format"),
// 配置错误 (5xxx)
INVALID_CONFIG(5001, "Invalid OAuth configuration"),
MISSING_PARAMETER(5002, "Required parameter missing");
private final int code;
private final String message;
OAuthErrorCode(int code, String message) {
this.code = code;
this.message = message;
}
// getters...
}
// 错误码的使用
public class GitHubOAuthRequest extends AbstractOAuthRequest {
@Override
public AccessToken getAccessToken(String code) {
try {
String response = httpClient.post(TOKEN_URL, buildRequestBody(code));
if (response.contains("error")) {
throw new OAuthException(
OAuthErrorCode.INVALID_CODE.name(),
"GitHub returned error: " + extractError(response),
"github"
);
}
return parseTokenResponse(response);
} catch (NetworkException e) {
throw new OAuthException(
OAuthErrorCode.NETWORK_ERROR.name(),
OAuthErrorCode.NETWORK_ERROR.getMessage(),
"github", e
);
}
}
}
🎯 实践3:异常信息的丰富化
java
// 丰富异常信息的工具类
public class ExceptionEnricher {
public static OAuthException enrichNetworkException(NetworkException e, String platform, String operation) {
String detailMessage = String.format(
"Network error during %s for platform %s. Original error: %s. " +
"Please check network connectivity and platform API status.",
operation, platform, e.getMessage()
);
return new OAuthException(
OAuthErrorCode.NETWORK_ERROR.name(),
detailMessage,
platform,
e
);
}
public static OAuthException enrichParseException(ParseException e, String platform, String responseBody) {
String detailMessage = String.format(
"Failed to parse response from %s platform. " +
"Response body: %s. Original error: %s",
platform,
truncateResponse(responseBody, 200),
e.getMessage()
);
return new OAuthException(
OAuthErrorCode.PARSE_ERROR.name(),
detailMessage,
platform,
e
);
}
private static String truncateResponse(String response, int maxLength) {
if (response == null) return "null";
if (response.length() <= maxLength) return response;
return response.substring(0, maxLength) + "... (truncated)";
}
}
// 使用丰富化工具
public class GitHubOAuthRequest extends AbstractOAuthRequest {
@Override
public AccessToken getAccessToken(String code) {
try {
String response = httpClient.post(TOKEN_URL, buildRequestBody(code));
return parseTokenResponse(response);
} catch (NetworkException e) {
throw ExceptionEnricher.enrichNetworkException(e, "github", "get access token");
} catch (ParseException e) {
throw ExceptionEnricher.enrichParseException(e, "github", response);
}
}
}
异常处理的监控与告警
java
// 异常监控接口
public interface ExceptionMonitor {
void recordException(OAuthException exception);
void recordNetworkIssue(String platform, String operation, long responseTime);
void recordApiError(String platform, String errorCode, String errorMessage);
}
// 默认监控实现
public class DefaultExceptionMonitor implements ExceptionMonitor {
private final Logger logger = LoggerFactory.getLogger(DefaultExceptionMonitor.class);
private final MetricRegistry metrics = new MetricRegistry();
@Override
public void recordException(OAuthException exception) {
// 记录日志
logger.error("OAuth exception occurred: platform={}, errorCode={}, message={}",
exception.getPlatform(), exception.getErrorCode(), exception.getMessage(), exception);
// 记录指标
metrics.counter("oauth.exceptions." + exception.getPlatform() + "." + exception.getErrorCode()).inc();
// 发送告警(如果是严重错误)
if (isCriticalError(exception.getErrorCode())) {
sendAlert(exception);
}
}
private boolean isCriticalError(String errorCode) {
return Arrays.asList("NETWORK_ERROR", "API_ERROR", "INVALID_CONFIG").contains(errorCode);
}
private void sendAlert(OAuthException exception) {
// 发送到告警系统
AlertManager.sendAlert(
"OAuth Critical Error",
String.format("Platform: %s, Error: %s, Message: %s",
exception.getPlatform(), exception.getErrorCode(), exception.getMessage())
);
}
}
// 在抽象基类中集成监控
public abstract class AbstractOAuthRequest implements SimpleOAuthRequest {
private ExceptionMonitor monitor = new DefaultExceptionMonitor();
@Override
public LoginResult login(String code, String state) {
try {
return performLogin(code, state);
} catch (OAuthException e) {
// 记录异常
monitor.recordException(e);
return buildErrorResult(e.getErrorCode(), e.getMessage(), e.getPlatform());
} catch (Exception e) {
// 创建并记录未预期异常
OAuthException wrappedException = new OAuthException(
"UNKNOWN_ERROR", "Unexpected error: " + e.getMessage(), "unknown", e);
monitor.recordException(wrappedException);
return buildErrorResult(wrappedException);
}
}
}
五、学习收获与设计思维培养
5.1 模板方法模式的深度理解
通过对JustAuth AuthDefaultRequest
的深入分析,我们对模板方法模式有了更深刻的理解:
🎓 核心收获1:算法骨架vs具体实现的分离艺术
java
// 模板方法模式的本质:分离"做什么"和"怎么做"
public abstract class AuthDefaultRequest {
// "做什么":定义完整的业务流程
public AuthResponse<AuthUser> login(AuthCallback authCallback) {
checkCode(authCallback); // 做什么:验证参数
AuthToken token = getAccessToken(authCallback); // 做什么:获取令牌
AuthUser user = getUserInfo(token); // 做什么:获取用户
return buildResponse(user); // 做什么:构建响应
}
// "怎么做":具体的实现细节由子类决定
public abstract AuthToken getAccessToken(AuthCallback authCallback); // 怎么做:各平台不同
public abstract AuthUser getUserInfo(AuthToken authToken); // 怎么做:各平台不同
}
// 设计智慧:
// ✅ 算法的稳定性:流程骨架不会因为平台差异而改变
// ✅ 实现的灵活性:具体步骤可以根据平台特点自由实现
// ✅ 扩展的便利性:新增平台只需实现抽象方法
// ✅ 维护的简单性:流程变更只需修改模板方法
🎓 核心收获2:钩子方法的设计哲学
java
// 钩子方法:在合适的地方留下"扩展点"
public abstract class AuthDefaultRequest {
public AuthResponse<AuthUser> login(AuthCallback authCallback) {
checkCode(authCallback);
// 钩子点:允许某些平台跳过状态验证
if (shouldCheckState()) {
AuthChecker.checkState(authCallback.getState(), source, authStateCache);
}
AuthToken authToken = this.getAccessToken(authCallback);
AuthUser user = this.getUserInfo(authToken);
// 钩子点:允许平台特定的用户信息处理
customizeUserInfo(user);
return buildResponse(user);
}
// 钩子方法:提供默认行为,允许子类覆盖
protected boolean shouldCheckState() {
return !config.isIgnoreCheckState();
}
protected void customizeUserInfo(AuthUser user) {
// 默认不做任何处理
}
}
// 钩子方法的设计原则:
// 🪝 预见性:在可能需要定制的地方预留钩子
// 🪝 合理性:钩子点应该在逻辑上有意义
// 🪝 简洁性:钩子方法应该职责单一
// 🪝 向后兼容:默认实现不应该破坏现有行为
🎓 核心收获3:异常处理的统一化设计
java
// 异常处理的分层设计
public abstract class AuthDefaultRequest {
@Override
public AuthResponse<AuthUser> login(AuthCallback authCallback) {
try {
// 所有业务逻辑
return performLogin(authCallback);
} catch (Exception e) {
// 统一异常处理:日志 + 转换 + 监控
Log.error("OAuth login failed", e);
notifyMonitor(e);
return responseError(e);
}
}
// 子类只需抛出异常,无需处理
@Override
public AuthToken getAccessToken(AuthCallback authCallback) {
String response = doPost(accessTokenUrl(authCallback.getCode()));
if (isError(response)) {
throw new AuthException("Token request failed"); // 直接抛出
}
return parseToken(response);
}
}
// 统一异常处理的好处:
// 🛡️ 一致性:所有平台的错误处理方式一致
// 🛡️ 可维护性:异常处理逻辑集中管理
// 🛡️ 可监控性:统一的异常监控和告警
// 🛡️ 易测试性:异常处理逻辑易于单元测试
5.2 抽象类vs接口的选择原则
通过JustAuth的设计,我们可以总结出抽象类和接口的选择原则:
📐 选择抽象类的场景
java
// ✅ 适合抽象类的场景:有公共实现逻辑
public abstract class AuthDefaultRequest implements AuthRequest {
// 公共字段:所有子类都需要
protected AuthConfig config;
protected AuthSource source;
// 公共方法:所有子类都相同
public String authorize(String state) {
return UrlBuilder.fromBaseUrl(source.authorize())
.queryParam("response_type", "code")
.queryParam("client_id", config.getClientId())
.queryParam("redirect_uri", config.getRedirectUri())
.queryParam("state", getRealState(state))
.build();
}
// 公共工具方法:避免代码重复
protected String doPostAuthorizationCode(String code) {
return new HttpUtils(config.getHttpConfig()).post(accessTokenUrl(code)).getBody();
}
// 模板方法:定义算法骨架
@Override
public AuthResponse<AuthUser> login(AuthCallback authCallback) {
// 复杂的公共逻辑
}
}
// 抽象类的适用条件:
// ✅ 有公共的状态(字段)
// ✅ 有公共的实现逻辑
// ✅ 需要模板方法定义算法骨架
// ✅ 子类之间有明确的"is-a"关系
🔌 选择接口的场景
java
// ✅ 适合接口的场景:纯粹的行为契约
public interface AuthRequest {
// 纯抽象方法:不同实现完全不同
AuthToken getAccessToken(AuthCallback authCallback);
AuthUser getUserInfo(AuthToken authToken);
// 默认方法:提供便利,但不依赖状态
default AuthResponse<AuthUser> login(AuthCallback authCallback) {
throw new AuthException(AuthResponseStatus.NOT_IMPLEMENTED);
}
}
// 接口的适用条件:
// ✅ 没有公共状态
// ✅ 定义纯粹的行为契约
// ✅ 需要多重继承的能力
// ✅ 强调"can-do"的能力而非"is-a"的关系
🔄 组合使用的最佳实践
java
// ✅ 最佳实践:接口定义契约,抽象类提供基础实现
public interface AuthRequest {
// 核心契约
AuthToken getAccessToken(AuthCallback authCallback);
AuthUser getUserInfo(AuthToken authToken);
AuthResponse<AuthUser> login(AuthCallback authCallback);
}
public abstract class AuthDefaultRequest implements AuthRequest {
// 公共实现和模板方法
@Override
public AuthResponse<AuthUser> login(AuthCallback authCallback) {
// 模板方法实现
}
}
public class AuthGithubRequest extends AuthDefaultRequest {
// 平台特定实现
@Override
public AuthToken getAccessToken(AuthCallback authCallback) {
// GitHub特定逻辑
}
}
// 这种设计的优势:
// 🎯 接口保证契约的一致性
// 🎯 抽象类避免代码重复
// 🎯 具体类专注平台特定逻辑
// 🎯 整体架构清晰且灵活
5.3 设计模式的选择思维
🤔 什么时候使用模板方法模式?
java
// 模板方法模式的适用场景判断:
// ✅ 场景1:有固定的算法骨架
// 问题:多个类有相同的处理流程,但具体步骤实现不同
// 示例:OAuth登录流程、数据处理流程、文件解析流程
// ✅ 场景2:需要控制子类的扩展点
// 问题:希望子类只能在特定步骤中进行定制,不能改变整体流程
// 示例:框架的生命周期管理、业务流程引擎
// ✅ 场景3:有大量重复代码需要复用
// 问题:多个类有相似的实现逻辑,但又不完全相同
// 示例:不同数据源的访问、不同格式的文件处理
// ❌ 不适用的场景:
// - 流程差异太大,没有固定骨架
// - 子类需要完全自由的实现方式
// - 没有公共逻辑可以复用
🎯 模板方法模式的设计检查清单
java
/**
* 模板方法模式设计检查清单
*
* □ 算法骨架设计
* ✓ 识别出了稳定的算法步骤
* ✓ 算法步骤的顺序是合理的
* ✓ 每个步骤的职责是单一的
*
* □ 抽象方法设计
* ✓ 抽象方法的粒度合适
* ✓ 输入输出定义明确
* ✓ 职责边界清晰
*
* □ 钩子方法设计
* ✓ 在合适的位置提供了扩展点
* ✓ 钩子方法有合理的默认实现
* ✓ 钩子方法不会破坏算法的完整性
*
* □ 异常处理设计
* ✓ 统一的异常处理策略
* ✓ 异常信息足够详细
* ✓ 异常分类合理
*
* □ 扩展性设计
* ✓ 新增子类的成本低
* ✓ 模板方法的修改不会影响子类
* ✓ 支持配置化的行为控制
*
* □ 可测试性设计
* ✓ 每个方法都可以独立测试
* ✓ 模板方法的测试覆盖率高
* ✓ 子类的测试简单
*/
5.4 架构设计的思维升级
🚀 从代码复用到架构复用
通过JustAuth的学习,我们的设计思维应该从"代码复用"升级到"架构复用":
java
// 代码复用层次:复用具体的代码片段
public class AuthUtils {
public static String buildUrl(String base, Map<String, String> params) {
// 工具方法复用
}
}
// 架构复用层次:复用整体的设计结构
public abstract class AuthDefaultRequest implements AuthRequest {
// 复用的不仅是代码,更是整套架构:
// - 流程控制的架构
// - 异常处理的架构
// - 扩展点设计的架构
// - 配置管理的架构
}
// 架构复用的价值:
// 🎯 一致性:所有实现都遵循相同的架构原则
// 🎯 可预测性:开发者容易理解和维护
// 🎯 可扩展性:新功能可以无缝集成
// 🎯 可演进性:架构可以逐步优化和升级
🎨 从功能实现到接口设计
设计思维的另一个升级是从"实现功能"转向"设计接口":
java
// 功能实现思维:关注如何实现具体功能
public class GitHubOAuth {
public User loginWithGitHub(String code) {
// 直接实现GitHub登录逻辑
String token = getGitHubToken(code);
return getGitHubUser(token);
}
}
// 接口设计思维:关注如何设计优雅的接口
public interface AuthRequest {
// 考虑的问题:
// 1. 接口如何既统一又灵活?
// 2. 如何平衡易用性和功能完整性?
// 3. 如何确保接口的向后兼容性?
// 4. 如何让接口易于测试和Mock?
AuthResponse<AuthUser> login(AuthCallback authCallback);
}
// 接口设计思维的价值:
// 🎯 抽象能力:能够从具体实现中抽象出通用接口
// 🎯 系统思维:考虑接口在整个系统中的作用
// 🎯 用户视角:从接口使用者的角度思考设计
// 🎯 演进能力:设计能够适应未来变化的接口
🌟 设计原则的内化理解
通过JustAuth的学习,经典的设计原则不再是抽象的理论,而是具体的实践指导:
java
// SOLID原则在JustAuth中的体现:
// S - 单一职责原则
public abstract class AuthDefaultRequest {
// 每个方法都有明确的单一职责
protected void checkCode() { /* 只负责参数校验 */ }
public abstract AuthToken getAccessToken() { /* 只负责获取令牌 */ }
public abstract AuthUser getUserInfo() { /* 只负责获取用户信息 */ }
}
// O - 开闭原则
// 对扩展开放:可以无限添加新的OAuth平台
// 对修改关闭:添加新平台不需要修改现有代码
// L - 里氏替换原则
// 任何AuthDefaultRequest的子类都可以替换父类使用
// I - 接口隔离原则
// AuthRequest接口只定义必要的方法,不强制实现不需要的功能
// D - 依赖倒置原则
// 依赖于AuthRequest抽象,而不是具体的实现类
六、本期总结与下期预告
本期核心要点回顾
- 模板方法模式精髓:通过算法骨架和具体实现的分离,实现了"一样的流程,不同的实现"
- 钩子方法设计:在关键节点提供扩展点,平衡统一性和灵活性
- 异常处理架构:分层异常处理、统一错误码设计、异常信息丰富化
- 抽象类vs接口:接口定义契约,抽象类提供模板实现,具体类专注细节
模板方法模式的设计智慧
模板方法模式体现了软件设计的哲学思考:
- 控制反转:由框架控制流程,具体实现只需关注自己的职责
- 关注点分离:将"做什么"和"怎么做"彻底分离
- 开放封闭:对算法骨架封闭,对具体实现开放
- 统一中求变化:在统一的接口下实现丰富的变化
实战能力提升
通过本期学习,你应该具备了:
- 识别适合模板方法模式的业务场景
- 设计稳定的算法骨架和合理的抽象方法
- 在合适位置提供钩子方法增强灵活性
- 设计分层的异常处理架构
- 从架构复用的角度思考问题
思考题
- 设计思考:如果要为JustAuth增加OAuth 2.0的设备码流程支持,如何在现有模板方法基础上扩展?
- 架构思考:在微服务架构中,如何设计跨服务的模板方法模式?
- 性能思考:模板方法模式中的钩子方法过多会不会影响性能?如何平衡灵活性和性能?
下期预告:建造者模式进阶 - AuthRequestBuilder设计解析
下一期我们将深入分析JustAuth的建造者模式实践:
- 🏗️ 建造者模式深度解析:复杂对象构建的优雅方案、链式调用的实现技巧、参数校验的最佳时机
- 🔍 AuthRequestBuilder源码剖析:流畅接口设计、反射技术的巧妙运用、构建逻辑与校验机制
- 📊 反射与性能优化:动态实例化的实现原理、构造函数选择策略、性能优化考虑
- 🛠️ 实战演练:实现支持多种构建方式的Builder、分析Builder模式的性能影响、设计优雅的API接口
我们将探讨如何通过建造者模式让复杂对象的构建过程变得优雅而直观,以及如何在保持易用性的同时确保性能和灵活性。
参考资料
- 《设计模式:可复用面向对象软件的基础》 - GoF
- 《重构:改善既有代码的设计》 - Martin Fowler
- JustAuth GitHub仓库
- 模板方法模式详解