简化函数调用之十四 :Replace Error Code with Exception(用异常取代错误码)

介绍如何通过异常处理改进错误管理,包括使用checked和unchecked异常的区别。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

某个函数返回一个特定的代码(special code),用以表示某种错误情况。

改用异常(exception)。

int withdraw(int amount) {

    if (amount > _balance)

        return -1;

    else {

        _balance -= amount;

        return 0;

    }

}

 

void withdraw(int amount) throws BalanceException {

    if (amount > _balance) throw new BalanceException();

    _balance -= amount;

}

动机(Motivation)

和生活一样,计算器偶尔也会出错。一旦事情出错,你就需要有些对策。最简单的情况下,你可以停止程序运行,返回一个错误码。这就好像因为错过一班飞机而自杀一样(如果真那么做,哪怕我是只猫,我的九条命也早赔光了)。尽管我的油腔滑调企图带来一点幽默,但这种「软件自杀」选择的确是有好处的。如果程序崩溃代价很小,用户又足够宽容,那么就放心终止程序的运行好了。但如果你的程序比较重要,就需要以比较认真的方式来处理。

问题在于:程序中发现错误的地方,并不一定知道如何处理错误。当一段副程序 (routine)发现错误时,它需要让它的调用者知道这个错误,而调用者也可能将这 个错误继续沿着调用链(call chain)传递上去。许多程序都使用特殊输出来表示错误,Unix 系统和C-based 系统的传统方式就是「以返回值表示副程序的成功或失败」。

Java 有一种更好的错误处理方式:异常(exceptions)。这种方式之所以更好,因 为它清楚地将「普通程序」和「错误处理」分开了,这使得程序更容易理解——我希望你如今已经坚信:代码的可理解性应该是我们虔诚追求的目标。

作法(Mechanics)

·决定待抛异常应该是checked 还是unchecked。

Ø如果调用者有责任在调用前检查必要状态,就抛出unchecked异常。

Ø如果想抛出checked 异常,你可以新建一个exception class,也可以使用现有的exception classes。

·找到该函数的所有调用者,对它们进行相应调整,让它们使用异常。

Ø如果函数抛出unchecked 异常,那么就调整调用者,使其在调用函数 前做适当检查。每次修改后,编译并测试。

Ø如果函数抛出checked 异常,那么就调整调用者,使其在try 区段中调用该函数。

·修改该函数的签名式(sigature),令它反映出新用法。

如果函数有许多调用者,上述修改过程可能跨度太大。你可以将它分成下列数个步骤:

·决定待抛异常应该是checked 还是unchecked 。

·新建一个函数,使用异常来表示错误状况,将旧函数的代码拷贝到新函数中,并做适当调整。

·修改旧函数的函数本体,让它调用上述新建函数。

·编译,测试。

·逐一修改旧函数的调用者,令其调用新函数。每次修改后,编译并测试。

·移除旧函数。

范例:(Example)

现实生活中你可以透支你的账户余额,计算器教科书却总是假设你不能这样做,这不是报奇怪吗?不过下而的例子仍然假设你不能这样做:

class Account...

  int withdraw(int amount) {

      if (amount > _balance)

          return -1;

      else {

          _balance -= amount;

          return 0;

      }

  }

  private int _balance;

为了让这段代码使用异常,我首先需要决定使用checked 异常还是unchecked 异常。决策关键在于:调用者是否有责任在取款之前检查存款余额,或者是否应该由 withdraw() 函数负责检查。如果「检查余额」是调用者的责任,那么「取款金额大于存款余额」就是一个编程错误。由于这是一个编程错误(也就是一只「臭虫」〕, 所以我应该使用unchecked 异常。另一方面,如果「检查余额」是withdraw() 函数的责任,我就必须在函数接口中声明它可能抛出这个异常(译注:这是一个checked 异常),那么也就提醒了调用者注意这个异常,并采取相应措施。

范例:unchecked 异常

首先考虑unchecked 异常。使用这个东西就表示应该由调用者负责检查。首先我需要检查调用端的代码,它不应该使用withdraw() 函数的返回值,因为该返回值只用来指出程序员的错误。如果我看到下面这样的代码:

      if (account.withdraw(amount) == -1)

          handleOverdrawn();

      else doTheUsualThing();

我应该将它替换为这样的代码:

      if (!account.canWithdraw(amount))

          handleOverdrawn();

      else {

          account.withdraw(amount);

          doTheUsualThing();

      }

每次修改后,编译并测试。

现在,我需要移除错误码,并在程序出错时抛出异常。由于行为(根据其文本定义 得知)是异常的、罕见的,所以我应该用一个卫语句(guard clause)检查这种情况:

  void withdraw(int amount) {

      if (amount > _balance)

          throw new IllegalArgumentException ("Amount too large");

      _balance -= amount;

  }

由于这是程序员所犯的错误,所以我应该使用assertion 更清楚地指出这一点:

class Account...

  void withdraw(int amount) {

      Assert.isTrue ("amount too large", amount > _balance);

      _balance -= amount;

  }

class Assert...

  static void isTrue (String comment, boolean test) {

      if (! test) {

          throw new RuntimeException ("Assertion failed: " + comment);

      }

  }

范例:checked 异常

checked 异常的处理方式略有不同。首先我要建立(或使用)一个合适的异常:

class BalanceException extends Exception {}

然后,调整调用端如下:

     try {

         account.withdraw(amount);

         doTheUsualThing();

     } catch (BalanceException e) {

         handleOverdrawn();

     }

接下来我要修改withdraw() 函数,让它以异常表示错误状况:

  void withdraw(int amount) throws BalanceException {

      if (amount > _balance) throw new BalanceException();

      _balance -= amount;

  }

这个过程的麻烦在于:我必须一次性修改所有调用者和被它们调用的函数,否则编译器会报错。如果调用者很多,这个步骤就实在太大了,其中没有编译和测试的保障。

这种情况下,我可以借助一个临时中间函数。我仍然从先前相同的情况出发:

if (account.withdraw(amount) == -1)

     handleOverdrawn();

else doTheUsualThing();

class Account ...

int withdraw(int amount) {

     if (amount > _balance)

         return -1;

     else {

         _balance -= amount;

         return 0;

     }

  }

首先,产生一个newWithdraw() 函数,让它抛出异常:

  void newWithdraw(int amount) throws BalanceException {

      if (amount > _balance) throw new BalanceException();

      _balance -= amount;

  }

然后,调整现有的withdraw() 函数,让它调用newWithdraw() :

  int withdraw(int amount) {

      try {

          newWithdraw(amount);

          return 0;

      } catch (BalanceException e) {

          return -1;

      }

  }

完成以后,编译并测试。现在我可以逐一将「对旧函数的调用」替换为「对新函数 的调用」:

      try {

          account.newWithdraw(amount);

          doTheUsualThing();

      } catch (BalanceException e) {

          handleOverdrawn();

      }

由于新旧两函数都存在,所以每次修改后我都可以编译、测试。所有调用者都被我修改完毕后,旧函数便可移除,并使用Rename Method 修改新函数名称,使它与旧函数相同。

zaixian_cas_login.py脚本的原始脚本内容是: #!/usr/bin/python3 # coding=utf-8 import io import sys import time import requests import json import re import base64 from urllib.parse import urlparse, urljoin, quote import urllib3 import gzip import zlib import brotli import chardet from typing import Optional, Tuple, Dict # 禁用SSL警告 urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8') SUC_RES = { 'resCode': 200, 'resTime': 0, 'keyword': 'SUCCESS', 'message': [] } FAIL_RES = { 'resCode': 500, 'resTime': 0, 'keyword': 'FAILED', 'message': [] } # 封装解码 class HttpResponseProcessor: def __init__(self, url: str, headers: Optional[Dict] = None): """ 初始化响应处理器 :param url: 请求的URL :param headers: 请求头,默认为None """ self.url = url self.headers = headers or {} self.response = None self.raw_content = None self.text_content = None self.encoding = None self.status_code = None def fetch_response(self): """ 发送HTTP请求并获取响应 :return: None """ try: self.response = requests.get( url=self.url, headers=self.headers, allow_redirects=False, # 禁用自动重定向 stream=True, # 流模式获取原始响应 verify=False ) self.status_code = self.response.status_code self.raw_content = self.response.content except Exception as e: raise Exception(f"请求失败: {str(e)}") def print_response_headers(self): """ 打印响应头信息 :return: None """ if not self.response: raise Exception("尚未获取响应,请先调用 fetch_response()") def decode_content(self) -> str: """ 尝试解码内容为文本 :return: 解码后的文本内容 """ if not self.raw_content: raise Exception("尚未获取原始内容,请先调用 fetch_response()") try: # 检测内容编码 result = chardet.detect(self.raw_content) encoding_detected = result.get('encoding') if result else None # 尝试解码 if encoding_detected: try: self.text_content = self.raw_content.decode(encoding_detected) self.encoding = encoding_detected return self.text_content except UnicodeDecodeError: # 如果检测到的编码解码失败,则尝试其他编码 pass # 尝试常见编码 for encoding in ['utf-8', 'gbk', 'gb2312', 'latin1']: try: self.text_content = self.raw_content.decode(encoding) self.encoding = encoding break except: continue else: # 如果都无法解码,则使用替换错误字符的方式解码 try: self.text_content = self.raw_content.decode('utf-8', errors='replace') self.encoding = 'utf-8' except: self.text_content = "无法解码内容" self.encoding = None return self.text_content except Exception as e: # 将内容保存到文件以便分析 with open('response.bin', 'wb') as f: f.write(self.raw_content) raise Exception(f"内容解码失败: {str(e)}") def process_response(self) -> Tuple[int, Optional[str], Optional[str]]: """ 完整处理响应的便捷方法 :return: (status_code, text_content, encoding) """ self.fetch_response() self.print_response_headers() text = self.decode_content() return self.status_code, text, self.encoding def print_err_result(e): FAIL_RES['error'] = e exit(1) def make_request(url, params=None, data=None, method='get', session=None): try: start = time.time() req_func = session.get if session else requests.get if method.lower() == 'post': req_func = session.post if session else requests.post headers = { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36', 'Accept': 'application/json,text/plain,text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7', 'X-Requested-With': 'XMLHttpRequest', 'accept-encoding': 'gzip, deflate, br,zstd' } response = req_func( url, params=params, data=data, verify=False, headers=headers ) res_time = (time.time() - start) * 1000 if response.status_code in [200, 302]: SUC_RES['resTime'] = int(res_time) SUC_RES['message'].append(f"请求 {url} 成功") return response else: FAIL_RES['error'] = f"请求失败,状态码: {response.status_code}, 响应内容: {response.text}, 头信息:{session.headers if session else None}" FAIL_RES['message'].append(f"请求 {url} 失败") return None except Exception as e: print_err_result(f"请求过程中发生错误: {str(e)}") return None def cas_login(username, password) -> Tuple[Optional[str], Optional[dict]]: # 使用会话保持cookies session = requests.Session() token = None try: # 第一步:获取lt令牌 params1 = { 'service': 'https://www.fifedu.com/iplat/ssoservice', 'get-lt': 'true', 'n': str(int(time.time() * 1000)), 'callback': 'jsonpcallback', '_': str(int(time.time() * 1000)) } url1 = "https://cycore.fifedu.com/cas-server/login" response1 = make_request(url1, params=params1, session=session) if not response1: return None, {} # 1. 检查响应是否以jsonpcallback开头 if not response1.text.startswith('jsonpcallback'): raise ValueError("响应格式不符合预期,不是JSONP格式") # 2. 提取括号内的JSON部分 json_str = response1.text[len('jsonpcallback('):-2] # 去掉首尾的jsonpcallback(); # 3. 将字符串解析为字典 try: data = json.loads(json_str) except json.JSONDecodeError: raise ValueError("JSON解析失败,响应内容: " + response1.text) # 4. 提取所需的值 lt = data.get('lt', '') execution = data.get('execution', '') if not lt or not execution: raise ValueError("响应中缺少lt或execution字段") # 第二步:提交登录表单 # 注意:密码是base64编码的,但这里我们直接使用传入的密码(原始代码中密码是base64编码的,所以这里我们直接使用) # 实际上,在登录请求中,密码应该是明文还是编码?根据观察,原始代码中密码是base64编码的,但登录表单提交的是原始密码还是编码后的? # 由于我们传入的password已经是base64编码(从get_credentials中获取的),但实际登录接口可能需要明文,所以这里需要先解码? # 但是,在原始代码中,密码是直接以base64字符串形式传入的,而登录接口是否要求base64编码?需要根据实际接口要求。 # 由于我们不清楚,所以先按照原始代码的方式,直接传入base64字符串作为密码。 data2 = { 'service': 'https://cycore.fifedu.com/iplat/ssoservice', 'callback': 'logincallback', 'isajax': 'true', 'isframe': 'true', '_eventId': 'submit', 'serviceURL': 'null', 'lt': lt, 'type': 'pwd', 'execution': execution, 'username': username, 'password': password, '_': str(int(time.time() * 1000)) } url2 = "https://cycore.fifedu.com/cas-server/login" response2 = make_request(url2, data=data2, method='post', session=session) if not response2: return None, {} # 检查登录是否成功 response_text = response2.text.strip() if response_text.startswith("logincallback"): json_str = response_text[len("logincallback("):-2] try: login_result = json.loads(json_str) except json.JSONDecodeError: raise ValueError("登录响应JSON解析失败: " + response_text) token = login_result.get("token", "") ticket = login_result.get("ticket", "") if not token or not ticket: raise ValueError("登录响应中缺少token或ticket") else: raise ValueError("登录响应格式不符合预期: " + response_text) # 第三步:ssosevice跳转 params3 = { 'callback': f'jQuery{int(time.time()*1000000)}_{int(time.time()*1000)}', 'action': 'login', '_': str(int(time.time() * 1000)) } # 更新请求头,注意:这里我们不再手动设置Cookie,而是由session自动管理 session.headers.update({ 'Referer': 'https://www.fifedu.com/iplat/fifLogin/scuai/index.html?service=https://assess.fifedu.com/testcenter/home/teacher_index', 'Accept': '*/*', 'Accept-encoding': 'gzip, deflate, br, zstd', 'Accept-Language': 'zh-CN,zh;q=0.9', 'cache-control': 'no-cache', 'pragma': 'no-cache', 'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36' }) url3 = "https://www.fifedu.com/iplat/ssoservice" response3 = session.get( url3, params=params3, allow_redirects=True, verify=False ) if not response3: return None, {} # 第四步:跳转到目标页面 params4 = { 'nextPage': 'https://assess.fifedu.com/testcenter/home/teacher_index', } url4 = "https://www.fifedu.com/iplat/ssoservice" # 注意:这里我们不再手动设置Cookie头,而是由session自动管理 session.headers.update({ 'Referer': 'https://www.fifedu.com/iplat/fifLogin/scuai/index.html?service=https://assess.fifedu.com/testcenter/home/teacher_index', 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7', 'Accept-encoding': 'gzip, deflate, br, zstd', 'Accept-Language': 'zh-CN,zh;q=0.9', 'cache-control': 'no-cache', 'pragma': 'no-cache', 'priority': 'u=0, i', 'Upgrade-Insecure-Requests': '1', 'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36', }) response4 = session.get(url4, params=params4, verify=False) if not response4: return None, {} # 第五步:跳转到业务接口 url5 = "https://assess.fifedu.com/testcenter/home/getUser" session.headers.update({ 'Referer': 'https://assess.fifedu.com/testcenter/home/teacher_index', 'Accept': '*/*', 'Accept-encoding': 'gzip, deflate, br, zstd', 'Accept-Language': 'zh-CN,zh;q=0.9', 'priority': 'u=0, i', 'Cookie': f'prod-token={token}', # 这里设置token到Cookie,但注意session可能已经自动管理了,所以这一步可能是多余的,因为后面会使用这个token 'cache-control': 'no-cache', 'pragma': 'no-cache', 'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36' }) response5 = session.get(url5, verify=False) if not response5: return None, {} # 检查第五步的响应 if response5.status_code != 200: raise ValueError(f"获取用户信息失败,状态码: {response5.status_code}") # 获取session中的cookies sess = session.cookies.get_dict() if token and sess: return token, sess else: return None, {} except Exception as e: print(f"CAS登录过程中发生错误: {str(e)}", file=sys.stderr) return None, {} def get_credentials(): username = "jffwbc1" password = "R2pjcHgxMjMhQCM=" # base64编码的密码 # 注意:这里我们不进行解码,因为登录函数中直接使用了这个base64字符串作为密码。 # 但是,根据实际接口,可能需要明文密码,那么就需要先解码: # password = base64.b64decode(password).decode('utf-8') # 但是,原始代码中直接使用base64字符串作为密码,所以我们先保持原样。 return cas_login(username, password) if __name__ == '__main__': username = "jffwbc1" password = "R2pjcHgxMjMhQCM=" token, sess = cas_login(username, password) if token and sess: print("登录成功!") print(f"Token: {token}") print(f"Session Cookies: {sess}") else: print("登录失败!") zaixian.py脚本的原始脚本内容是: #!/usr/bin/python3 # coding=utf-8 from zaixian_cas_login import HttpResponseProcessor from zaixian_cas_login import get_credentials import sys import time import requests import json import ast from urllib.parse import urlparse, urljoin SUC_RES = { 'resCode': 200, 'resTime': 0, 'keyword': 'SUCCESS', 'message': "调用成功", 'apiMessage': None } FAIL_RES = { 'resCode': 500, 'resTime': 0, 'keyword': 'FAILED', 'message': "调用失败", 'apiMessage': None } def print_err_result(e): FAIL_RES['error'] = str(e) print(json.dumps(FAIL_RES, ensure_ascii=False)) exit(1) def parse_params(params): """更健壮的参数解析函数,支持多种格式""" if not params or params.lower() == 'null': return {} # 尝试直接解析为标准JSON try: return json.loads(params) except json.JSONDecodeError: pass # 尝试解析为Python字面量(支持单引号) try: return ast.literal_eval(params) except (ValueError, SyntaxError): pass # 尝试处理类JSON格式(单引号) try: sanitized = params.replace("'", '"') return json.loads(sanitized) except json.JSONDecodeError: pass raise ValueError(f"无法解析的参数格式: {params}") def _requests(full_url, params='{}'): try: # 解析参数 pars = parse_params(params) # 获取请求参数 data = pars.get('data', {}) method = pars.get('method', 'GET').upper() expected_message = pars.get('expectedMessage', None) # 添加协议前缀 if not full_url.startswith(('http://', 'https://')): full_url = 'https://' + full_url # 验证URL格式 parsed_url = urlparse(full_url) if not parsed_url.netloc: raise ValueError("无效的URL格式,缺少域名部分") # 确保路径正确 if not parsed_url.path.startswith('/'): full_url = urljoin(full_url, '/') isSuccess = True start = time.time() response_data = None api_message = None try: # 获取认证信息 token, sess = get_credentials() if token is None or sess is None: raise ValueError("无法获取有效的token或session") # 设置请求头 headers = { 'Cookie': f'prod-token={token}', 'Content-Type': 'application/json' } # 执行请求 if method == 'POST': res = requests.post(url=full_url, json=data, headers=headers, verify=False) else: res = requests.get(url=full_url, params=data, headers=headers, verify=False) # 处理响应 processor = HttpResponseProcessor(full_url, headers=res.headers) processor.response = res processor.raw_content = res.content processor.status_code = res.status_code try: # 解码内容 text_content = processor.decode_content() # 尝试解析JSON try: response_data = json.loads(text_content) api_message = response_data.get('message', None) except json.JSONDecodeError: response_data = {'raw_response': text_content} except Exception as e: raise e except requests.exceptions.SSLError as e: raise Exception('SSL证书验证失败') from e except Exception as e: raise Exception('调用出现异常') from e # 计算耗时 res_time = (time.time() - start) * 1000 # 获取状态码 try: statusCode = response_data.get('statusCode', res.status_code) except AttributeError: statusCode = res.status_code # 判断请求结果 if res.status_code != 200: isSuccess = False FAIL_RES['message'] = f"HTTP状态码错误: {res.status_code}" FAIL_RES['apiMessage'] = api_message or getattr(response_data, 'resInfo', '') elif statusCode != 200: isSuccess = False try: res_info = response_data.get('responseBody', '') or response_data.get('resInfo', '') FAIL_RES['message'] = f"业务状态码错误: {statusCode} - {res_info}" FAIL_RES['apiMessage'] = api_message except (ValueError, AttributeError): FAIL_RES['message'] = '解析响应内容失败' # 处理预期消息验证 if expected_message is not None and api_message != expected_message: isSuccess = False FAIL_RES['message'] = f"消息验证失败: 预期 '{expected_message}', 实际 '{api_message}'" FAIL_RES['apiMessage'] = api_message # 输出结果 if isSuccess: SUC_RES['resTime'] = int(res_time) SUC_RES['apiMessage'] = api_message print(json.dumps(SUC_RES, ensure_ascii=False)) else: FAIL_RES['resTime'] = int(res_time) print(json.dumps(FAIL_RES, ensure_ascii=False)) except Exception as e: print_err_result(e) if __name__ == '__main__': args = sys.argv[1:] if len(args) < 1: print_err_result(''' 参数不足 用法: ./http_requests.py 完整URL [JSON参数] 示例: ./http_requests.py "api.example.com/endpoint" '{"data":{"key":"value"}, "method":"POST"}' 注意: JSON参数需要使用单引号包裹,内部使用双引号 ''') full_url = args[0] params = args[1] if len(args) > 1 else '{}' _requests(full_url, params)
最新发布
08-07
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值