这篇文章让你搞懂 SpringMVC 国际化!
松哥之前写过 Spring Boot 国际化的问题,不过那一次没讲源码,这次咱们整点源码来深入理解下这个问题。
国际化,也叫 i18n,为啥叫这个名字呢?因为国际化英文是 internationalization ,在 i 和 n 之间有 18 个字母,所以叫 i18n。我们的应用如果做了国际化就可以在不同的语言环境下,方便的进行切换,最常见的就是中文和英文之间的切换,国际化这个功能也是相当的常见。
1.SpringMVC 国际化配置
还是先来说说用法,再来说源码,这样大家不容易犯迷糊。我们先说在 SSM 中如何处理国际化问题。
首先国际化我们可能有两种需求:
- 在页面渲染时实现国际化(这个借助于 Spring 标签实现)
- 在接口中获取国际化匹配后的消息
大致上就是上面这两种场景。接下来松哥通过一个简单的用法来和大家演示下具体玩法。
首先我们在项目的 resources 目录下新建语言文件,language_en_US.properties 和 language_zh-CN.properties,如下图:
内容分别如下:
language_en_US.properties:
login.username=Username
login.password=Password
language_zh-CN.properties:
login.username=用户名
login.password=用户密码
这两个分别对应英中文环境。配置文件写好之后,还需要在 SpringMVC 容器中提供一个 ResourceBundleMessageSource 实例去加载这两个实例,如下:
<bean class="org.springframework.context.support.ResourceBundleMessageSource" id="messageSource"><property name="basename" value="language"/><property name="defaultEncoding" value="UTF-8"/>
</bean>
这里配置了文件名 language 和默认的编码格式。
接下来我们新建一个 login.jsp 文件,如下:
<%@ taglib prefix="spring" uri="http://www.springframework.org/tags" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head><title>Title</title>
</head>
<body>
<spring:message code="login.username"/> <input type="text"> <br>
<spring:message code="login.password"/> <input type="text"> <br>
</body>
</html>
在这个文件中,我们通过 spring:message
标签来引用变量,该标签会根据当前的实际情况,选择合适的语言文件。
接下来我们为 login.jsp 提供一个控制器:
@Controller
public class LoginController {@AutowiredMessageSource messageSource;@GetMapping("/login")public String login() {String username = messageSource.getMessage("login.username", null, LocaleContextHolder.getLocale());String password = messageSource.getMessage("login.password", null, LocaleContextHolder.getLocale());System.out.println("username = " + username);System.out.println("password = " + password);return "login";}
}
控制器中直接返回 login 视图即可。
另外我这还注入了 MessageSource 对象,主要是为了向大家展示如何在处理器中获取国际化后的语言文字。
配置完成后,启动项目进行测试。
默认情况下,系统是根据请求头的中 Accept-Language 字段来判断当前的语言环境的,该这个字段由浏览器自动发送,我们这里为了测试方便,可以使用 POSTMAN 进行测试,然后手动设置 Accept_Language 字段。
首先测试中文环境:
然后测试英文环境:
都没问题,完美!同时观察 IDEA 控制台,也能正确打印出语言文字。
上面这个是基于 AcceptHeaderLocaleResolver 来解析出当前的区域和语言的。
有的时候,我们希望语言环境直接通过请求参数来传递,而不是通过请求头来传递,这个需求我们通过 SessionLocaleResolver 或者 CookieLocaleResolver 都可以实现。
先来看 SessionLocaleResolver。
首先在 SpringMVC 配置文件中提供 SessionLocaleResolver 的实例,同时配置一个拦截器,如下:
<mvc:interceptors><mvc:interceptor><mvc:mapping path="/**"/><bean class="org.springframework.web.servlet.i18n.LocaleChangeInterceptor"><property name="paramName" value="locale"/></bean></mvc:interceptor>
</mvc:interceptors>
<bean class="org.springframework.web.servlet.i18n.SessionLocaleResolver" id="localeResolver">
</bean>
SessionLocaleResolver 是负责区域解析的,这个没啥好说的。拦截器 LocaleChangeInterceptor 则主要是负责参数解析的,我们在配置拦截器的时候,设置了参数名为 locale(默认即此),也就是说我们将来可以通过 locale 参数来传递当前的环境信息。
配置完成后,我们还是来访问刚才的 login 控制器,如下:
此时我们可以直接通过 locale 参数来控制当前的语言环境,这个 locale 参数就是在前面所配置的 LocaleChangeInterceptor 拦截器中被自动解析的。
如果你不想配置 LocaleChangeInterceptor 拦截器也是可以的,直接自己手动解析 locale 参数然后设置 locale 也行,像下面这样:
@Controller
public class LoginController {@AutowiredMessageSource messageSource;@GetMapping("/login")public String login(String locale,HttpSession session) {if ("zh-CN".equals(locale)) {session.setAttribute(SessionLocaleResolver.LOCALE_SESSION_ATTRIBUTE_NAME, new Locale("zh", "CN"));} else if ("en-US".equals(locale)) {session.setAttribute(SessionLocaleResolver.LOCALE_SESSION_ATTRIBUTE_NAME, new Locale("en", "US"));}String username = messageSource.getMessage("login.username", null, LocaleContextHolder.getLocale());String password = messageSource.getMessage("login.password", null, LocaleContextHolder.getLocale());System.out.println("username = " + username);System.out.println("password = " + password);return "login";}
}
SessionLocaleResolver 所实现的功能也可以通过 CookieLocaleResolver 来实现,不同的是前者将解析出来的区域信息保存在 session 中,而后者则保存在 Cookie 中。保存在 session 中,只要 session 没有发生变化,后续就不用再次传递区域语言参数了,保存在 Cookie 中,只要 Cookie 没变,后续也不用再次传递区域语言参数了。
使用 CookieLocaleResolver 的方式很简单,直接在 SpringMVC 中提供 CookieLocaleResolver 的实例即可,如下:
<bean class="org.springframework.web.servlet.i18n.CookieLocaleResolver" id="localeResolver"/>
注意这里也需要使用到 LocaleChangeInterceptor 拦截器,如果不使用该拦截器,则需要自己手动解析并配置语言环境,手动解析并配置的方式如下:
@GetMapping("/login3")
public String login3(String locale, HttpServletRequest req, HttpServletResponse resp) {CookieLocaleResolver resolver = new CookieLocaleResolver();if ("zh-CN".equals(locale)) {resolver.setLocale(req, resp, new Locale("zh", "CN"));} else if ("en-US".equals(locale)) {resolver.setLocale(req, resp, new Locale("en", "US"));}String username = messageSource.getMessage("login.username", null, LocaleContextHolder.getLocale());String password = messageSource.getMessage("login.password", null, LocaleContextHolder.getLocale());System.out.println("username = " + username);System.out.println("password = " + password);return "login";
}
配置完成后,启动项目进行测试,这次测试的方式跟 SessionLocaleResolver 的测试方式一致,松哥就不再多说了。
除了前面介绍的这几种 LocaleResolver 之外,还有一个 FixedLocaleResolver,因为比较少见,松哥这里就不做过多介绍了。
2.Spring Boot 国际化配置
2.1 基本使用
Spring Boot 和 Spring 一脉相承,对于国际化的支持,默认是通过 AcceptHeaderLocaleResolver 解析器来完成的,这个解析器,默认是通过请求头的 Accept-Language 字段来判断当前请求所属的环境的,进而给出合适的响应。
所以在 Spring Boot 中做国际化,这一块我们可以不用配置,直接就开搞。
首先创建一个普通的 Spring Boot 项目,添加 web 依赖即可。项目创建成功后,默认的国际化配置文件放在 resources 目录下,所以我们直接在该目录下创建四个测试文件,如下:
- 我们的 message 文件是直接创建在 resources 目录下的,IDEA 在展示的时候,会多出一个 Resource Bundle,这个大家不用管,千万别手动去创建这个目录。
- messages.properties 这个是默认的配置,其他的则是不同语言环境下的配置,en_US 是英语(美国),zh_CN 是中文简体,zh_TW 是中文繁体(文末附录里边有一个完整的语言简称表格)。
四个文件创建好之后,第一个默认的我们可以先空着,另外三个分别填入以下内容:
messages_zh_CN.properties
user.name=江南一点雨
messages_zh_TW.properties
user.name=江南壹點雨
messages_en_US.properties
user.name=javaboy
配置完成后,我们就可以直接开始使用了。在需要使用值的地方,直接注入 MessageSource 实例即可。
在 Spring 中需要配置的 MessageSource 现在不用配置了,Spring Boot 会通过
org.springframework.boot.autoconfigure.context.MessageSourceAutoConfiguration
自动帮我们配置一个 MessageSource 实例。
创建一个 HelloController ,内容如下:
@RestController
public class HelloController {@AutowiredMessageSource messageSource;@GetMapping("/hello")public String hello() {return messageSource.getMessage("user.name", null, LocaleContextHolder.getLocale());}
}
在 HelloController 中我们可以直接注入 MessageSource 实例,然后调用该实例中的 getMessage 方法去获取变量的值,第一个参数是要获取变量的 key,第二个参数是如果 value 中有占位符,可以从这里传递参数进去,第三个参数传递一个 Locale 实例即可,这相当于当前的语言环境。
接下来我们就可以直接去调用这个接口了。
默认情况下,在接口调用时,通过请求头的 Accept-Language 来配置当前的环境,我这里通过 POSTMAN 来进行测试,结果如下:
小伙伴们看到,我在请求头中设置了 Accept-Language 为 zh-CN,所以拿到的就是简体中文;如果我设置了 zh-TW,就会拿到繁体中文:
是不是很 Easy?
2.2 自定义切换
有的小伙伴觉得切换参数放在请求头里边好像不太方便,那么也可以自定义解析方式。例如参数可以当成普通参数放在地址栏上,通过如下配置可以实现我们的需求。
@Configuration
public class WebConfig implements WebMvcConfigurer {@Overridepublic void addInterceptors(InterceptorRegistry registry) {LocaleChangeInterceptor interceptor = new LocaleChangeInterceptor();interceptor.setParamName("lang");registry.addInterceptor(interceptor);}@BeanLocaleResolver localeResolver() {SessionLocaleResolver localeResolver = new SessionLocaleResolver();localeResolver.setDefaultLocale(Locale.SIMPLIFIED_CHINESE);return localeResolver;}
}
在这段配置中,我们首先提供了一个 SessionLocaleResolver 实例,这个实例会替换掉默认的 AcceptHeaderLocaleResolver,不同于 AcceptHeaderLocaleResolver 通过请求头来判断当前的环境信息,SessionLocaleResolver 将客户端的 Locale 保存到 HttpSession 对象中,并且可以进行修改(这意味着当前环境信息,前端给浏览器发送一次即可记住,只要 session 有效,浏览器就不必再次告诉服务端当前的环境信息)。
另外我们还配置了一个拦截器,这个拦截器会拦截请求中 key 为 lang 的参数(不配置的话是 locale),这个参数则指定了当前的环境信息。
好了,配置完成后,启动项目,访问方式如下:
我们通过在请求中添加 lang 来指定当前环境信息。这个指定只需要一次即可,也就是说,在 session 不变的情况下,下次请求可以不必带上 lang 参数,服务端已经知道当前的环境信息了。
CookieLocaleResolver 也是类似用法,不再赘述。
2.3 其他自定义
默认情况下,我们的配置文件放在 resources 目录下,如果大家想自定义,也是可以的,例如定义在 resources/i18n 目录下:
但是这种定义方式系统就不知道去哪里加载配置文件了,此时还需要 application.properties 中进行额外配置(注意这是一个相对路径):
spring.messages.basename=i18n/messages
另外还有一些编码格式的配置等,内容如下:
spring.messages.cache-duration=3600
spring.messages.encoding=UTF-8
spring.messages.fallback-to-system-locale=true
spring.messages.cache-duration 表示 messages 文件的缓存失效时间,如果不配置则缓存一直有效。
spring.messages.fallback-to-system-locale 属性则略显神奇,网上竟然看不到一个明确的答案,后来翻了一会源码才看出端倪。
这个属性的作用在 org.springframework.context.support.AbstractResourceBasedMessageSource#getDefaultLocale
方法中生效:
protected Locale getDefaultLocale() {if (this.defaultLocale != null) {return this.defaultLocale;}if (this.fallbackToSystemLocale) {return Locale.getDefault();}return null;
}
从这段代码可以看出,在找不到当前系统对应的资源文件时,如果该属性为 true,则会默认查找当前系统对应的资源文件,否则就返回 null,返回 null 之后,最终又会调用到系统默认的 messages.properties 文件。
3.LocaleResolver
国际化这块主要涉及到的组件是 LocaleResolver,这是一个开放的接口,官方默认提供了四个实现。当前该使用什么环境,主要是通过 LocaleResolver 来进行解析的。
LocaleResolver
public interface LocaleResolver {Locale resolveLocale(HttpServletRequest request);void setLocale(HttpServletRequest request, @Nullable HttpServletResponse response, @Nullable Locale locale);}
这里两个方法:
- resolveLocale:根据当前请求解析器出 Locale 对象。
- 设置 Locale 对象。
我们来看看 LocaleResolver 的继承关系:
虽然中间有几个抽象类,不过最终负责实现的其实就四个:
- AcceptHeaderLocaleResolver:根据请求头中的 Accept-Language 字段来确定当前的区域语言等。
- SessionLocaleResolver:根据请求参数来确定区域语言等,确定后会保存在 Session 中,只要 Session 不变,Locale 对象就一直有效。
- CookieLocaleResolver:根据请求参数来确定区域语言等,确定后会保存在 Cookie 中,只要 Session 不变,Locale 对象就一直有效。
- FixedLocaleResolver:配置时直接提供一个 Locale 对象,以后不能修改。
接下来我们就对这几个类逐一进行分析。
3.1 AcceptHeaderLocaleResolver
AcceptHeaderLocaleResolver 直接实现了 LocaleResolver 接口,我们来看它的 resolveLocale 方法:
@Override
public Locale resolveLocale(HttpServletRequest request) {Locale defaultLocale = getDefaultLocale();if (defaultLocale != null && request.getHeader("Accept-Language") == null) {return defaultLocale;}Locale requestLocale = request.getLocale();List<Locale> supportedLocales = getSupportedLocales();if (supportedLocales.isEmpty() || supportedLocales.contains(requestLocale)) {return requestLocale;}Locale supportedLocale = findSupportedLocale(request, supportedLocales);if (supportedLocale != null) {return supportedLocale;}return (defaultLocale != null ? defaultLocale : requestLocale);
}
- 首先去获取默认的 Locale 对象。
- 如果存在默认的 Locale 对象,并且请求头中没有设置
Accept-Language
字段,则直接返回默认的 Locale。 - 从 request 中取出当前的 Locale 对象,然后查询出支持的 supportedLocales,如果 supportedLocales 或者 supportedLocales 中包含 requestLocale,则直接返回 requestLocale。
- 如果前面还是没有匹配成功的,则从 request 中取出 locales 集合,然后再去和支持的 locale 进行比对,选择匹配成功的 locale 返回。
- 如果前面都没能返回,则判断 defaultLocale 是否为空,如果不为空,就返回 defaultLocale,否则返回 defaultLocale。
再来看看它的 setLocale 方法,直接抛出异常,意味着通过请求头处理 Locale 是不允许修改的。
@Override
public void setLocale(HttpServletRequest request, @Nullable HttpServletResponse response, @Nullable Locale locale) {throw new UnsupportedOperationException("Cannot change HTTP accept header - use a different locale resolution strategy");
}
3.2 SessionLocaleResolver
SessionLocaleResolver 的实现多了一个抽象类 AbstractLocaleContextResolver,AbstractLocaleContextResolver 中增加了对 TimeZone 的支持,我们先来看下 AbstractLocaleContextResolver:
public abstract class AbstractLocaleContextResolver extends AbstractLocaleResolver implements LocaleContextResolver {@Nullableprivate TimeZone defaultTimeZone;public void setDefaultTimeZone(@Nullable TimeZone defaultTimeZone) {this.defaultTimeZone = defaultTimeZone;}@Nullablepublic TimeZone getDefaultTimeZone() {return this.defaultTimeZone;}@Overridepublic Locale resolveLocale(HttpServletRequest request) {Locale locale = resolveLocaleContext(request).getLocale();return (locale != null ? locale : request.getLocale());}@Overridepublic void setLocale(HttpServletRequest request, @Nullable HttpServletResponse response, @Nullable Locale locale) {setLocaleContext(request, response, (locale != null ? new SimpleLocaleContext(locale) : null));}}
可以看到,多了一个 TimeZone 属性。从请求中解析出 Locale 还是调用了 resolveLocaleContext 方法,该方法在子类中被实现,另外调用 setLocaleContext 方法设置 Locale,该方法的实现也在子类中。
我们来看下它的子类 SessionLocaleResolver:
@Override
public Locale resolveLocale(HttpServletRequest request) {Locale locale = (Locale) WebUtils.getSessionAttribute(request, this.localeAttributeName);if (locale == null) {locale = determineDefaultLocale(request);}return locale;
}
直接从 Session 中获取 Locale,默认的属性名是 SessionLocaleResolver.class.getName() + ".LOCALE"
,如果 session 中不存在 Locale 信息,则调用 determineDefaultLocale 方法去加载 Locale,该方法会首先找到 defaultLocale,如果 defaultLocale 不为 null 就直接返回,否则就从 request 中获取 Locale 返回。
再来看 setLocaleContext 方法,就是将解析出来的 Locale 保存起来。
@Override
public void setLocaleContext(HttpServletRequest request, @Nullable HttpServletResponse response,@Nullable LocaleContext localeContext) {Locale locale = null;TimeZone timeZone = null;if (localeContext != null) {locale = localeContext.getLocale();if (localeContext instanceof TimeZoneAwareLocaleContext) {timeZone = ((TimeZoneAwareLocaleContext) localeContext).getTimeZone();}}WebUtils.setSessionAttribute(request, this.localeAttributeName, locale);WebUtils.setSessionAttribute(request, this.timeZoneAttributeName, timeZone);
}
保存到 Session 中即可。大家可以看到,这种保存方式其实和我们前面演示的自己保存代码基本一致,殊途同归。
3.3 FixedLocaleResolver
FixedLocaleResolver 有三个构造方法,无论调用哪一个,都会配置默认的 Locale:
public FixedLocaleResolver() {setDefaultLocale(Locale.getDefault());
}
public FixedLocaleResolver(Locale locale) {setDefaultLocale(locale);
}
public FixedLocaleResolver(Locale locale, TimeZone timeZone) {setDefaultLocale(locale);setDefaultTimeZone(timeZone);
}
要么自己传 Locale 进来,要么调用 Locale.getDefault() 方法获取默认的 Locale。
再来看 resolveLocale 方法:
@Override
public Locale resolveLocale(HttpServletRequest request) {Locale locale = getDefaultLocale();if (locale == null) {locale = Locale.getDefault();}return locale;
}
这个应该就不用解释了吧。
需要注意的是它的 setLocaleContext 方法,直接抛异常出来,也就意味着 Locale 在后期不能被修改。
@Override
public void setLocaleContext( HttpServletRequest request, @Nullable HttpServletResponse response,@Nullable LocaleContext localeContext) {throw new UnsupportedOperationException("Cannot change fixed locale - use a different locale resolution strategy");
}
3.4 CookieLocaleResolver
CookieLocaleResolver 和 SessionLocaleResolver 比较类似,只不过存储介质变成了 Cookie,其他都差不多,松哥就不再重复介绍了。
4.附录
搜刮了一个语言简称表,分享给各位小伙伴:
语言 | 简称 |
---|---|
简体中文(中国) | zh_CN |
繁体中文(中国台湾) | zh_TW |
繁体中文(中国香港) | zh_HK |
英语(中国香港) | en_HK |
英语(美国) | en_US |
英语(英国) | en_GB |
英语(全球) | en_WW |
英语(加拿大) | en_CA |
英语(澳大利亚) | en_AU |
英语(爱尔兰) | en_IE |
英语(芬兰) | en_FI |
芬兰语(芬兰) | fi_FI |
英语(丹麦) | en_DK |
丹麦语(丹麦) | da_DK |
英语(以色列) | en_IL |
希伯来语(以色列) | he_IL |
英语(南非) | en_ZA |
英语(印度) | en_IN |
英语(挪威) | en_NO |
英语(新加坡) | en_SG |
英语(新西兰) | en_NZ |
英语(印度尼西亚) | en_ID |
英语(菲律宾) | en_PH |
英语(泰国) | en_TH |
英语(马来西亚) | en_MY |
英语(阿拉伯) | en_XA |
韩文(韩国) | ko_KR |
日语(日本) | ja_JP |
荷兰语(荷兰) | nl_NL |
荷兰语(比利时) | nl_BE |
葡萄牙语(葡萄牙) | pt_PT |
葡萄牙语(巴西) | pt_BR |
法语(法国) | fr_FR |
法语(卢森堡) | fr_LU |
法语(瑞士) | fr_CH |
法语(比利时) | fr_BE |
法语(加拿大) | fr_CA |
西班牙语(拉丁美洲) | es_LA |
西班牙语(西班牙) | es_ES |
西班牙语(阿根廷) | es_AR |
西班牙语(美国) | es_US |
西班牙语(墨西哥) | es_MX |
西班牙语(哥伦比亚) | es_CO |
西班牙语(波多黎各) | es_PR |
德语(德国) | de_DE |
德语(奥地利) | de_AT |
德语(瑞士) | de_CH |
俄语(俄罗斯) | ru_RU |
意大利语(意大利) | it_IT |
希腊语(希腊) | el_GR |
挪威语(挪威) | no_NO |
匈牙利语(匈牙利) | hu_HU |
土耳其语(土耳其) | tr_TR |
捷克语(捷克共和国) | cs_CZ |
斯洛文尼亚语 | sl_SL |
波兰语(波兰) | pl_PL |
瑞典语(瑞典) | sv_SE |
西班牙语(智利) | es_CL |
5.小结
好啦,今天主要和小伙伴们聊了下 SpringMVC 中的国际化问题,以及 LocaleResolver 相关的源码,相信大家对 SpringMVC 的理解应该又更近一步了吧。
这篇文章让你搞懂 SpringMVC 国际化!相关推荐
- 运算放大器基本公式_还在被三阶/四阶/运算放大器滤波器PLL这些概念困扰?这篇文章帮你搞懂它...
<e世绘>系列 这是关于现代合成器的系列文章的第一篇文章,本文介绍了基本的锁相环操作以及各种拓扑结构. 近年来,频率合成技术发生了重大变化.数十年来,超低噪声的分立式VCO一直是低噪声合成 ...
- 两篇文章带你搞懂GC垃圾回收之基础篇
文章目录 1.JVM GC回收哪个区域的垃圾? 2.判断对象可以回收的方法 2.1 引用计数法 2.2 可达分析算法 2.3 什么对象可以当作GC Roots? 虚拟机栈中的引用对象 全局的静态的对象 ...
- 看了这篇文章,我搞懂了StringTable
好好学习,天天向上 本文已收录至我的Github仓库DayDayUP:github.com/RobodLee/DayDayUP,欢迎Star,更多文章请前往:目录导航 前言 String应该是Java ...
- opc客户端读取数据品质是bad_听说看了这篇文章就彻底搞懂了什么是OPC(上)
从2000年初以来,我们就一直在使用OPC软件互操作性标准,而那些正准备踏入和想要踏入工业自动化领域的人们却对这些含义感到困惑. 所以在本中,我将系统地为你梳理OPC知识. OPC首字母缩写词代表什么 ...
- android adc按键原理,看完这篇文章,终于搞懂了ADC的原理及分类!
在仪器仪表系统中,常常需要将检测到的连续变化的模拟量如:温度.压力.流量.速度.光强等转变成离散的数字量,才能输入到计算机中进行处理.这些模拟量经过传感器转变成电信号(一般为电压信号),经过放大器放大 ...
- C语言重点——指针篇(一篇让你完全搞懂指针)
C语言重点--指针篇(一篇让你完全搞懂指针) 一. 前言 C语言是比较偏底层的语言,为什么他比较偏底层,就是因为他的很多操作都是直接针对内存操作的. 这篇我们就来讲解C语言的一大特点,也是难点,指针和 ...
- 【UE·蓝图底层篇】一文搞懂NativeClass、GeneratedClass、BlueprintClass、ParentClass
本文将对蓝图类UBlueprint的几个UClass成员变量NativeClass.GeneratedClass.BlueprintClass.ParentClass进行比较深入的讲解,看完之后对蓝图 ...
- 安防人 这篇文章你能看懂多少?
安防人 这篇文章你能看懂多少? 在互联网技术高速发展的今天,因网络的开放性.隐蔽性.跨地域性等特性,许多安全问题亟待解决,比如在过去的2015年,全球便发生了多起网络安全事件,如美国人事管理局OPM数 ...
- 关于Facebook messenger广告,看了这篇文章你就全懂了
关于Facebook messenger广告,看了这篇文章你就全懂了 想产生更多的线索?考虑点击Messenger广告,这是最有效的线索生成广告之一.这篇博文解释了什么是点击Messenger广告,以 ...
- gorm存指针数据_C语言重点——指针篇(一篇让你完全搞懂指针)
一. 前言 C语言是比较偏底层的语言,为什么他比较偏底层,就是因为他的很多操作都是直接针对内存操作的. 这篇我们就来讲解C语言的一大特点,也是难点,指针和指针操作. 这篇文章我会先从基本类型的存储过程 ...
最新文章
- Flutter控件--Switch 和 SwitchListTile
- python 爬虫 scrapy 和 requsts 哪个快_Scrapy爬虫框架结构以及和Requests库的比较
- 第三方登录 人人php,人人网第三方登录接口方案
- 红黑树 RBTree
- Linux for Matlab中文注释乱码(亲测有效)
- moodle4.04无法上传中文文件名
- python按键精灵是哪个库_按键精灵命令库的介绍和使用
- ubuntu20.04安装qq音乐并解决闪退问题
- 需求分析-业务需求、用户需求、功能需求
- excel财务案例建模_Pro Excel财务建模:技术创业公司的构建模型
- 如何让电脑的时间显示到秒
- bittorrent_最好的免费BitTorrent客户
- 对比LDA,NCA,PCA
- 自己做语料——Python爬取新闻联播文字版
- 我在上海奋斗的五年---从月薪3500到700万(读后感:一个真汉子的人生)
- C语言实现图的深度优先遍历和广度优先遍历
- 微信小程序语音识别java_微信小程序实现语音识别功能
- nginx屏蔽某些地区访问网站
- 【牛客网——算法刷题】开篇介绍
- python重复执行命令_怎样能重复执行一条命令直到运行成功