Mproxy项目实录第7天

本文介绍了Mproxy项目中用于监控代理服务器数量及验证时间的系统设计与实现,包括监测方案、技术验证、代码实现及异常处理等内容。

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

关于这个系列

这个项目实录系列是记录Mproxy项目的整个开发流程。项目最终的目标是开发一套代理服务器的API。这个系列中会记录项目的需求、设计、验证、实现、升级等等,包括设计决策的依据,开发过程中的各种坑。希望和大家共同交流,一起进步。

项目的源码我会同步更新到GitHub,项目地址:https://github.com/mrbcy/Mproxy

系列地址:

Mproxy项目实录第1天

Mproxy项目实录第2天

Mproxy项目实录第3天

Mproxy项目实录第4天

Mproxy项目实录第5天

Mproxy项目实录第6天

今日计划

本来想着要在代理服务器的收集系统里面加一个内建的web页面,这样就可以通过浏览器查看系统的运行状态。

后来想了想,我的主要目的是能够确保系统在正确的运行就可以了。要达到这个目标并不是一定要内建web页面。要确保系统的正常运行只要卡主两个关键指标就可以了。

第1个指标是可用的代理服务器数量。这个是重中之重,只要这个指标没问题,其他可以不用那么紧张。

第2个指标是last_validate_time中的最晚时间。如果这个时间离现在很远,表示系统很久没有重新验证代理服务器了,也就证明目前的可用代理服务器数量是不可靠的。

针对这两个指标,提出了如下的监测方案。

  1. 如果发现可用的代理服务器数量少于4000,就运行爬虫进行爬取。这个检查2小时执行1次。
  2. 如果last_validate_time中的最晚时间距今超过24小时,发送短信提醒。这个检查6小时执行1次。
  3. 如果发现可用的代理服务器数量少于2000,发送短信提醒。这个检查2小时执行1次。
  4. 如果在执行上述查询的过程中抛出异常,也发送短信提醒。

所以,今天的任务就是完成监测器的开发,并且微调之前的系统各个参数。为真正在ubuntu集群上的部署做好准备。

技术验证

还是有两个技术验证工作要做。第一个是通过Python执行其他的Python程序;第二个是用Python发送短信。

用Python执行其他的Python程序

原来以为这个很难,结果很简单,哈哈。

首先写一个test.bat文件,代码如下:

cd /d "D:\软件编程学习\Mproxy\代码\Spiders\kuaidaili"
scrapy crawl kuaidaili

看到路径里面有中文,则test.bat文件的编码必须跟cmd命令窗口的编码格式一致,否则会找不到路径。我这里的编码是GBK。

然后编写调用bat的代码:

#-*- coding: utf-8 -*-
import os


def func():
    os.system('test.bat')


if __name__ == '__main__':
    func()

使用Python发送短信

我这边选用了阿里大于的短信接口。主要优点是使用淘宝用户名直接登录,很方便,还有Python的SDK可以直接用。而且毕竟大厂出品,应该有保障。

在使用之前要完成应用创建,签名审批和短信模板审批几个流程。具体就不说了,基本都能过。

在申请的时候一定要申请合适的短信模板。经过反复测试,变量长度最多为15个字符(中英文都算1个)。

在申请的时候一定要申请合适的短信模板。经过反复测试,变量长度最多为15个字符(中英文都算1个)。

在申请的时候一定要申请合适的短信模板。经过反复测试,变量长度最多为15个字符(中英文都算1个)。

发送短信的API文档可以参考这里:https://api.alidayu.com/doc2/apiDetail.htm?spm=a3142.8062968.3.1.2fJlnI&apiId=25450

代码如下:

#-*- coding: utf-8 -*-
import top.api

def func():
    url = " gw.api.taobao.com"
    req = top.api.AlibabaAliqinFcSmsNumSendRequest(url, port=80)
    req.set_app_info(top.appinfo(appkey, secret))

    req.extend = "123456"
    req.sms_type = "normal"
    req.sms_free_sign_name = "阿里大于"
    req.sms_param = "{\"code\":\"1234\",\"product\":\"alidayu\"}"
    req.rec_num = "13000000000"
    req.sms_template_code = "SMS_585014"
    try:
        resp = req.getResponse()
        print(resp)
    except Exception, e:
        print(e)


if __name__ == '__main__':
    func()

真实运行时需要把appkey,secret等信息填成系统分配过来的值。

这里有点坑,我申请的短信模板是这样的:

系统运行异常,请处理。${alarm_info}

本来想发一条这样的短信:

系统运行异常,请处理。Mproxy系统提示,代理服务器数量已经不足2000

然后反复的收到isv.PARAM_LENGTH_LIMIT的异常,就是说变量长度限制。

所以我们在申请的时候一定要申请合适的短信模板。经过反复测试,变量长度最多为15个字符(中英文都算1个)。

配置文件工具类

为了后续维护的方便和上传GitHub时能不泄露我的关键信息,必须把发送短信关键的数据放到配置文件里,然后提供一个配置文件的工具类供其他代码使用。

#-*- coding: utf-8 -*-
import ConfigParser
import os


class ConfigLoader:
    def __init__(self):
        # get the project path
        dir_name = "Monitor" + os.sep
        thePath = os.getcwdu()
        if thePath.find(dir_name) > 0:
            thePath = thePath[:thePath.find(dir_name) + len(dir_name)]
        else:
            thePath += os.sep
        print thePath
        self.cp = ConfigParser.SafeConfigParser()
        self.cp.read(thePath + 'monitor.cfg')

    def get_app_key(self):
        return self.cp.get('sms','appkey')

    def get_secret_key(self):
        return self.cp.get('sms','secret')

    def get_sign_name(self):
        return self.cp.get('sms','sign_name')

    def get_sms_template_code(self):
        return self.cp.get('sms','sms_template_code')

    def get_phone_num(self):
        return self.cp.get('sms','phone_num')

    def get_mysql_host(self):
        return self.cp.get('mysql','host')

    def get_mysql_port(self):
        return int(self.cp.get('mysql','port'))

    def get_mysql_user(self):
        return self.cp.get('mysql','user')

    def get_mysql_pwd(self):
        return self.cp.get('mysql','password')

    def get_mysql_db_name(self):
        return self.cp.get('mysql','db_name')

配置文件的结构如下,具体内容请自行填写。

[sms]
appkey = 
secret = 
sign_name = 
sms_template_code = 
phone_num = 

[mysql]
host = amaster
port = 3306
user = 
password = 
db_name = mproxy

感觉经过了几个版本的迭代,classloader终于写得在各个路径下以及在Windows和Linux下都能用了。稍后把这个代码更新到其他组件上去。

封装数据库的查询工具类

接下来使用三层架构来封装需要查询数据库的工具类。这里从dispatcher中把dbpool,domain包都复制过来待用。

dao的代码如下:

#-*- coding: utf-8 -*-
import traceback

from dao.proxystatus import ProxyStatus
from dbpool.poolutil import PoolUtil
from domain.proxydaoitem import ProxyDaoItem


class ProxyDao:

    def find_proxy_last_validate_time(self):
        try:
            conn = PoolUtil.pool.connection()
            cur = conn.cursor()
            sql = "select * from proxy_list order by last_validate_time desc limit 1"
            count = cur.execute(sql)
            last_validate_time = None
            if count != 0:
                data = cur.fetchone()
                last_validate_time = data[5]

            cur.close()
            conn.close()
            return last_validate_time
        except Exception as e:
            return None

    def get_avaliable_proxy_count(self):
        try:
            conn = PoolUtil.pool.connection()
            cur = conn.cursor()
            sql = "select count(*) from proxy_list where status = %s"
            count = cur.execute(sql,ProxyStatus.AVAILABLE)
            result = None
            if count != 0:
                data = cur.fetchone()
                result = int(data[0])

            cur.close()
            conn.close()
            return result
        except Exception as e:
            return 0

然后写dao的单元测试:

#-*- coding: utf-8 -*-
import datetime

from dao.proxydao import ProxyDao
from dao.proxystatus import ProxyStatus
from domain.proxydaoitem import ProxyDaoItem

proxy_dao = ProxyDao()

def test_get_proxy_count():
    global proxy_dao
    print proxy_dao.get_avaliable_proxy_count()

def test_get_last_validate_time():
    global proxy_dao
    last_validate_time = proxy_dao.find_proxy_last_validate_time()
    print type(last_validate_time)
    print last_validate_time

if __name__ == '__main__':
    test_get_last_validate_time()

service就是在dao上包了一层,所以也不用写单元测试了。

#-*- coding: utf-8 -*-
from dao.proxydao import ProxyDao


class ProxyService():
    def __init__(self):
        self.proxy_dao = ProxyDao()

    def find_proxy_last_validate_time(self):
        return self.proxy_dao.find_proxy_last_validate_time()

    def get_avaliable_proxy_count(self):
        return self.proxy_dao.get_avaliable_proxy_count()

封装发送短信工具类

#-*- coding: utf-8 -*-
import traceback

import top
from conf.configloader import ConfigLoader


class SmsUtil:
    conf_loader = ConfigLoader()
    @classmethod
    def send_sms(cls,system_name,exception_name,key_prompt):
        url = "gw.api.taobao.com"
        appkey = cls.conf_loader.get_app_key()
        secret = cls.conf_loader.get_secret_key()
        req = top.api.AlibabaAliqinFcSmsNumSendRequest(url)
        req.set_app_info(top.appinfo(appkey, secret))

        req.sms_type = "normal"
        req.sms_free_sign_name = cls.conf_loader.get_sign_name()
        req.sms_param = """{"system_name":"%s","exception_name":"%s","key_prompt":"%s"}""" % (system_name,exception_name,key_prompt)
        req.rec_num = cls.conf_loader.get_phone_num()
        req.sms_template_code = cls.conf_loader.get_sms_template_code()

        try:
            resp = req.getResponse()
            print(resp)
        except Exception as e:
            traceback.print_exc()

这里后续还可以改进,如果发送短信报异常,可以再发送邮件,让运维人员知道短信接口也有问题。

封装检查任务线程类

检查可用代理服务器数量的线程类

#-*- coding: utf-8 -*-
import os
import threading

import time
import traceback

from conf.configloader import ConfigLoader
from service.proxyservice import ProxyService
from sms.smsutil import SmsUtil


class AvailableCountTask(threading.Thread):
    def __init__(self):
        threading.Thread.__init__(self)
        self.proxy_service = ProxyService()
        self.conf_loader = ConfigLoader()

    def run(self):

        while True:
            count = self.proxy_service.get_avaliable_proxy_count()
            if count < 4500:
                try:
                    # start spiders
                    exit_code = os.system(self.conf_loader.get_start_kuaidaili_command())
                    print exit_code
                    if exit_code != 0:
                        SmsUtil.send_sms('Mproxy', '快代理爬虫运行出错', '无')

                    time.sleep(5)

                    exit_code = os.system(self.conf_loader.get_start_xicidaili_command())
                    print exit_code
                    if exit_code != 0:
                        SmsUtil.send_sms('Mproxy', '西刺代理爬虫运行出错', '无')
                except Exception as e:
                    traceback.print_exc()
                    SmsUtil.send_sms('Mproxy','启动爬虫出错','无')

            elif count < 2000:
                SmsUtil.send_sms('Mproxy', '代理服务器数量不足', str(count))

            time.sleep(60*60*2)

这里还对爬虫的代码进行了一点修改,如果启动过程出错(一般是连接Kafka集群失败)就以退出码1退出,让监测器能够知道确实出错了。

检查验证时间的任务类

#-*- coding: utf-8 -*-
import os
import threading

import time
import traceback

import datetime

from service.proxyservice import ProxyService
from sms.smsutil import SmsUtil


class ValidateCheckTask(threading.Thread):
    def __init__(self):
        threading.Thread.__init__(self)
        self.proxy_service = ProxyService()

    def run(self):

        while True:
            last_validate_time = self.proxy_service.find_proxy_last_validate_time()
            now = datetime.datetime.now()
            seconds = (now - last_validate_time).total_seconds()

            if seconds > 12 * 60 * 60:
                SmsUtil.send_sms('Mproxy', '超过12小时未执行验证', '无')

            time.sleep(60*60*6)

写监控器的代码

剩下的部分很简单,只要把两个任务启动起来就行了。

#-*- coding: utf-8 -*-
from sms.smsutil import SmsUtil
from task.availablecounttask import AvailableCountTask
from task.validatechecktask import ValidateCheckTask


def func():
    validate_check_task = ValidateCheckTask()
    validate_check_task.start()

    available_count_check_task = AvailableCountTask()
    available_count_check_task.start()
    # SmsUtil.send_sms('Mproxy','代理服务器数量不足','200')


if __name__ == '__main__':
    func()

封装邮件发送的工具类

直接在网上找到了代码http://www.cnblogs.com/xiaowuyi/archive/2012/03/17/2404015.html,用一下吧。

#-*- coding: utf-8 -*-
import smtplib
from email.mime.text import MIMEText

from conf.configloader import ConfigLoader


class EmailUtil:
    conf_loader = ConfigLoader()

    @classmethod
    def send_email(cls,sms_content,exception_info):
        mailto_list = cls.conf_loader.get_mail_to_list()
        mail_host = "smtp.qq.com"
        mail_user = cls.conf_loader.get_mail_username()
        mail_pass = cls.conf_loader.get_mail_password()
        mail_postfix = "qq.com"
        sub = "Mproxy短信接口异常"

        content = "尊敬的管理员您好,您收到这封邮件是因为我们的短信接口出现了问题,无法向运维人员发送短信。" \
                  "\n\n尝试发送的短信内容如下:\n%s \n\n发送过程中出现的异常为:\n%s" % (sms_content,exception_info)

        me = "Mproxy" + "<" + mail_user + "@" + mail_postfix + ">"
        msg = MIMEText(content, _subtype='plain', _charset='utf-8')
        msg['Subject'] = sub
        msg['From'] = me
        msg['To'] = ";".join(mailto_list)
        try:
            server = smtplib.SMTP_SSL("smtp.qq.com", 465)
            server.connect(mail_host)
            server.login(mail_user, mail_pass)
            server.sendmail(me, mailto_list, msg.as_string())
            server.quit()

        except Exception, e:
            print str(e)

然后修改一下短信发送工具类,如果发送短信出错,就发送邮件。

#-*- coding: utf-8 -*-
import traceback

import top
from conf.configloader import ConfigLoader
from mail.emailutil import EmailUtil
from sms.tracebackcontainer import TracebackContainer


class SmsUtil:
    conf_loader = ConfigLoader()
    @classmethod
    def send_sms(cls,system_name,exception_name,key_prompt):
        url = "gw.api.taobao.com"
        appkey = cls.conf_loader.get_app_key()
        secret = cls.conf_loader.get_secret_key()
        req = top.api.AlibabaAliqinFcSmsNumSendRequest(url)
        req.set_app_info(top.appinfo(appkey, secret))

        req.sms_type = "normal"
        req.sms_free_sign_name = cls.conf_loader.get_sign_name()
        req.sms_param = """{"system_name":"%s","exception_name":"%s","key_prompt":"%s"}""" % (system_name,exception_name,key_prompt)
        req.rec_num = cls.conf_loader.get_phone_num()
        req.sms_template_code = cls.conf_loader.get_sms_template_code()

        try:
            resp = req.getResponse()
            print(resp)
        except Exception as e:
            traceback_container = TracebackContainer()
            traceback.print_exc(file=traceback_container)
            sms_content = "%s运行异常:%s,关键参数:%s,请尽快处理" % (system_name,exception_name,key_prompt)
            EmailUtil.send_email(sms_content, traceback_container.message)

这里额外定义了一个类来收集traceback的打印信息。

#-*- coding: utf-8 -*-

class TracebackContainer:
    def __init__(self):
        self.message = ""

    def write(self, str):
        '''
        把traceback信息存储必须的函数
        '''
        self.message += str

小结

到现在来说,应该是能够保证收集系统稳定运行了。下面真的要部署到Linux环境下面去了,再把文档补一补,差的太多了。

<think>我们需要解决两个问题: 1. 对于MyBatis-Plus的查询,如何将返回结果字段名转为小写。 2. 对于JdbcTemplate的查询,如何将返回结果字段名转为小写。 ### 解决方案 #### 1. MyBatis-Plus 部分 MyBatis-Plus 默认使用MyBatis的机制,可以通过配置MyBatis的`ObjectWrapperFactory`或者使用结果集映射来实现。但是,在MyBatis-Plus中,我们可以通过配置`MybatisConfiguration`来设置`mapUnderscoreToCamelCase`为false,并自定义一个`ObjectWrapper`来处理字段名的大小写转换。然而,更直接的方式是使用MyBatis的`ResultMap`或者通过拦截器修改结果。 但是,考虑到我们要将所有返回字段名转为小写,我们可以使用MyBatis的`ResultSetHandler`拦截器,在结果集处理阶段将列名转为小写。 另一种方式:MyBatis-Plus 3.4.0支持配置`MybatisConfiguration`,我们可以通过设置`aggressiveLazyLoading`等属性,但并没有直接提供字段大小写转换的配置。因此,我们可以使用拦截器。 **步骤:** - 创建一个实现`ResultSetHandler`接口的拦截器,在`handleResultSets`方法中处理结果集,将列名转换为小写。 - 但是,注意:MyBatis的结果集处理比较复杂,我们也可以考虑在`ResultSet`被处理前修改列名为小写。 然而,更简单的方式是使用MyBatis的`ResultSetWrapper`,但需要深入MyBatis内部。因此,我们可以采用另一种思路:在MyBatis的`ResultSet`处理过程中,通过反射修改列名。 这里,我们选择使用拦截器,在结果集返回后,遍历结果集中的每一行,将每一列的列名转为小写,然后重新构建一个`Map`(针对非实体类查询,如`Map`)或者修改实体类的属性名?但是注意,实体类的属性名是固定的,我们无法修改,所以这个方案主要针对返回`Map`类型的结果。 但是,在MyBatis中,如果返回的是实体类对象,那么列名会通过映射(通过`ResultMap`)到实体类的属性上。如果数据库返回的列名是大写,而实体类属性是小写,那么我们需要开启`mapUnderscoreToCamelCase`(将下划线转为驼峰)并不能解决大小写问题(因为这里是大写转小写,且没有下划线)。因此,我们需要在映射之前将列名转为小写。 我们可以通过自定义一个`ResultSet`包装类,将`ResultSetMetaData`的列名方法重写,返回小写列名。然后,在拦截器中替换`ResultSet`。 **实现方案:** - 创建一个拦截器,拦截`ResultSetHandler`的`handleResultSets`方法。 - 在拦截器中,我们使用自定义的`ResultSet`包装类(将原`ResultSet`的`getMetaData`方法返回的列名全部转为小写)替换原`ResultSet`。 - 这样,MyBatis在处理结果集时,会使用小写的列名去映射。 但是,这种方式需要修改`ResultSet`的元数据,而`ResultSetMetaData`是一个接口,我们无法直接修改,所以需要动态代理或者包装。 **具体代码:** 我们可以通过动态代理来代理`ResultSetMetaData`,将`getColumnLabel`和`getColumnName`方法返回的列名转为小写。 **注意:** 在MyBatis中,默认使用`getColumnLabel`来获取列名(别名优先),如果没有别名则使用`getColumnName`。所以,我们只需要将这两个方法返回的字符串转为小写即可。 **拦截器实现:** ```java import org.apache.ibatis.executor.resultset.ResultSetHandler; import org.apache.ibatis.plugin.*; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Proxy; import java.sql.ResultSet; import java.sql.ResultSetMetaData; import java.sql.SQLException; import java.sql.Statement; import java.util.*; @Intercepts({ @Signature(type = ResultSetHandler.class, method = "handleResultSets", args = {Statement.class}) }) public class LowerCaseResultSetInterceptor implements Interceptor { @Override public Object intercept(Invocation invocation) throws Throwable { // 执行原方法,获取结果 Object result = invocation.proceed(); // 处理结果集,将结果集中的列名转为小写(针对Map类型) // 但是,如果我们修改了ResultSet的元数据,那么MyBatis在映射时会使用小写的列名,这样实体类映射也会正常 // 因此,我们修改Statement中的ResultSet,将其元数据代理 Statement statement = (Statement) invocation.getArgs()[0]; // 获取结果集 try (ResultSet rs = statement.getResultSet()) { if (rs == null) { return result; } // 包装ResultSet ResultSet lowerCaseRs = wrapResultSet(rs); // 将原Statement的结果集替换为我们包装后的结果集 // 注意:Statement的getResultSet方法返回的是原始结果集,我们需要替换它,但是Statement没有提供设置方法 // 因此,我们只能通过反射修改 // 但是,MyBatis的ResultSetHandler在处理结果集时已经关闭了ResultSet,所以我们不能在这里再次处理 // 所以,我们选择在MyBatis处理结果集之前修改ResultSet,因此需要在执行原方法之前替换Statement中的ResultSet } // 由于上述原因,我们改为在执行原方法之前替换ResultSet return result; } // 由于上面无法替换,我们改变思路:在执行原方法之前,将Statement的ResultSet替换为包装后的 // 但是,Statement的ResultSet是只读的,所以我们只能通过代理Statement来返回包装后的ResultSet // 因此,我们拦截器修改为代理Statement @Override public Object plugin(Object target) { // 创建代理对象 return Plugin.wrap(target, this); } @Override public void setProperties(Properties properties) { } // 包装ResultSet,使其元数据返回小写的列名 private ResultSet wrapResultSet(ResultSet rs) { return (ResultSet) Proxy.newProxyInstance( ResultSet.class.getClassLoader(), new Class[]{ResultSet.class}, (proxy, method, args) -> { if ("getMetaData".equals(method.getName())) { ResultSetMetaData metaData = rs.getMetaData(); return (ResultSetMetaData) Proxy.newProxyInstance( ResultSetMetaData.class.getClassLoader(), new Class[]{ResultSetMetaData.class}, (mProxy, mMethod, mArgs) -> { if ("getColumnLabel".equals(mMethod.getName()) || "getColumnName".equals(mMethod.getName())) { String columnName = (String) mMethod.invoke(metaData, mArgs); return columnName.toLowerCase(); } return mMethod.invoke(metaData, mArgs); }); } return method.invoke(rs, args); }); } } ``` 但是,上述拦截器在`handleResultSets`方法执行前替换了`ResultSet`,但是MyBatis的`ResultSetHandler`在调用`handleResultSets`时已经将`ResultSet`从`Statement`中取出,所以我们无法在拦截器内部通过替换`Statement`的`ResultSet`来影响结果。 因此,我们需要在`handleResultSets`方法执行之前替换`Statement`的`ResultSet`。但是,`Statement`的`ResultSet`一旦被获取,就无法改变。所以,我们只能代理`Statement`,在调用`getResultSet`方法时返回我们包装过的`ResultSet`。 修改拦截器: ```java import org.apache.ibatis.executor.resultset.ResultSetHandler; import org.apache.ibatis.plugin.*; import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; import java.lang.reflect.Proxy; import java.sql.ResultSet; import java.sql.ResultSetMetaData; import java.sql.Statement; import java.util.Properties; @Intercepts({ @Signature(type = ResultSetHandler.class, method = "handleResultSets", args = {Statement.class}) }) public class LowerCaseResultSetInterceptor implements Interceptor { @Override public Object intercept(Invocation invocation) throws Throwable { // 获取参数中的Statement Statement statement = (Statement) invocation.getArgs()[0]; // 代理Statement,使其返回的ResultSet是经过包装的 Statement proxyStatement = (Statement) Proxy.newProxyInstance( Statement.class.getClassLoader(), new Class[]{Statement.class}, new StatementInvocationHandler(statement)); // 替换参数 invocation.getArgs()[0] = proxyStatement; // 执行原方法 return invocation.proceed(); } @Override public Object plugin(Object target) { return Plugin.wrap(target, this); } @Override public void setProperties(Properties properties) { } private static class StatementInvocationHandler implements InvocationHandler { private final Statement target; private ResultSet proxyResultSet; StatementInvocationHandler(Statement target) { this.target = target; } @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { if ("getResultSet".equals(method.getName())) { // 如果已经包装过,则直接返回包装后的ResultSet if (proxyResultSet == null) { ResultSet rs = (ResultSet) method.invoke(target, args); if (rs != null) { proxyResultSet = wrapResultSet(rs); } } return proxyResultSet; } return method.invoke(target, args); } // 包装ResultSet,使其元数据返回小写的列名 private ResultSet wrapResultSet(ResultSet rs) { return (ResultSet) Proxy.newProxyInstance( ResultSet.class.getClassLoader(), new Class[]{ResultSet.class}, (proxy1, method1, args1) -> { if ("getMetaData".equals(method1.getName())) { ResultSetMetaData metaData = rs.getMetaData(); return (ResultSetMetaData) Proxy.newProxyInstance( ResultSetMetaData.class.getClassLoader(), new Class[]{ResultSetMetaData.class}, (proxy2, method2, args2) -> { if ("getColumnLabel".equals(method2.getName()) || "getColumnName".equals(method2.getName())) { String columnName = (String) method2.invoke(metaData, args2); return columnName.toLowerCase(); } return method2.invoke(metaData, args2); }); } return method1.invoke(rs, args1); }); } } } ``` 然后,在MyBatis的配置文件中配置这个拦截器: ```xml <plugins> <plugin interceptor="com.example.LowerCaseResultSetInterceptor"/> </plugins> ``` 这样,MyBatis在查询时,返回的列名就会全部转为小写。 **注意:** 这个拦截器会影响所有的查询,包括实体类和Map。对于实体类,需要确保实体类的属性名是小写的(或者与数据库列名的小写形式匹配,比如数据库列名为大写,转为小写后,实体类属性名如果是驼峰,则需要开启`mapUnderscoreToCamelCase`,或者使用`@Column`注解指定列名)。如果实体类属性名是大写,那么可能无法映射,因为数据库返回的列名是小写。 #### 2. JdbcTemplate 部分 对于JdbcTemplate,我们可以通过设置`ColumnMapRowMapper`来实现列名的小写转换。JdbcTemplate在查询时,可以通过指定`RowMapper`来控制结果映射。 我们可以自定义一个`RowMapper`,将列名转为小写。 **方案:** - 自定义`RowMapper`,在`mapRow`方法中,将列名转为小写。 - 或者,我们可以通过继承`ColumnMapRowMapper`并重写`mapRow`方法,在创建`Map`时,将键(即列名)转为小写。 **自定义RowMapper:** ```java import org.springframework.jdbc.core.ColumnMapRowMapper; import org.springframework.jdbc.support.JdbcUtils; import java.sql.ResultSet; import java.sql.ResultSetMetaData; import java.sql.SQLException; import java.util.Map; public class LowerCaseColumnMapRowMapper extends ColumnMapRowMapper { @Override public Map<String, Object> mapRow(ResultSet rs, int rowNum) throws SQLException { ResultSetMetaData rsmd = rs.getMetaData(); int columnCount = rsmd.getColumnCount(); Map<String, Object> map = createColumnMap(columnCount); for (int i = 1; i <= columnCount; i++) { String columnName = JdbcUtils.lookupColumnName(rsmd, i); // 将列名转为小写 columnName = columnName.toLowerCase(); Object obj = getColumnValue(rs, i); map.put(columnName, obj); } return map; } } ``` 然后,在使用JdbcTemplate查询时,指定这个RowMapper: ```java List<Map<String, Object>> result = jdbcTemplate.query(sql, new LowerCaseColumnMapRowMapper()); ``` 如果项目中有很多地方使用了JdbcTemplate的查询,我们可以通过配置一个全局的默认RowMapper。但是,JdbcTemplate默认的查询方法(如`queryForList`)内部使用的是`ColumnMapRowMapper`,我们无法直接替换。因此,我们可以创建一个自定义的JdbcTemplate,覆盖这些方法,或者在使用时显式指定RowMapper。 另一种方式:通过配置,将JdbcTemplate的默认RowMapper设置为我们自定义的RowMapper。但是,JdbcTemplate没有提供这样的设置。因此,我们可以通过AOP或者包装JdbcTemplate来实现。 **配置自定义的JdbcTemplate:** ```java @Configuration public class JdbcTemplateConfig { @Bean public JdbcTemplate jdbcTemplate(DataSource dataSource) { JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource); // 设置默认的RowMapper(用于queryForList等方法) // 但是,JdbcTemplate并没有直接提供设置默认RowMapper的方法,因此我们需要包装它 // 所以,我们创建一个自定义的JdbcTemplate return new LowerCaseJdbcTemplate(dataSource); } public static class LowerCaseJdbcTemplate extends JdbcTemplate { public LowerCaseJdbcTemplate(DataSource dataSource) { super(dataSource); } @Override public List<Map<String, Object>> queryForList(String sql) throws DataAccessException { return query(sql, new LowerCaseColumnMapRowMapper()); } @Override public List<Map<String, Object>> queryForList(String sql, @Nullable Object... args) throws DataAccessException { return query(sql, args, new LowerCaseColumnMapRowMapper()); } // 根据需要,覆盖其他queryForList方法 // 注意:queryForList还有其他重载方法,根据项目使用情况覆盖 } } ``` 这样,在使用`jdbcTemplate.queryForList`时,就会使用我们自定义的RowMapper,返回的列名都是小写。 **注意:** 如果项目中使用了其他返回Map的查询方法(如`queryForMap`),也需要覆盖相应的方法。 ### 总结 1. 对于MyBatis-Plus,我们通过拦截器将查询结果集的列名在元数据中转为小写,这样MyBatis在映射时会使用小写的列名。 2. 对于JdbcTemplate,我们通过自定义RowMapper将列名转为小写,并通过自定义JdbcTemplate覆盖默认的查询方法,使得返回的Map中键为小写。 这样,整个项目中的查询结果字段名都会是小写。 ### 注意事项 - 拦截器可能会影响性能,因为使用了动态代理,但通常影响不大。 - 确保实体类的属性名与转换后的小写列名匹配(例如,如果数据库列名是`USER_NAME`,转为小写后是`user_name`,而实体类属性名可能是`userName`,这时需要开启`mapUnderscoreToCamelCase`,或者使用`@Column`注解指定列名为`USER_NAME`,但这样就不会被转换了)。所以,如果使用拦截器,建议实体类属性名使用驼峰命名,并开启`mapUnderscoreToCamelCase`(在MyBatis配置中设置)。 在MyBatis配置文件中开启驼峰映射: ```xml <settings> <setting name="mapUnderscoreToCamelCase" value="true"/> </settings> ``` 这样,小写列名`user_name`会被映射到`userName`属性。 如果数据库列名是大写(如`USER_NAME`),经过拦截器转为小写(`user_name`),再通过驼峰映射转为`userName`,这样就能正确映射。 ### 相关问题 §§相关问题§§: 1. 在MyBatis中,除了使用拦截器,还有其他方式将查询结果的列名统一转为小写吗? 2. 如何优化上述拦截器的性能? 3. 在使用JdbcTemplate时,除了自定义RowMapper,还有其他方法实现返回结果字段名小写吗? 4. 如果项目中同时使用了MyBatis和JdbcTemplate,如何统一管理结果字段名的大小写转换?</think>### 解决方案:统一将Oracle查询结果字段名转为小写 在Java Web项目中同时使用MyBatis-Plus和JdbcTemplate查询Oracle数据库时,需要将所有返回结果字段名统一转为小写。以下是针对两种技术的完整解决方案: #### 1. MyBatis-Plus 解决方案 使用MyBatis的`ResultSetHandler`拦截器,在结果集处理阶段将列名转为小写。 ```java import org.apache.ibatis.executor.resultset.ResultSetHandler; import org.apache.ibatis.plugin.*; import java.sql.ResultSet; import java.sql.ResultSetMetaData; import java.sql.Statement; import java.util.*; @Intercepts({ @Signature(type = ResultSetHandler.class, method = "handleResultSets", args = {Statement.class}) }) public class LowerCaseInterceptor implements Interceptor { @Override public Object intercept(Invocation invocation) throws Throwable { // 执行原查询获取结果 List<Object> results = (List<Object>) invocation.proceed(); // 处理结果集:将Map的key转为小写 for (Object result : results) { if (result instanceof Map) { processMap((Map<?, ?>) result); } } return results; } private void processMap(Map<?, ?> map) { Map<String, Object> newMap = new LinkedHashMap<>(); map.forEach((key, value) -> { if (key instanceof String) { // 将key转为小写 newMap.put(((String) key).toLowerCase(), value); } else { newMap.put(key.toString(), value); } }); map.clear(); ((Map<String, Object>) map).putAll(newMap); } @Override public Object plugin(Object target) { return Plugin.wrap(target, this); } @Override public void setProperties(Properties properties) { } } ``` **配置到MyBatis-Plus:** ```xml <!-- 在mybatis-config.xml中注册拦截器 --> <configuration> <plugins> <plugin interceptor="com.example.LowerCaseInterceptor"/> </plugins> </configuration> ``` #### 2. JdbcTemplate 解决方案 自定义`RowMapper`实现字段名小写转换: ```java import org.springframework.jdbc.core.RowMapper; import java.sql.ResultSet; import java.sql.ResultSetMetaData; import java.sql.SQLException; import java.util.LinkedHashMap; import java.util.Map; public class LowerCaseRowMapper implements RowMapper<Map<String, Object>> { @Override public Map<String, Object> mapRow(ResultSet rs, int rowNum) throws SQLException { ResultSetMetaData metaData = rs.getMetaData(); int columnCount = metaData.getColumnCount(); Map<String, Object> resultMap = new LinkedHashMap<>(); for (int i = 1; i <= columnCount; i++) { // 获取列名并转为小写 String columnName = metaData.getColumnName(i).toLowerCase(); Object value = rs.getObject(i); resultMap.put(columnName, value); } return resultMap; } } ``` **使用示例:** ```java // 在Service中使用自定义RowMapper @Autowired private JdbcTemplate jdbcTemplate; public List<Map<String, Object>> queryData() { String sql = "SELECT USER_ID, USER_NAME FROM USERS"; return jdbcTemplate.query(sql, new LowerCaseRowMapper()); } ``` ### 解决方案说明 1. **MyBatis-Plus部分**: - 通过实现`Interceptor`接口创建结果集拦截器 - 在`intercept`方法中处理查询结果: - 遍历所有结果对象 - 当结果为Map类型时,将key转为小写 - 使用`Plugin.wrap`将拦截器织入MyBatis执行流程 - **优点**:全局生效,无需修改现有查询代码 2. **JdbcTemplate部分**: - 自定义`LowerCaseRowMapper`实现 - 在`mapRow`方法中: - 获取结果集元数据 - 遍历所有列并将列名转为小写 - 构建新的小写字段名Map - **优点**:精确控制结果集映射,可复用 3. **注意事项**: - 实体类映射:确保实体类属性名与转换后的小写列名匹配 - 性能影响:拦截器对性能影响极小(仅增加一次遍历操作) - 兼容性:同时支持MyBatis-Plus和JdbcTemplate的查询操作 - 特殊字符:处理带下划线的列名(如`USER_NAME` → `user_name`) ### 相关优化建议 1. 若项目使用实体类接收结果: ```java // 在实体类中使用@TableField匹配小写列名 @TableField(value = "user_id") private Long userId; ``` 2. 统一配置JdbcTemplate: ```java @Configuration public class JdbcConfig { @Bean public JdbcTemplate jdbcTemplate(DataSource dataSource) { JdbcTemplate jt = new JdbcTemplate(dataSource); // 设置默认RowMapper jt.setRowMapper(new LowerCaseRowMapper()); return jt; } } ``` 3. 处理嵌套结果集(MyBatis-Plus): ```java // 在拦截器中递归处理嵌套Map private void processNestedMap(Object obj) { if (obj instanceof Map) { processMap((Map<?, ?>) obj); } else if (obj instanceof List) { ((List<?>) obj).forEach(this::processNestedMap); } } ```
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值