Spring国际化实现
Java国际化
Java使用Unicode来处理所有字符。
Locales
国际化主要涉及的是数字、日期、金额等。
有若干个专门负责格式处理的类。为了对格式进行控制,可以使用Locale类。它描述了:
- 一种语言
- 一个位置(通常包含)
- 一段脚本(可选,自Java SE7开始支持)
- 一个变体(可选)
- 指定诸如方言或拼写规则之类的杂项。
Locale对象示例
language=English,location=United States
language=German,location=Germany #货币表示为欧元
#瑞士有四种官方语言,一个说德语的瑞士人使用:货币会被表示成瑞士法郎。
language=German,location=Switzerland
如果只设定了语言,则不能处理和国家相关的问题。
language=German
为了以一种简练而标准的方式来表达语言和位置,Java语言使用ISO所定义的编码。本地语言由小写的2个字母来代替,它遵循ISO-639-1,国家代码由大写的2个字母的代码来表示,它遵循ISO-3166-1,
只要提供了语言代码,或者语言与国家代码,就可以构造Locale对象了。
Locale german=new Locale("de")
Locale germanGermany=new Locale("de","DE")
locale germanSwitzerland = new Locale("de","CH")
JAVA SE定义了大量Locale对象和语言Locale,但是没有设定位置。
//获取默认Locale
Locale locale = Locale.getDefault()
对于所有依赖Locale的类,可以返回一个他们所支持的Locale列表。
Locale[] l = DataFormat.getAvailableLocales();
参考
数字格式
数字和货币是高度依赖locale的,Java提供了一个格式器(formatter)对象的集合,可以对java.txt包中的数字值进行格式化和解析。
相关类:
- NumberFormat
参考
货币标识(ISO 4217):https://www.iban.hk/currency-codes
日期和时间
当格式化日期和时间时,需要考虑4个与Locale相关的问题:
- 月份和星期
- 年月日的顺序
- 公历可能不是首选的日期表示法
- 时区
相关类:
- DataFormat
排序
Java语言中,String类的CompareTo()方式是用Unicode字符来决定顺序的。
如果需要根据Locale排序,则先获取一个Locale对象,然后获取**Collator**对象,再比较。
Locale loc = new Locale("de","DE");
Collator col = Collator.getInstance(loc);
if (col.Compare(a,b) < 0)
{... ...
}
排序强度
可以设置排序器的强度来选择不同的排序行为。字符间的差别可以分为:首要的(Primary),其次的(secondary),和再次的(tertiary)。
**Collator**定义了对应的枚举值
分解
偶尔一个字符或字符序列在描述成Unicode时,可以有多种方式。Unicode标准对字符串定义了四种范化形式:D,KD,C,KC。参考:https://www.unicode.org/reports/tr15/tr15-23.html
消息格式化
MessageFormat类,用来格式化带变量的文本,
"On {2},a {0} destroyed {1} houses and cuased {3} of damage"
占位符索引后可以跟一个类型和一个风格,他们之间用逗号隔开。
类型可以是:
- number
- time
- date
- choice
如果类型是number,则风格可以是:
- integer
- currency
- precent
参考:DecimalFormat类,SimpleDateFormat类。
静态的MessageFormat.format()方法使用当前Locale对值进行格式化,如果要指定Locale,则:
MessageFormat mf= new MessageFormat(pattern,loc); String msg= mf.format(new Object[]{values});
选择格式
选择格式可以用来根据不同语言,使用特殊的语法,例如a,an等
choice格式化选项就是为了这个目的。一个格式化选项是由一个序列对构成的,每一个对包括:
- 一个下限(lower limit)
- 一个格式字符串(format string)
下限和格式字符串由一个#符号分隔,对与对之间由符号|分隔。
{1,choice,0#no houses|1#one house|2#{1} houses}
| {1} | 结果 |
|---|---|
| 0 | “no houses” |
| 1 | “one house” |
| 3 | “3 houses” |
| -1 | “no houses” |
也可以用<,小于等于(\u2264)实现#相同的结果。
文本文件和字符集
源文件的字符编码
在程序编译和运行时,有3种字符编码参与其中:
- 源文件:本地编码
- 类文件:modified UTF-8
- 虚拟机:UTF-16
为了使源文件到处使用,则必须使用普通的ASCII吗,也就是说必须把所有非ASCII字符转换成等价的UNICODE字符。例如,spring源码中的 message资源文件。JDK自带工具,native2ascii,可以将本地字符编码转换成普通的ASCII编码。
资源包
定位资源包
当本地化一个应用时,会制造很多资源包(resource bundle)。每个包,都要为想要支持的locale提供相应的版本,格式:
包名_语言_国家
包名_语言
ResourceBundle resource=ResourceBundle.getBundle(bundleName,currentLocale);
getBundle方法试图加载匹配当前Locale定义的语言和国家的包,如果失败,则依次通过放弃国家和语言来进行查找,然后同样的查找被应用于默认的Locale,如果最后还是不行,则去查看默认的包文件,如果失败,则抛出异常。
- 包名_当前locale的语言_当前locale的国家_当前locale的变量
- 包名_当前locale的语言_当前locale的国家
- 包名_当前locale的语言
- 包名_默认locale的语言_默认locale的国家_默认locale的变量
- 包名_默认locale的语言_默认locale的国家
- 包名_默认locale的语言
- 包名
可以使用JDK命令对属性文件中非Unicode字符进行转码
native2ascii -encoding GBK messages_zh_CN.properties m.zh
属性文件
ResourceBundle resource=ResourceBundle.getBundle(bundleName,currentLocale);
String s = resource.getString("name")
包类
为了提供字符串类型以外的资源,需要定义类,它必须扩展自ResourceBundle类,使用标准的命名规则来命名类。
MyResource.java
Myresource_en.java
Myresource_en_UN.java//获取资源ResourceBundle resource=ResourceBundle.getBundle(bundleName,currentLocale);
(Color) resource.getObject("name");
(Map) resource.getObject("name");
Spring中的国际化实现
pring中国际化是通过MessageSource这个接口来支持的
public interface MessageSource {/*** 获取国际化信息* @param code 表示国际化资源中的属性名;* @param args 用于传递格式化串占位符所用的运行参数;* @param defaultMessage 当在资源找不到对应属性名时,返回defaultMessage参数所指定的默认信息;* @param locale 表示本地化对象*/@NullableString getMessage(String code, @Nullable Object[] args, @Nullable String defaultMessage, Locale locale);String getMessage(String code, @Nullable Object[] args, Locale locale) throws NoSuchMessageException;String getMessage(MessageSourceResolvable resolvable, Locale locale) throws NoSuchMessageException;}
常见3个实现类
ResourceBundleMessageSource:这个是基于Java的ResourceBundle基础类实现,允许仅通过资源名加载国际化资源
ReloadableResourceBundleMessageSource:这个功能和第一个类的功能类似,多了定时刷新功能,允许在不重启系统的情况下,更新资源的信息
StaticMessageSource:它允许通过编程的方式提供国际化信息,一会我们可以通过这个来实现db中存储国际化信息的功能。
ResourceBundleMessageSource
类继承图:

-
HierarchicalMessageSource
支持层级资源查找
public interface HierarchicalMessageSource extends MessageSource {void setParentMessageSource(@Nullable MessageSource parent);@NullableMessageSource getParentMessageSource();
}
-
MessageSourceSupport
提供 参数占位 格式化支持。
AbstractMessageSource
工厂方法模式。
public abstract class AbstractMessageSource extends MessageSourceSupport implements HierarchicalMessageSource {@Nullableprivate MessageSource parentMessageSource;@Nullableprivate Properties commonMessages;private boolean useCodeAsDefaultMessage = false;//获取消息 @Overridepublic final String getMessage(String code, @Nullable Object[] args, @Nullable String defaultMessage, Locale locale) {String msg = getMessageInternal(code, args, locale);if (msg != null) {return msg;}if (defaultMessage == null) {return getDefaultMessage(code);}return renderDefaultMessage(defaultMessage, args, locale);}@Overridepublic final String getMessage(String code, @Nullable Object[] args, Locale locale) throws NoSuchMessageException {String msg = getMessageInternal(code, args, locale);if (msg != null) {return msg;}String fallback = getDefaultMessage(code);if (fallback != null) {return fallback;}throw new NoSuchMessageException(code, locale);}@Overridepublic final String getMessage(MessageSourceResolvable resolvable, Locale locale) throws NoSuchMessageException {String[] codes = resolvable.getCodes();if (codes != null) {for (String code : codes) {String message = getMessageInternal(code, resolvable.getArguments(), locale);if (message != null) {return message;}}}String defaultMessage = getDefaultMessage(resolvable, locale);if (defaultMessage != null) {return defaultMessage;}throw new NoSuchMessageException(!ObjectUtils.isEmpty(codes) ? codes[codes.length - 1] : "", locale);}/*** 内部 实现,用于 子类扩展*/@Nullableprotected String getMessageInternal(@Nullable String code, @Nullable Object[] args, @Nullable Locale locale) {if (code == null) {return null;}if (locale == null) {locale = Locale.getDefault();}Object[] argsToUse = args;if (!isAlwaysUseMessageFormat() && ObjectUtils.isEmpty(args)) {String message = resolveCodeWithoutArguments(code, locale);if (message != null) {return message;}}else {argsToUse = resolveArguments(args, locale);MessageFormat messageFormat = resolveCode(code, locale);if (messageFormat != null) {synchronized (messageFormat) {return messageFormat.format(argsToUse);}}}// Check locale-independent common messages for the given message code.Properties commonMessages = getCommonMessages();if (commonMessages != null) {String commonMessage = commonMessages.getProperty(code);if (commonMessage != null) {return formatMessage(commonMessage, args, locale);}}// Not found -> check parent, if any.return getMessageFromParent(code, argsToUse, locale);}/*** 内部 实现,用于 子类扩展*/@Nullableprotected abstract MessageFormat resolveCode(String code, Locale locale);}
AbstractResourceBasedMessageSource
提供设置 basename的能力。
public abstract class AbstractResourceBasedMessageSource extends AbstractMessageSource {//保存basename。private final Set<String> basenameSet = new LinkedHashSet<>(4);//默认编码@Nullableprivate String defaultEncoding;private boolean fallbackToSystemLocale = true;//默认locale。@Nullableprivate Locale defaultLocale;
}
ResourceBundleMessageSource实现
protected MessageFormat resolveCode(String code, Locale locale) {Set<String> basenames = getBasenameSet();//循环所有basenamefor (String basename : basenames) {ResourceBundle bundle = getResourceBundle(basename, locale);if (bundle != null) {//获取指定的MessageFormatMessageFormat messageFormat = getMessageFormat(bundle, code, locale);if (messageFormat != null) {return messageFormat;}}}return null;}
bean名称必须是
messageSource。
Spring MVC 国际化使用
创建消息属性文件
通过basename指定消息的名称,则对不同locale创建不同的属性文件。具体见前面内容。
示例:
ValidationMessages.properties
ValidationMessages_ar.properties
ValidationMessages_cs.properties
ValidationMessages_de.properties
ValidationMessages_en.properties
ValidationMessages_es.properties
ValidationMessages_fa.properties
ValidationMessages_fr.properties
Spring boot 的默认
basename为 :messages,通过spring.messages.basename设置。属性文件配置在resources下。同一个basename会以Resource Bundle目录显示。
在每个属性文件中,为每个属性设置属性值。
示例:
javax.validation.constraints.NotNull.message = must not be null
javax.validation.constraints.Null.message = must be null
javax.validation.constraints.Past.message = must be a past date
设置LocaleResolver
Spring Mvc 默认有4种 LocaleResolver。
public interface LocaleResolver {Locale resolveLocale(HttpServletRequest request);void setLocale(HttpServletRequest request, @Nullable HttpServletResponse response, @Nullable Locale locale);}
AcceptHeaderLocaleResolver
接受 Accept-Language header 来设置locale。
SessionLocaleResolver
SessionLocaleResolver 将客户端的 Locale 保存到 HttpSession 对象中,并且可以进行修改(这意味着当前环境信息,前端给浏览器发送一次即可记住,只要 session 有效,浏览器就不必再次告诉服务端当前的环境信息)
public static final String LOCALE_SESSION_ATTRIBUTE_NAME = SessionLocaleResolver.class.getName() + ".LOCALE";
CookieLocaleResolver
Locale会保存到cookie中,cookieName:org.springframework.web.servlet.i18n.CookieLocaleResolver.LOCALE
public static final String LOCALE_REQUEST_ATTRIBUTE_NAME = CookieLocaleResolver.class.getName() + ".LOCALE";
接受的request参数,默认参数名为locale,通过LocaleChangeInterceptor 设置,可以在构造CookieLocaleResolver时设置。
FixedLocaleResolver
固定Locale。
spring.mvc.locale=zh_CN
#或者
spring:web:locale: zh_CNlocale-resolver: fixed
spring.web.locale-resolver优先级比spring.mvc.locale-resolver高一些。
spring.web.locale、spring.mvc.locale这两个配置属性,假如存在,就会成为AcceptHeaderLocaleResolver的默认的Locale 区域对象。 并在请求响应的请求头中没有Accept-Language这个属性时,成为AcceptHeaderLocaleResolver返回的Locale 区域对象。
LocaleChangeInterceptor
LocaleChangeInterceptor 则主要是负责参数解析的,在配置拦截器的时候,设置了参数名为 locale(默认),也就是说可以通过 locale 参数来传递当前的环境信息
@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)throws ServletException {//通过设置的参数,获取locale。String newLocale = request.getParameter(getParamName());if (newLocale != null) {if (checkHttpMethod(request.getMethod())) {//获取 LocaleResolverLocaleResolver localeResolver = RequestContextUtils.getLocaleResolver(request);if (localeResolver == null) {throw new IllegalStateException("No LocaleResolver found: not in a DispatcherServlet request?");}try {//设置 locale。localeResolver.setLocale(request, response, parseLocaleValue(newLocale));}catch (IllegalArgumentException ex) {if (isIgnoreInvalidLocale()) {if (logger.isDebugEnabled()) {logger.debug("Ignoring invalid locale value [" + newLocale + "]: " + ex.getMessage());}}else {throw ex;}}}}// Proceed in any case.return true;}
配置
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {@Overridepublic void addInterceptors(InterceptorRegistry registry) {registry.addInterceptor(localeChangeInterceptor());}@Beanpublic LocaleResolver localeResolver() {return new CookieLocaleResolver();}/*** 切换语言按钮URL?language=zh_CN,切换后将语言信息存入cookie;** @return*/@Beanpublic LocaleChangeInterceptor localeChangeInterceptor() {LocaleChangeInterceptor lci = new LocaleChangeInterceptor();//承载 locale的参数。lci.setParamName("language");return lci;}
}
相关文章:
Spring国际化实现
Java国际化 Java使用Unicode来处理所有字符。 Locales 国际化主要涉及的是数字、日期、金额等。 有若干个专门负责格式处理的类。为了对格式进行控制,可以使用Locale类。它描述了: 一种语言一个位置(通常包含)一段脚本(可选,自Java SE7开…...
10- 天猫用户复购预测 (机器学习集成算法) (项目十) *
项目难点 merchant: 商人重命名列名: user_log.rename(columns{seller_id:merchant_id}, inplaceTrue)数据类型转换: user_log[item_id] user_log[item_id].astype(int32)主要使用方法: xgboost, lightbm竞赛地址: 天猫复购预测之挑战Baseline_学习赛_天池大赛-阿里云天池…...
对于《MySQL 实战45讲》的理解
一.理论 一条SQL执行过程 连接器分析器优化器执行器 索引 索引的出现其实就是为了提高数据查询的效率,就像书的目录一样 常见索引数据结构(每碰到一个新数据库,我们需要先关注它的数据模型,这样才能从理论上分析出这个数据库的适用场景) 哈希…...
XQuery 函数
XQuery 1.0、XPath 2.0 以及 XSLT 2.0 共享相同的函数库。 XQuery 函数 XQuery 含有超过 100 个内建的函数。这些函数可用于字符串值、数值、日期以及时间比较、节点和 QName 操作、序列操作、逻辑值等等。您也可在 XQuery 中定义自己的函数。 XQuery 内建函数 XQuery 函数命…...
Elasticsearch的安装及常用操作
文章目录一、Elasticsearch的介绍1、Elasticsearch索引2、Elasticsearch的介绍二、Elasticsearch的安装1、安装ES服务2、安装kibana3、Docker安装ES4、Docker安装Kibana三、ES的常用操作1、索引操作2、文档操作3、域的属性3.1 index3.2 type3.3 store总结一、Elasticsearch的介…...
网络安全应急响应服务方案怎么写?包含哪些阶段?一文带你了解!
文章目录一、服务范围及流程1.1 服务范围1.2 服务流程及内容二、准备阶段2.1 负责人准备内容2.2 技术人员准备内容(一)服务需求界定(二)主机和网络设备安全初始化快照和备份2.3市场人员准备内容(1)预防和预…...
11、事务原理和实战,MVCC
事务原理和实战 1. 认识事务2. 事务控制语句2.1 开启事务2.2 事务提交2.3 事务回滚3. 事务的实现方式3.1 原子性3.2 一致性3.3 隔离性3.3 持久性4purge thread线程5事务统计QPS与TPS5.1 QPS5.2 TPS6. 事务隔离级别6.1 隔离级别6.2 查看隔离级别6.3 设置隔离级别6.4 不同隔离级别…...
Robust Self-Augmentation for Named Entity Recognition with Meta Reweighting
摘要 近年来,自我增强成为在低资源场景下提升命名实体识别性能的研究热点。Token substitution and mixup (token替换和表征混合)是两种有效提升NER性能的自增强方法。明显,自增强方法得到的增强数据可能由潜在的噪声。先前的研究…...
Java基础-xml
1.xml 1.1概述 万维网联盟(W3C) 万维网联盟(W3C)创建于1994年,又称W3C理事会。1994年10月在麻省理工学院计算机科学实验室成立。 建立者: Tim Berners-Lee (蒂姆伯纳斯李)。 是Web技术领域最具权威和影响力的国际中立性技术标准机构。 到目前为止&#…...
TCP的Nagle算法和delayed ack---延时发送和延时应答与稍带应答选项
本文目录提高TCP的网络利用率的二个思考解决方案:Nagle算法和delayed ack(延时发送和延时应答与稍带应答选项)Nagle算法和delayed ack算法同时启动可能会导致的问题提高TCP的网络利用率的二个思考 我们都知道,TCP是一个基于字节流…...
智能拣配单解决方案
电子货架标签系统(ESLs),是一种放置在货架上、可替代传统纸质价格标签的电子显示装置, 每一个电子货架标签通过有线或者无线网络与商场计算机数据库相连, 并将最新的商品价格通过电子货架标签上的屏显示出来。 电子…...
如何防御入侵服务器
根据中华人民共和国刑法: 第二百八十六条违反国家规定,对计算机信息系统功能进行删除、修改、增加、干扰,造成计算机信息系统不能正常运行,后果严重的,处五年以下有期徒刑或者拘役;后果特别严重的ÿ…...
[软件工程导论(第六版)]第4章 形式化说明技术(课后习题详解)
文章目录1. 举例对比形式化方法和欠形式化方法的优缺点。2. 在什么情况下应该使用形式化说明技术?使用形式化说明技术时应遵守哪些准则?3. 一个浮点二进制数的构成是:一个可选的符号(+或-)&…...
Premiere基础操作
一:设置缓存二:ctrI导入素材三:导入图像序列四:打开吸附。打开吸附后素材会对齐。五:按~键可以全屏窗口。六:向前选择轨道工具。在时间线上点击,向前选中时间线上素材。向后选择轨道工具&#x…...
Prometheus监控案例-tomcat、mysql、redis、haproxy、nginx
监控tomcat tomcat自身并不能提供监控指标数据,需要借助第三方exporter实现:https://github.com/nlighten/tomcat_exporter 构建镜像 基于tomcat官方镜像,重新制作一个镜像,将tomcat-exporter和tomcat整合到一起。Ddockerfile如…...
如何寻找SAP中的增强
文章目录0 简介1 寻找一代增强2 寻找二代增强2.2 在包里也可以看到2.3 在出口对象里输入包的名字也可以找到2.4 通过以下函数可以发现已有的增强2.5 也可以在cmod里直接找2.6 总结3 寻找第三代增强0 简介 在SAP中,对原代码的修改最不容易的是找增强,以下…...
算法刷题打卡第95天: 最大平均通过率
最大平均通过率 难度:中等 一所学校里有一些班级,每个班级里有一些学生,现在每个班都会进行一场期末考试。给你一个二维数组 classes ,其中 classes[i] [passi, totali] ,表示你提前知道了第 i 个班级总共有 totali…...
Springboot扩展点系列之终结篇:Bean的生命周期
前言关于Springboot扩展点系列已经输出了13篇文章,分别梳理出了各个扩展点的功能特性、实现方式和工作原理,为什么要花这么多时间来梳理这些内容?根本原因就是这篇文章:Spring bean的生命周期。你了解Spring bean生命周期…...
OnGUI Color 控件||Unity 3D GUI 简介||OnGUI TextField 控件
Unity 3D Color 控件与 Background Color 控件类似,都是渲染 GUI 颜色的,但是两者不同的是 Color 不但会渲染 GUI 的背景颜色,同时还会影响 GUI.Text 的颜色。具体使用时,要作如下定义:public static var color:Color;…...
【日刻一诗】
日刻一诗 1)LeetCode总结(线性表)_链表类 2)LeetCode总结(线性表)_栈队列类 3)LeetCode总结(线性表)_滑动窗口 4)LeetCode总结(线性表&#x…...
手游刚开服就被攻击怎么办?如何防御DDoS?
开服初期是手游最脆弱的阶段,极易成为DDoS攻击的目标。一旦遭遇攻击,可能导致服务器瘫痪、玩家流失,甚至造成巨大经济损失。本文为开发者提供一套简洁有效的应急与防御方案,帮助快速应对并构建长期防护体系。 一、遭遇攻击的紧急应…...
HTML 语义化
目录 HTML 语义化HTML5 新特性HTML 语义化的好处语义化标签的使用场景最佳实践 HTML 语义化 HTML5 新特性 标准答案: 语义化标签: <header>:页头<nav>:导航<main>:主要内容<article>&#x…...
【Linux】shell脚本忽略错误继续执行
在 shell 脚本中,可以使用 set -e 命令来设置脚本在遇到错误时退出执行。如果你希望脚本忽略错误并继续执行,可以在脚本开头添加 set e 命令来取消该设置。 举例1 #!/bin/bash# 取消 set -e 的设置 set e# 执行命令,并忽略错误 rm somefile…...
视频字幕质量评估的大规模细粒度基准
大家读完觉得有帮助记得关注和点赞!!! 摘要 视频字幕在文本到视频生成任务中起着至关重要的作用,因为它们的质量直接影响所生成视频的语义连贯性和视觉保真度。尽管大型视觉-语言模型(VLMs)在字幕生成方面…...
【python异步多线程】异步多线程爬虫代码示例
claude生成的python多线程、异步代码示例,模拟20个网页的爬取,每个网页假设要0.5-2秒完成。 代码 Python多线程爬虫教程 核心概念 多线程:允许程序同时执行多个任务,提高IO密集型任务(如网络请求)的效率…...
3403. 从盒子中找出字典序最大的字符串 I
3403. 从盒子中找出字典序最大的字符串 I 题目链接:3403. 从盒子中找出字典序最大的字符串 I 代码如下: class Solution { public:string answerString(string word, int numFriends) {if (numFriends 1) {return word;}string res;for (int i 0;i &…...
高防服务器能够抵御哪些网络攻击呢?
高防服务器作为一种有着高度防御能力的服务器,可以帮助网站应对分布式拒绝服务攻击,有效识别和清理一些恶意的网络流量,为用户提供安全且稳定的网络环境,那么,高防服务器一般都可以抵御哪些网络攻击呢?下面…...
第 86 场周赛:矩阵中的幻方、钥匙和房间、将数组拆分成斐波那契序列、猜猜这个单词
Q1、[中等] 矩阵中的幻方 1、题目描述 3 x 3 的幻方是一个填充有 从 1 到 9 的不同数字的 3 x 3 矩阵,其中每行,每列以及两条对角线上的各数之和都相等。 给定一个由整数组成的row x col 的 grid,其中有多少个 3 3 的 “幻方” 子矩阵&am…...
Hive 存储格式深度解析:从 TextFile 到 ORC,如何选对数据存储方案?
在大数据处理领域,Hive 作为 Hadoop 生态中重要的数据仓库工具,其存储格式的选择直接影响数据存储成本、查询效率和计算资源消耗。面对 TextFile、SequenceFile、Parquet、RCFile、ORC 等多种存储格式,很多开发者常常陷入选择困境。本文将从底…...
IP如何挑?2025年海外专线IP如何购买?
你花了时间和预算买了IP,结果IP质量不佳,项目效率低下不说,还可能带来莫名的网络问题,是不是太闹心了?尤其是在面对海外专线IP时,到底怎么才能买到适合自己的呢?所以,挑IP绝对是个技…...
