当前位置: 首页 > news >正文

关键词自然排名优化搜索引擎优化的主要手段

关键词自然排名优化,搜索引擎优化的主要手段,建筑网建设通查询,高端大气的ppt模板最近在新项目的开发过程中,遇到了个问题,需要将一些异常的业务流程返回给前端,需要提供给前端不同的响应码,前端再在次基础上做提示语言的国际化适配。这些异常流程涉及业务层和控制层的各个地方,如果每个地方都写一些…

最近在新项目的开发过程中,遇到了个问题,需要将一些异常的业务流程返回给前端,需要提供给前端不同的响应码,前端再在次基础上做提示语言的国际化适配。这些异常流程涉及业务层和控制层的各个地方,如果每个地方都写一些重复代码显得很冗余。

然后查询解决方案时发现了@ControllerAdvice这个注解,可以对业务异常进行统一处理。经过仔细了解后,发现这个注解还有更多的用处,都很实用。

1 ControllerAdvice介绍

@ControllerAdvice一般和三个以下注解一块使用,起到不同的作用,

  1. @ExceptionHandler: 该注解作用于方法上,,可以捕获到controller中抛出的一些自定义异常,统一进行处理,一般用于进行一些特定的异常处理。
  2. @InitBinder:该注解作用于方法上,用于将前端请求的特定类型的参数在到达controller之前进行处理,从而达到转换请求参数格式的目的。
  3. @ModelAttribute:该注解作用于方法和请求参数上,在方法上时设置一个值,可以直接在进入controller后传入该参数。

2 ControllerAdvice应用场景

2.1@ExceptionHandler统一处理业务异常

@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {
// 这里就是对各个层返回的异常进行统一捕获处理
@ExceptionHandler(value = BusinessException.class)
public ResponseData<Void> bizException(BusinessException e){log.error("业务异常记录",e);return ResponseData.error(e.getCode(),e.getMessage());
}
}
//业务异常处代码示例:
if(CollectionUtil.isNotEmpty(companies)){
// 通过BusinessExceptionEnum枚举对业务异常进行统一管理
throw new BusinessException(BusinessExceptionEnum.ERROR_10003);
}

需要注意的是,如果这里有多个ExceptionHandler,按照异常类的层次体系,越高层的异常,优先级越低。

2.2@InitBinder做日期格式的统一处理

@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {
// 将前端传入的字符串时间格式转换为LocalDate时间  
@InitBinderprotected void initBinder(WebDataBinder binder) {
//将前端传入的字符串格式时间数据转为LocalDate格式的数据binder.registerCustomEditor(LocalDate.class, new PropertyEditorSupport() {@Overridepublic void setAsText(String text) throws IllegalArgumentException {setValue(LocalDate.parse(text, DateTimeFormatter.ofPattern(DEFAULT_DATE_FORMAT)));}});
//将前端传入的字符串格式时间数据转为LocalDateTime格式的数据binder.registerCustomEditor(LocalDateTime.class, new PropertyEditorSupport() {@Overridepublic void setAsText(String text) throws IllegalArgumentException {setValue(LocalDateTime.parse(text, DateTimeFormatter.ofPattern(DEFAULT_DATE_TIME_FORMAT)));}});
//将前端传入的字符串格式时间数据转为LocalTim格式的数据binder.registerCustomEditor(LocalTime.class, new PropertyEditorSupport() {@Overridepublic void setAsText(String text) throws IllegalArgumentException {setValue(LocalTime.parse(text, DateTimeFormatter.ofPattern(DEFAULT_TIME_FORMAT)));}});}
}
// controller进行参数绑定
public ResponseData<List<WorkCalendarVo>> listWorkCalendar(@RequestParam LocalDate  date){}

2.3 ModelAttribute提前绑定全局user对象

// 这里@ModelAttribute("loginUser")标注的modelAttribute()方法表示会在Controller方法之前将user设置到contoller里的已绑定参数里@ModelAttribute("loginUser")public User setLoginUser(HttpServletRequest request) {return LoginContextUtils.getLoginUser(request);}
// 使用@PostMapping("/list")public ResponseData<IPage<EmployeeVo>> listEmployee(@ModelAttribute("loginUser") User user, @RequestBody EmployeeSearch employeeSearch){return ResponseData.success(employeeService.listEmployee(user, employeeSearch));}

3 ControllerAdvice作用原理探究

在探究ControllerAdvice如何生效时,不得不提到springMvc绕不过的DispatcherServlet,这个类是SpringMVC统一的入口,所有的请求都通过它,里面的一些初始化方法如下。

public class DispatcherServlet extends FrameworkServlet {// ......protected void initStrategies(ApplicationContext context) {initMultipartResolver(context);initLocaleResolver(context);initThemeResolver(context);initHandlerMappings(context);
//请求处理的adapterinitHandlerAdapters(context);
// 异常响应处理的resolverinitHandlerExceptionResolvers(context);initRequestToViewNameTranslator(context);initViewResolvers(context);initFlashMapManager(context);}// ......
}

3.1@initBinder和@ModelAttribute的作用原理

@initBinder和@ModelAttribute都是请求过程中的处理,我们知道springMvc通过HandlerApapter定位到具体的方法进行请求处理,因此查看HandlerHaper的实现类,发现RequestMappingHandlerAdapter比较符合我们的目标

点进去RequestMappingHandlerAdapter后发现里面的一个方法如下

@Overridepublic void afterPropertiesSet() {// Do this first, it may add ResponseBody advice beans
// 这里会添加ResponseBody advice beansinitControllerAdviceCache();if (this.argumentResolvers == null) {List<HandlerMethodArgumentResolver> resolvers = getDefaultArgumentResolvers();this.argumentResolvers = new HandlerMethodArgumentResolverComposite().addResolvers(resolvers);}if (this.initBinderArgumentResolvers == null) {List<HandlerMethodArgumentResolver> resolvers = getDefaultInitBinderArgumentResolvers();this.initBinderArgumentResolvers = new HandlerMethodArgumentResolverComposite().addResolvers(resolvers);}if (this.returnValueHandlers == null) {List<HandlerMethodReturnValueHandler> handlers = getDefaultReturnValueHandlers();this.returnValueHandlers = new HandlerMethodReturnValueHandlerComposite().addHandlers(handlers);}}
// 这里找到contollerAdvice注解的类,缓存里面的方法
private void initControllerAdviceCache() {if (getApplicationContext() == null) {return;}
// 找到@ControllerAdvice注解标注的类List<ControllerAdviceBean> adviceBeans = ControllerAdviceBean.findAnnotatedBeans(getApplicationContext());List<Object> requestResponseBodyAdviceBeans = new ArrayList<>();for (ControllerAdviceBean adviceBean : adviceBeans) {Class<?> beanType = adviceBean.getBeanType();if (beanType == null) {throw new IllegalStateException("Unresolvable type for ControllerAdviceBean: " + adviceBean);}
// 找到所有ModelAttribute标注的方法进行缓存,就可以使用了Set<Method> attrMethods = MethodIntrospector.selectMethods(beanType, MODEL_ATTRIBUTE_METHODS);if (!attrMethods.isEmpty()) {this.modelAttributeAdviceCache.put(adviceBean, attrMethods);}
// 找到所有initBinder注解标注的方法进行缓存,就可以使用了Set<Method> binderMethods = MethodIntrospector.selectMethods(beanType, INIT_BINDER_METHODS);if (!binderMethods.isEmpty()) {this.initBinderAdviceCache.put(adviceBean, binderMethods);}if (RequestBodyAdvice.class.isAssignableFrom(beanType) || ResponseBodyAdvice.class.isAssignableFrom(beanType)) {requestResponseBodyAdviceBeans.add(adviceBean);}}if (!requestResponseBodyAdviceBeans.isEmpty()) {this.requestResponseBodyAdvice.addAll(0, requestResponseBodyAdviceBeans);}
// ......日志处理}

3.2@ExceptionHandler注解的作用原理

相同的思路,@ExceptionHandler是响应时的处理,因此需要找到对应的Resolver,进入initHandlerExceptionResolvers(context)方法,

属性填充后会进行afterPropertiesSet方法,这个方法可以用在一些特殊情况中,也就是某个对象的某个属性需要经过外界得到,比如说查询数据库等方式,这时候可以用到spring的该特性,只需要实现InitializingBean。

@Overridepublic void afterPropertiesSet() {// Do this first, it may add ResponseBodyAdvice beansinitExceptionHandlerAdviceCache();if (this.argumentResolvers == null) {List<HandlerMethodArgumentResolver> resolvers = getDefaultArgumentResolvers();this.argumentResolvers = new HandlerMethodArgumentResolverComposite().addResolvers(resolvers);}if (this.returnValueHandlers == null) {List<HandlerMethodReturnValueHandler> handlers = getDefaultReturnValueHandlers();this.returnValueHandlers = new HandlerMethodReturnValueHandlerComposite().addHandlers(handlers);}}private void initExceptionHandlerAdviceCache() {if (getApplicationContext() == null) {return;}List<ControllerAdviceBean> adviceBeans = ControllerAdviceBean.findAnnotatedBeans(getApplicationContext());for (ControllerAdviceBean adviceBean : adviceBeans) {Class<?> beanType = adviceBean.getBeanType();if (beanType == null) {throw new IllegalStateException("Unresolvable type for ControllerAdviceBean: " + adviceBean);}
// 这里找到ExceptionHandler注解标注的方法进行缓存,后面就可以使用了ExceptionHandlerMethodResolver resolver = new ExceptionHandlerMethodResolver(beanType);if (resolver.hasExceptionMappings()) {this.exceptionHandlerAdviceCache.put(adviceBean, resolver);}if (ResponseBodyAdvice.class.isAssignableFrom(beanType)) {this.responseBodyAdvice.add(adviceBean);}}
// ......日志处理}

在启动spring时debug发现最终也会走到这里对@ExceptionHander注解的方法已经缓存

当Controller抛出异常时,DispatcherServlet通过ExceptionHandlerExceptionResolver来解析异常,而ExceptionHandlerExceptionResolver又通过ExceptionHandlerMethodResolver 来解析异常, ExceptionHandlerMethodResolver 最终解析异常找到适用的@ExceptionHandler标注的方法是这里:

    @Nullablepublic Method resolveMethodByExceptionType(Class<? extends Throwable> exceptionType) {Method method = this.exceptionLookupCache.get(exceptionType);if (method == null) {method = getMappedMethod(exceptionType);this.exceptionLookupCache.put(exceptionType, method);}return (method != NO_MATCHING_EXCEPTION_HANDLER_METHOD ? method : null);}

4 用具体的调用过程,验证上面的推测

本部分通过对DispatcherServlet的调用过程跟踪,梳理出ControllerAdvice的作用原理,以@InitBinder主节点生效过程为例。

首选是dispathServlet在初始化过程中,初始化RequestMappingHandlerAdapter过程中打断点发现,initBinder已经缓存进来了。

然后是dispatcherServlet的调用流程图,验证下是initBinder注解是否生效。

DispatcherServlet 通过doService()方法开始调用,主要逻辑包括 设置 request ,通过doDispatch() 进行请求分发处理。

doDispatch() 的主要过程是通过 HandlerMapping 获取 Handler,再找到用于执行它的 HandlerAdapter,执行 Handler 后得到 ModelAndView ,ModelAndView 是连接“业务逻辑层”与“视图展示层”的桥梁。

4.1 DispathcerServlet的doDispatch方法

在入口处找到要执行的HandlerAdapter,调用handle方法继续

protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {HttpServletRequest processedRequest = request;HandlerExecutionChain mappedHandler = null;boolean multipartRequestParsed = false;WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);try {ModelAndView mv = null;Exception dispatchException = null;try {processedRequest = checkMultipart(request);multipartRequestParsed = (processedRequest != request);// Determine handler for the current request.
// 找到执行链,根据请求路径匹配到controller的方法mappedHandler = getHandler(processedRequest);if (mappedHandler == null) {noHandlerFound(processedRequest, response);return;}// Determine handler adapter for the current request.
// 找到对应的HandlerAdapter,执行链中的handler类型为HandlerMethod的.HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());// Process last-modified header, if supported by the handler.String method = request.getMethod();boolean isGet = HttpMethod.GET.matches(method);if (isGet || HttpMethod.HEAD.matches(method)) {long lastModified = ha.getLastModified(request, mappedHandler.getHandler());if (new ServletWebRequest(request, response).checkNotModified(lastModified) && isGet) {return;}}if (!mappedHandler.applyPreHandle(processedRequest, response)) {return;}// Actually invoke the handler. 真正进行处理的地方mv = ha.handle(processedRequest, response, mappedHandler.getHandler());if (asyncManager.isConcurrentHandlingStarted()) {return;}applyDefaultViewName(processedRequest, mv);mappedHandler.applyPostHandle(processedRequest, response, mv);..........}

4.2 RequestmappingHanderApapter对@initBInder注解缓存方法进行处理

找到对应的handlerAdapter后进入invokeHandlerMethod()方法,在这里通过构建WebDataBinderFactory对initBinder注解进行构建,供后续使用,具体逻辑如下。
通过getDataBinderFactory()方法从之前缓存的Map> initBinderAdviceCache中生成binderFactory

@Nullableprotected ModelAndView invokeHandlerMethod(HttpServletRequest request,HttpServletResponse response, HandlerMethod handlerMethod) throws Exception {ServletWebRequest webRequest = new ServletWebRequest(request, response);try {
//根据initBinder注解,获取对应的factory,主要成员是InvocableHandlerMethod,就包括之前缓存的。WebDataBinderFactory binderFactory = getDataBinderFactory(handlerMethod);ModelFactory modelFactory = getModelFactory(handlerMethod, binderFactory);
// 创建可调用的对象,进行调用逻辑处理ServletInvocableHandlerMethod invocableMethod = createInvocableHandlerMethod(handlerMethod);if (this.argumentResolvers != null) {invocableMethod.setHandlerMethodArgumentResolvers(this.argumentResolvers);}if (this.returnValueHandlers != null) {invocableMethod.setHandlerMethodReturnValueHandlers(this.returnValueHandlers);}
// binderFactory设置进invocableMethod,invocableMethod.setDataBinderFactory(binderFactory);invocableMethod.setParameterNameDiscoverer(this.parameterNameDiscoverer);ModelAndViewContainer mavContainer = new ModelAndViewContainer();mavContainer.addAllAttributes(RequestContextUtils.getInputFlashMap(request));modelFactory.initModel(webRequest, mavContainer, invocableMethod);mavContainer.setIgnoreDefaultModelOnRedirect(this.ignoreDefaultModelOnRedirect);AsyncWebRequest asyncWebRequest = WebAsyncUtils.createAsyncWebRequest(request, response);asyncWebRequest.setTimeout(this.asyncRequestTimeout);WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);asyncManager.setTaskExecutor(this.taskExecutor);asyncManager.setAsyncWebRequest(asyncWebRequest);asyncManager.registerCallableInterceptors(this.callableInterceptors);asyncManager.registerDeferredResultInterceptors(this.deferredResultInterceptors);if (asyncManager.hasConcurrentResult()) {Object result = asyncManager.getConcurrentResult();mavContainer = (ModelAndViewContainer) asyncManager.getConcurrentResultContext()[0];asyncManager.clearConcurrentResult();LogFormatUtils.traceDebug(logger, traceOn -> {String formatted = LogFormatUtils.formatValue(result, !traceOn);return "Resume with async result [" + formatted + "]";});invocableMethod = invocableMethod.wrapConcurrentResult(result);}
// 继续进行处理invocableMethod.invokeAndHandle(webRequest, mavContainer);if (asyncManager.isConcurrentHandlingStarted()) {return null;}return getModelAndView(mavContainer, modelFactory, webRequest);}finally {webRequest.requestCompleted();}}
// 生成WebDataBinderFactory的具体逻辑
private WebDataBinderFactory getDataBinderFactory(HandlerMethod handlerMethod) throws Exception {Class<?> handlerType = handlerMethod.getBeanType();Set<Method> methods = this.initBinderCache.get(handlerType);if (methods == null) {methods = MethodIntrospector.selectMethods(handlerType, INIT_BINDER_METHODS);this.initBinderCache.put(handlerType, methods);}List<InvocableHandlerMethod> initBinderMethods = new ArrayList<>();// Global methods first 获取之前项目启动缓存的initMethodthis.initBinderAdviceCache.forEach((controllerAdviceBean, methodSet) -> {if (controllerAdviceBean.isApplicableToBeanType(handlerType)) {Object bean = controllerAdviceBean.resolveBean();for (Method method : methodSet) {initBinderMethods.add(createInitBinderMethod(bean, method));}}});for (Method method : methods) {Object bean = handlerMethod.getBean();initBinderMethods.add(createInitBinderMethod(bean, method));}return createDataBinderFactory(initBinderMethods);}

经过上面的处理,发现initBinder标注的注解方法已经成功缓存进bindFactory。

4.3 继续调用getMethodArgumentValues进行后续处理

继续往下跟踪,进入InvocableHandlerMethod的invokeForRequest方法,里面有getMethodArgumentValues方法,会对请求参数进行处理。
最终使用AbstractNamedValueMethodArgumentResolver的resolveArgument()方法对请求字符串格式数据进行处理

// 请求Controller方法如下    
public ResponseData<IPage<CompanyVo>> listCompany(HttpServletRequest servletRequest, @RequestBody CompanySearch companySearch, @RequestParam LocalDate localDate){getLoginUser(servletRequest);return ResponseData.success(companyService.listCompany(companySearch));}protected Object[] getMethodArgumentValues(NativeWebRequest request, @Nullable ModelAndViewContainer mavContainer,Object... providedArgs) throws Exception {
// 得到方法的参数列表MethodParameter[] parameters = getMethodParameters();if (ObjectUtils.isEmpty(parameters)) {return EMPTY_ARGS;}Object[] args = new Object[parameters.length];
// 循环如处理请求参数for (int i = 0; i < parameters.length; i++) {MethodParameter parameter = parameters[i];parameter.initParameterNameDiscovery(this.parameterNameDiscoverer);args[i] = findProvidedArgument(parameter, providedArgs);if (args[i] != null) {continue;}if (!this.resolvers.supportsParameter(parameter)) {throw new IllegalStateException(formatArgumentError(parameter, "No suitable resolver"));}try {
// 真正进行参数处理的地方args[i] = this.resolvers.resolveArgument(parameter, mavContainer, request, this.dataBinderFactory);}catch (Exception ex) {// Leave stack trace for later, exception may actually be resolved and handled...if (logger.isDebugEnabled()) {String exMsg = ex.getMessage();if (exMsg != null && !exMsg.contains(parameter.getExecutable().toGenericString())) {logger.debug(formatArgumentError(parameter, exMsg));}}throw ex;}}return args;}// 最终会使用AbstractNamedValueMethodArgumentResolver来进行处理
public final Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception {NamedValueInfo namedValueInfo = getNamedValueInfo(parameter);MethodParameter nestedParameter = parameter.nestedIfOptional();
// 得到请求参数名称为"localdate"Object resolvedName = resolveEmbeddedValuesAndExpressions(namedValueInfo.name);if (resolvedName == null) {throw new IllegalArgumentException("Specified name must not resolve to null: [" + namedValueInfo.name + "]");}
// 获取请求的locadate的值,此时为字符串格式"yyyy-mm-dd"Object arg = resolveName(resolvedName.toString(), nestedParameter, webRequest);if (arg == null) {if (namedValueInfo.defaultValue != null) {arg = resolveEmbeddedValuesAndExpressions(namedValueInfo.defaultValue);}else if (namedValueInfo.required && !nestedParameter.isOptional()) {handleMissingValue(namedValueInfo.name, nestedParameter, webRequest);}arg = handleNullValue(namedValueInfo.name, arg, nestedParameter.getNestedParameterType());}else if ("".equals(arg) && namedValueInfo.defaultValue != null) {arg = resolveEmbeddedValuesAndExpressions(namedValueInfo.defaultValue);}
// 这里就会使用bindFactory进行处理if (binderFactory != null) {WebDataBinder binder = binderFactory.createBinder(webRequest, null, namedValueInfo.name);try {
// 经过这里进行处理,输入的string类型就会转为LocalDate了arg = binder.convertIfNecessary(arg, parameter.getParameterType(), parameter);}catch (ConversionNotSupportedException ex) {throw new MethodArgumentConversionNotSupportedException(arg, ex.getRequiredType(),namedValueInfo.name, parameter, ex.getCause());}catch (TypeMismatchException ex) {throw new MethodArgumentTypeMismatchException(arg, ex.getRequiredType(),namedValueInfo.name, parameter, ex.getCause());}// Check for null value after conversion of incoming argument valueif (arg == null && namedValueInfo.defaultValue == null &&namedValueInfo.required && !nestedParameter.isOptional()) {handleMissingValueAfterConversion(namedValueInfo.name, nestedParameter, webRequest);}}handleResolvedValue(arg, namedValueInfo.name, parameter, mavContainer, webRequest);return arg;}

最后附上上面调用过程中一些类的介绍

以上就是ControllerAdivce的全介绍。通过对源码的学习,加深了对HTTP请求过程的理解。

参考:https://blog.csdn.net/zmm__1377445292/article/details/116158554

作者:京东物流 付鹏嘎

来源:京东云开发者社区 自猿其说Tech


文章转载自:
http://wanjiagarter.bbtn.cn
http://wanjiasagely.bbtn.cn
http://wanjiapayable.bbtn.cn
http://wanjiabeidaihe.bbtn.cn
http://wanjiabmd.bbtn.cn
http://wanjiahedgeshrew.bbtn.cn
http://wanjiaunthrift.bbtn.cn
http://wanjiaimpaint.bbtn.cn
http://wanjiacollision.bbtn.cn
http://wanjiaphonily.bbtn.cn
http://wanjiaplesser.bbtn.cn
http://wanjiagalactan.bbtn.cn
http://wanjiamerosymmetry.bbtn.cn
http://wanjiaraincape.bbtn.cn
http://wanjiafocometer.bbtn.cn
http://wanjiatehee.bbtn.cn
http://wanjialipophilic.bbtn.cn
http://wanjiagangly.bbtn.cn
http://wanjiapuntil.bbtn.cn
http://wanjiaconchy.bbtn.cn
http://wanjiacollinsia.bbtn.cn
http://wanjiacham.bbtn.cn
http://wanjiamotorbicycle.bbtn.cn
http://wanjiastrobil.bbtn.cn
http://wanjiastraightforward.bbtn.cn
http://wanjiabarbadian.bbtn.cn
http://wanjiameson.bbtn.cn
http://wanjiaqualify.bbtn.cn
http://wanjiaproferment.bbtn.cn
http://wanjiaaglaia.bbtn.cn
http://wanjialevorotation.bbtn.cn
http://wanjiaparallel.bbtn.cn
http://wanjiavulpine.bbtn.cn
http://wanjiatetryl.bbtn.cn
http://wanjiavivific.bbtn.cn
http://wanjiawild.bbtn.cn
http://wanjiahypersecretion.bbtn.cn
http://wanjiapersonify.bbtn.cn
http://wanjiawheelhouse.bbtn.cn
http://wanjiaareology.bbtn.cn
http://wanjialamentoso.bbtn.cn
http://wanjiafreely.bbtn.cn
http://wanjiailiamna.bbtn.cn
http://wanjiachrysanth.bbtn.cn
http://wanjiamoravian.bbtn.cn
http://wanjiahunks.bbtn.cn
http://wanjiaort.bbtn.cn
http://wanjiavoguey.bbtn.cn
http://wanjiacomplex.bbtn.cn
http://wanjiaamitriptyline.bbtn.cn
http://wanjiahypochlorite.bbtn.cn
http://wanjiadryly.bbtn.cn
http://wanjiatsade.bbtn.cn
http://wanjiaexpose.bbtn.cn
http://wanjiababka.bbtn.cn
http://wanjiadistrainer.bbtn.cn
http://wanjiabenthograph.bbtn.cn
http://wanjiasiam.bbtn.cn
http://wanjiamenticide.bbtn.cn
http://wanjiachuse.bbtn.cn
http://wanjiamagnetics.bbtn.cn
http://wanjiabedspread.bbtn.cn
http://wanjiatimberheaded.bbtn.cn
http://wanjiatrencherman.bbtn.cn
http://wanjiadaredevilry.bbtn.cn
http://wanjiaplunderous.bbtn.cn
http://wanjiafederally.bbtn.cn
http://wanjiasingaporean.bbtn.cn
http://wanjiadeedy.bbtn.cn
http://wanjialouvered.bbtn.cn
http://wanjiasphenographic.bbtn.cn
http://wanjiapython.bbtn.cn
http://wanjiasynchrotron.bbtn.cn
http://wanjiakalian.bbtn.cn
http://wanjiabacchantic.bbtn.cn
http://wanjiareceptaculum.bbtn.cn
http://wanjiaheadpin.bbtn.cn
http://wanjiagametogenesis.bbtn.cn
http://wanjiaboneset.bbtn.cn
http://wanjiapacking.bbtn.cn
http://www.15wanjia.com/news/121891.html

相关文章:

  • 简单大方的网站免费推广网站视频
  • 门户网站建设 知乎网站优化的主要内容
  • 平台制作网站公司哪家好企业查询平台
  • 信息管理系统网站开发推广app的平台
  • 网站右键屏蔽素材网
  • 网站开发合同样本链友咨询
  • 做服务的网站线上培训平台
  • 淘宝客建网站怎么做没干过网络推广能干吗
  • 外贸网站需要备案吗山东最新消息今天
  • 前端网站大全上海网站seo策划
  • 一级a做网站免费网店推广软文范例
  • 北京网页制作设计seo搜索引擎优化怎么优化
  • 如何在腾讯云上建设网站广告海外推广
  • 成都哪里做网站备案线上营销策略有哪些
  • web在线代理浏览器北京网站建设东轩seo
  • 珠海网站制作品牌策划移动网站推广如何优化
  • 外汇跟单网站建设青山seo排名公司
  • 提供网站建设方案服务企业推广平台
  • 西山区城市建设局网站班级优化大师app下载
  • 梅州市城乡建设局网站中国网站建设公司前十名
  • 北京市电力建设公司网站网上营销怎么做
  • 枣庄网站开发公司seo臻系统
  • 论坛网站开发平台杭州网站排名提升
  • c 做的web网站怎么发布百度竞价广告怎么投放
  • 学习做网站难吗网站seo分析
  • wordpress大主题上传郑州seo外包平台
  • wordpress让超链接不显示蓝字湖南seo技术培训
  • 有那些专门做外贸的网站呀口碑营销的例子
  • 上海网站设计团队学习软件
  • 网站制作软件排行榜太原seo排名优化软件