xxl-job如何滥用netty导致的问题及解决方案

netty作为一种高性能的网络编程框架,在很多开源项目中大放异彩,十分亮眼,但是在有些项目中却被滥用,导致使用者使用起来非常的难受。

笔者使用的是2.3.0版本的xxl-job,也是当前的最新版本;下面所有的代码修改全部基于2.3.0版本的xxl-job源代码

https://github.com/xuxueli/xxl-job/tree/2.3.0

其中,xxl-job-admin对应着项目:https://github.com/xuxueli/xxl-job/tree/2.3.0/xxl-job-admin

spring-boot项目对应着示例项目:https://github.com/xuxueli/xxl-job/tree/master/xxl-job-executor-samples/xxl-job-executor-sample-springboot

一、xxl-job存在的多端口问题

关于xxl-job如何使用的问题,可以参考我的另外一篇文章:分布式任务调度系统:xxl-job

现在java开发基本上已经离不开spring boot了吧,我在spring boot中集成了xxl-job-core组件并且已经能够正常使用,但是一旦部署到测试环境就不行了,这是因为测试环境使用了docker,spring boot集成xxl-job-core组件之后会额外开启9999端口号给xxl-job-admin调用使用,如果docker不开启宿主机到docker的端口映射,xxl-job-admin自然就会调用失败。这导致了以下问题:

  • 每个spring boot程序都要开两个端口号,意味着同时运行着两个服务进行端口监听,浪费计算和内存资源
  • 如果使用docker部署,需要再额外做宿主机和容器的9999端口号的映射,否则外部的xxl-job-admin将无法访问。

那如果两个不同的服务都集成了xxl-job,但是部署在同一台机器上,又会发生什么呢?答案是如果不指定特定端口号,两个服务肯定都要使用9999端口号,势必会端口冲突,但是xxl-job已经想到了9999端口号被占用的情况,如果9999端口号被占用,则会端口号加一再重试。

xxl-job-core组件额外开启9999端口号到底合不合理?

举个例子:spring boot程序集成swagger-ui是很常见的操作吧,也没见swagger-ui再额外开启端口号啊,我认为是不合理的。但是,我认为作者这样做也有他的考虑---并非所有程序都是spring-boot的程序,也有使用其它框架的程序,使用独立的netty server作为客户端能够保证在使用java的任意xxl-job客户端都能稳定的向xxl-job-admin提供服务。然而java开发者们绝大多数情况下都是使用spirng-boot构建程序,在这种情况下,作者偷懒没有构建专门在spirng boot框架下使用的xxl-job-core,而是想了个类似万金油的蠢招解决问题,让所有在spring-boot框架下的开发者都一起难受,实在是令人费解。

二、源码追踪

一切的起点要从spring-boot程序集成xxl-job-core说起,集成方式很简单,只需要成功创建一个XxlJobSpringExecutor Bean对象即可。

@Bean
    public XxlJobSpringExecutor xxlJobExecutor() {
        logger.info(">>>>>>>>>>> xxl-job config init.");
        XxlJobSpringExecutor xxlJobSpringExecutor = new XxlJobSpringExecutor();
        xxlJobSpringExecutor.setAdminAddresses(adminAddresses);
        xxlJobSpringExecutor.setAppname(appname);
        xxlJobSpringExecutor.setAddress(address);
        xxlJobSpringExecutor.setIp(ip);
        xxlJobSpringExecutor.setPort(port);
        xxlJobSpringExecutor.setAccessToken(accessToken);
        xxlJobSpringExecutor.setLogPath(logPath);
        xxlJobSpringExecutor.setLogRetentionDays(logRetentionDays);

        return xxlJobSpringExecutor;
    }

XxlJobSpringExecutor对象创建完成之后会做一些xxl-job初始化的操作,包含连接xxl-job-admin以及启动netty server。

展开XxlJobSpringExecutor源码,可以看到它实现了SmartInitializingSingleton接口,这就意味着Bean对象创建完成之后会回调afterSingletonsInstantiated接口

// start
    @Override
    public void afterSingletonsInstantiated() {

        // init JobHandler Repository
        /*initJobHandlerRepository(applicationContext);*/

        // init JobHandler Repository (for method)
        initJobHandlerMethodRepository(applicationContext);

        // refresh GlueFactory
        GlueFactory.refreshInstance(1);

        // super start
        try {
            super.start();
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

super.start();这行代码中,会调用父类XxlJobExecutor的start方法做初始化

public void start() throws Exception {

        // init logpath
        XxlJobFileAppender.initLogPath(logPath);

        // init invoker, admin-client
        initAdminBizList(adminAddresses, accessToken);

        // init JobLogFileCleanThread
        JobLogFileCleanThread.getInstance().start(logRetentionDays);

        // init TriggerCallbackThread
        TriggerCallbackThread.getInstance().start();

        // init executor-server
        initEmbedServer(address, ip, port, appname, accessToken);
    }

initEmbedServer(address, ip, port, appname, accessToken);这行代码做开启netty-server的操作

 private void initEmbedServer(String address, String ip, int port, String appname, String accessToken) throws Exception {

        // fill ip port
        port = port>0?port: NetUtil.findAvailablePort(9999);
        ip = (ip!=null&&ip.trim().length()>0)?ip: IpUtil.getIp();

        // generate address
        if (address==null || address.trim().length()==0) {
            String ip_port_address = IpUtil.getIpPort(ip, port);   // registry-address:default use address to registry , otherwise use ip:port if address is null
            address = "http://{ip_port}/".replace("{ip_port}", ip_port_address);
        }

        // accessToken
        if (accessToken==null || accessToken.trim().length()==0) {
            logger.warn(">>>>>>>>>>> xxl-job accessToken is empty. To ensure system security, please set the accessToken.");
        }

        // start
        embedServer = new EmbedServer();
        embedServer.start(address, port, appname, accessToken);
    }

可以看到这里会创建EmbedServer对象,并且使用start方法开启netty-server,在这里就能看到熟悉的一大坨了

除了开启读写空闲检测之外,就只做了一件事:开启http服务,也就是说,xxl-job-admin是通过http请求调用客户端的接口触发客户端的任务调度的。最终处理方法在EmbedHttpServerHandler类中,顺着EmbedHttpServerHandler类的方法找,可以最终找到处理的方法com.xxl.job.core.server.EmbedServer.EmbedHttpServerHandler#process

private Object process(HttpMethod httpMethod, String uri, String requestData, String accessTokenReq) {
    // valid
    if (HttpMethod.POST != httpMethod) {
        return new ReturnT<String>(ReturnT.FAIL_CODE, "invalid request, HttpMethod not support.");
    }
    if (uri==null || uri.trim().length()==0) {
        return new ReturnT<String>(ReturnT.FAIL_CODE, "invalid request, uri-mapping empty.");
    }
    if (accessToken!=null
        && accessToken.trim().length()>0
        && !accessToken.equals(accessTokenReq)) {
        return new ReturnT<String>(ReturnT.FAIL_CODE, "The access token is wrong.");
    }

    // services mapping
    try {
        if ("/beat".equals(uri)) {
            return executorBiz.beat();
        } else if ("/idleBeat".equals(uri)) {
            IdleBeatParam idleBeatParam = GsonTool.fromJson(requestData, IdleBeatParam.class);
            return executorBiz.idleBeat(idleBeatParam);
        } else if ("/run".equals(uri)) {
            TriggerParam triggerParam = GsonTool.fromJson(requestData, TriggerParam.class);
            return executorBiz.run(triggerParam);
        } else if ("/kill".equals(uri)) {
            KillParam killParam = GsonTool.fromJson(requestData, KillParam.class);
            return executorBiz.kill(killParam);
        } else if ("/log".equals(uri)) {
            LogParam logParam = GsonTool.fromJson(requestData, LogParam.class);
            return executorBiz.log(logParam);
        } else {
            return new ReturnT<String>(ReturnT.FAIL_CODE, "invalid request, uri-mapping("+ uri +") not found.");
        }
    } catch (Exception e) {
        logger.error(e.getMessage(), e);
        return new ReturnT<String>(ReturnT.FAIL_CODE, "request error:" + ThrowableUtil.toString(e));
    }
}

从这段代码的逻辑可以看到

  • 只接受POST请求
  • 如果有token,则会校验token
  • 只提供/beat、/idelBeat、/run、/kill、/log 五个接口,所有请求的处理都会委托给executorBiz处理。

最后,netty将executorBiz处理结果写回xxl-job-admin,然后请求就结束了。这里netty扮演的角色非常简单,我认为可以使用spring-mvc非常容易的替换掉它的功能。

三、使用spring-mvc替换netty的功能

1.新增spring-mvc代码

这里要修改xxl-job-core的源代码,首先,加入spring-mvc的依赖

		<!-- spring-web -->
		<dependency>
			<groupId>org.springframework</groupId>
			<artifactId>spring-web</artifactId>
			<version>${spring.version}</version>
			<scope>provided</scope>
		</dependency>

然后新增Controller文件

package com.xxl.job.core.controller;

import com.xxl.job.core.biz.impl.ExecutorBizImpl;
import com.xxl.job.core.biz.model.*;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

/**
 * @author kdyzm
 * @date 2021/5/7
 */
@RestController
public class XxlJobController {

    @PostMapping("/beat")
    public ReturnT<String> beat() {
        return new ExecutorBizImpl().beat();
    }

    @PostMapping("/idleBeat")
    public ReturnT<String> idleBeat(@RequestBody IdleBeatParam param) {
        return new ExecutorBizImpl().idleBeat(param);
    }

    @PostMapping("/run")
    public ReturnT<String> run(@RequestBody TriggerParam param) {
        return new ExecutorBizImpl().run(param);
    }

    @PostMapping("/kill")
    public ReturnT<String> kill(@RequestBody KillParam param) {
        return new ExecutorBizImpl().kill(param);
    }

    @PostMapping("/log")
    public ReturnT<LogResult> log(@RequestBody LogParam param) {
        return new ExecutorBizImpl().log(param);
    }
}

2.删除老代码&移除netty依赖

之后,就要删除老的代码了,修改com.xxl.job.core.server.EmbedServer#start方法,清空所有代码,新增

// start registry
startRegistry(appname, address);

然后删除EmbedServer类中的以下两个变量及相关的引用

 private ExecutorBiz executorBiz;
    private Thread thread;

之后删除netty的依赖

		<!-- ********************** embed server: netty + gson ********************** -->
		<dependency>
			<groupId>io.netty</groupId>
			<artifactId>netty-all</artifactId>
			<version>${netty-all.version}</version>
		</dependency>

将报错的代码全部删除,之后就可以编译成功了,当然这还不行。

3.修改注册到xxl-job-admin的端口号

注册的ip地址可以不用改,但是端口号要取spring-boot程序的端口号。

因为要复用springk-boot容器的端口号,所以这里注册的端口号要和它保持一致,修改com.xxl.job.core.executor.XxlJobExecutor#initEmbedServer方法,注释掉

port = port > 0 ? port : NetUtil.findAvailablePort(9999);

然后修改spring-boot的配置文件,xxl-job的端口号配置改成server.port

server.port=8081
xxl.job.executor.port=${server.port}

在创建XxlJobSpringExecutor Bean对象的时候将改值传递给它。

@Bean
public XxlJobSpringExecutor xxlJobExecutor() {
    logger.info(">>>>>>>>>>> xxl-job config init.");
    XxlJobSpringExecutor xxlJobSpringExecutor = new XxlJobSpringExecutor();
    xxlJobSpringExecutor.setAdminAddresses(adminAddresses);
    xxlJobSpringExecutor.setAppname(appname);
    xxlJobSpringExecutor.setAddress(address);
    xxlJobSpringExecutor.setIp(ip);
    xxlJobSpringExecutor.setPort(port);
    xxlJobSpringExecutor.setAccessToken(accessToken);
    xxlJobSpringExecutor.setLogPath(logPath);
    xxlJobSpringExecutor.setLogRetentionDays(logRetentionDays);
    return xxlJobSpringExecutor;
}

4.将xxl-job-core改造成spring-boot-starter

上面改造完了之后已经将逻辑变更为使用spring-mvc,但是spring-boot程序还没有办法扫描到xxl-job-core中的controller,可以手动扫描包,这里推荐使用spring-boot-starter,这样只需要将xxl-job-core加入classpath,就可以自动生效。

在 com.xxl.job.core.config包下新建Config类

package com.xxl.job.core.config;

import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;

/**
 * @author kdyzm
 * @date 2021/5/7
 */
@Configuration
@ComponentScan(basePackages = {"com.xxl.job.core.controller"})
public class Config {
}

src/main/resources/META-INF文件夹下新建spring.factories文件,文件内容如下

org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.xxl.job.core.config.Config

5.增加特殊前缀匹配

上面修改之后将使用spring mvc接口替代原netty功能提供的http接口,但是暴露出的接口是/run、/beat、/kill这种有可能和宿主服务路径冲突的接口,为了防止出现路径冲突,做出以下修改

修改com.xxl.job.core.controller.XxlJobController类,添加@RequestMapping("/xxl-job")

@RestController
@RequestMapping("/xxl-job")
public class XxlJobController {
	...
}

修改com.xxl.job.core.biz.client.ExecutorBizClient类,为每个请求添加/xxl-job前缀

package com.xxl.job.core.biz.client;

import com.xxl.job.core.biz.ExecutorBiz;
import com.xxl.job.core.biz.model.*;
import com.xxl.job.core.util.XxlJobRemotingUtil;

/**
 * admin api test
 *
 * @author xuxueli 2017-07-28 22:14:52
 */
public class ExecutorBizClient implements ExecutorBiz {

    public ExecutorBizClient() {
    }
    public ExecutorBizClient(String addressUrl, String accessToken) {
        this.addressUrl = addressUrl;
        this.accessToken = accessToken;

        // valid
        if (!this.addressUrl.endsWith("/")) {
            this.addressUrl = this.addressUrl + "/";
        }
    }

    private String addressUrl ;
    private String accessToken;
    private int timeout = 3;

    @Override
    public ReturnT<String> beat() {
        return XxlJobRemotingUtil.postBody(addressUrl+"xxl-job/beat", accessToken, timeout, "", String.class);
    }

    @Override
    public ReturnT<String> idleBeat(IdleBeatParam idleBeatParam){
        return XxlJobRemotingUtil.postBody(addressUrl+"xxl-job/idleBeat", accessToken, timeout, idleBeatParam, String.class);
    }

    @Override
    public ReturnT<String> run(TriggerParam triggerParam) {
        return XxlJobRemotingUtil.postBody(addressUrl + "xxl-job/run", accessToken, timeout, triggerParam, String.class);
    }

    @Override
    public ReturnT<String> kill(KillParam killParam) {
        return XxlJobRemotingUtil.postBody(addressUrl + "xxl-job/kill", accessToken, timeout, killParam, String.class);
    }

    @Override
    public ReturnT<LogResult> log(LogParam logParam) {
        return XxlJobRemotingUtil.postBody(addressUrl + "xxl-job/log", accessToken, timeout, logParam, LogResult.class);
    }

}

这样,就全部修改完了。

四、测试

重启xxl-job-executor-sample-springboot项目,查看注册到xxl-job-admin上的信息

可以看到端口号已经不是默认的9999,而是和spring-boot程序保持一致的端口号,然后执行默认的job

可以看到已经执行成功,在查看日志详情

日志也一切正常,表示一切都改造成功了。

完整的代码修改:https://github.com/kdyzm/xxl-job/commit/449ee5c7bbb659356af25b164c251f960b9a6891

五、实际使用

由于原作者基本上不理睬人,我克隆了项目2.3.0版本并且新增了2.4.1版本:https://github.com/kdyzm/xxl-job/releases/tag/2.4.1

有需要的可以下载源代码自己打包xxl-job-core项目上传私服后就可以使用了

以上就是xxl-job如何滥用netty导致的问题及解决方案的详细内容,更多关于xxl-job滥用netty的资料请关注我们其它相关文章!

(0)

相关推荐

  • SpringBoot+WebSocket+Netty实现消息推送的示例代码

    上一篇文章讲了Netty的理论基础,这一篇讲一下Netty在项目中的应用场景之一:消息推送功能,可以满足给所有用户推送,也可以满足给指定某一个用户推送消息,创建的是SpringBoot项目,后台服务端使用Netty技术,前端页面使用WebSocket技术. 大概实现思路: 前端使用webSocket与服务端创建连接的时候,将用户ID传给服务端 服务端将用户ID与channel关联起来存储,同时将channel放入到channel组中 如果需要给所有用户发送消息,直接执行channel组的writ

  • Java实战之用springboot+netty实现简单的一对一聊天

    一.引入pom <?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4

  • Spring Boot实战之netty-socketio实现简单聊天室(给指定用户推送消息)

    网上好多例子都是群发的,本文实现一对一的发送,给指定客户端进行消息推送 1.本文使用到netty-socketio开源库,以及MySQL,所以首先在pom.xml中添加相应的依赖库 <dependency> <groupId>com.corundumstudio.socketio</groupId> <artifactId>netty-socketio</artifactId> <version>1.7.11</version&

  • spring+netty服务器搭建的方法

    游戏一般是长连接,自定义协议,不用http协议,BIO,NIO,AIO这些我就不说了,自己查资料 我现在用spring+netty搭起简单的游戏服 思路:1自定义协议和协议包:2spring+netty整合:3半包粘包处理,心跳机制等:4请求分发(目前自己搞的都是单例模式) 下个是测试用的,结构如下 首先自定义包头 Header.java package com.test.netty.message; /** * Header.java * 自定义协议包头 * @author janehuang

  • Netty与Spring Boot的整合的实现

    ​ 最近有朋友向我询问一些Netty与SpringBoot整合的相关问题,这里,我就总结了一下基本整合流程,也就是说,这篇文章 ,默认大家是对netty与Spring,SpringMVC的整合是没有什么问题的.现在,就进入正题吧. Server端: 总的来说,服务端还是比较简单的,自己一共写了三个核心类.分别是 NettyServerListener:服务启动监听器 ServerChannelHandlerAdapter:通道适配器,主要用于多线程共享 RequestDispatcher:请求分

  • 在SpringBoot中整合使用Netty框架的详细教程

    Netty是一个非常优秀的Socket框架.如果需要在SpringBoot开发的app中,提供Socket服务,那么Netty是不错的选择. Netty与SpringBoot的整合,我想无非就是要整合几个地方 让netty跟springboot生命周期保持一致,同生共死 让netty能用上ioc中的Bean 让netty能读取到全局的配置 整合Netty,提供WebSocket服务 这里演示一个案例,在SpringBoot中使用Netty提供一个Websocket服务. servlet容器本身提

  • Netty学习教程之基础使用篇

    什么Netty? Netty是由JBOSS提供的一个java开源框架.Netty提供异步的.事件驱动的网络应用程序框架和工具,用以快速开发高性能.高可靠性的网络服务器和客户端程序. 也就是说,Netty 是一个基于NIO的客户.服务器端编程框架,使用Netty 可以确保你快速和简单的开发出一个网络应用,例如实现了某种协议的客户,服务端应用.Netty相当简化和流线化了网络应用的编程开发过程,例如,TCP和UDP的socket服务开发. 我们下面编写四个类 1.用于接收数据的服务器端Socket

  • xxl-job如何滥用netty导致的问题及解决方案

    netty作为一种高性能的网络编程框架,在很多开源项目中大放异彩,十分亮眼,但是在有些项目中却被滥用,导致使用者使用起来非常的难受. 笔者使用的是2.3.0版本的xxl-job,也是当前的最新版本:下面所有的代码修改全部基于2.3.0版本的xxl-job源代码 https://github.com/xuxueli/xxl-job/tree/2.3.0 其中,xxl-job-admin对应着项目:https://github.com/xuxueli/xxl-job/tree/2.3.0/xxl-j

  • 滥用@PathVariable导致bug原因分析解决

    目录 前言 复现 3个匹配步骤 1,根据Path精准匹配 2,如果精准匹配没有成功,就开始模糊匹配 3,如果模糊匹配还匹配不上,就返回null 最后 前言 最近测试同学反馈,上周上线的一个功能会偶然性的报404,按理说这个功能在测试环境已经测试通过,也在线上运行了好几天,怎么会突然报错呢. 一开始以为是前端同学请求的接口有误,但是测试又说只是偶然性的404,几率也不高,于是打开日志找到对应的接口,一眼看到了接口上定义的@PathVariable,再一看参数,基本就确定是开发同学为了偷懒又误用@P

  • Asp.Net程序目录下文件夹或文件操作导致Session失效的解决方案

    1.配置web.config <system.web> <sessionState mode="StateServer" stateConnectionString="tcpip=127.0.0.1:42424" sqlConnectionString="data source=127.0.0.1;Trusted_Connection=yes" cookieless="false" timeout="

  • 探究Android中ListView复用导致布局错乱的解决方案

    首先来说一下具体的需求是什么样的: 需求如图所示,这里面有ABCD四个选项的题目,当点击A选项,如果A是正确的答案,则变成对勾的图案,如果是错误答案,则变成错误的图案,这里当时在写的时候觉得很简单,只要是在点击的时候判断我点击的选项与正确答案是否一样,是一样就将图片换成正确的样式,如果不一样就换成错误的样式,于是我便写了下面的代码(只贴出了核心Adapter中的代码) package com.fizzer.anbangproject_dahuo_test.Adapter; import andr

  • Java @Async注解导致spring启动失败解决方案详解

    前言 在这篇文章里,最后总结处,我说了会讲讲循环依赖中,其中一个类添加@Async有可能会导致注入失败而抛异常的情况,今天就分析一下. 一.异常表现,抛出内容 1.1循环依赖的两个class 1.CycleService1 @Service public class CycleService1 { @Autowired private CycleService2 cycleService2; @WangAnno @Async public void doThings() { System.out

  • fastjson全局日期序列化设置导致JSONField失效问题解决方案

    目录 问题描述 使用版本 全局设置代码 属性设置代码 返回结果 解决方案 统一扫描 统一修改 问题描述 fastjson通过代码指定全局序列化返回时间格式,导致使用JSONField注解标注属性的特殊日期返回格式失效 使用版本 应用名称 版本 springboot 2.0.0.RELEASE fastjson 1.2.83 全局设置代码 public class WebConfig implements WebMvcConfigurer { @Override public void confi

  • 关于Apache默认编码错误 导致网站乱码的解决方案

    最近经常有同学在使用LAMP/WAMP时,遇到这样的编码错误问题: A网站程序编码UTF-8编码安装成功,运行成功. B网站程序编gb2312也要安装在同一服务器上. 这样就出现问题了,Apache默认编码UTF-8在解析A网站的时候没有任何问题,当运行B网站时出现的"蝌蚪文"乱码问题. 单纯的修改Apache默认编码为gb2312这样就导致A网站出现"蝌蚪文". 问题分析: 如果你在网上搜索 "apache配置",搜到的页面大多都会建议你在ht

  • Netty粘包拆包问题解决方案

    TCP黏包拆包 TCP是一个流协议,就是没有界限的一长串二进制数据.TCP作为传输层协议并不不了解上层业务数据的具体含义,它会根据TCP缓冲区的实际情况进行数据包的划分,所以在业务上认为是一个完整的包,可能会被TCP拆分成多个包进行发送,也有可能把多个小的包封装成一个大的数据包发送,这就是所谓的TCP粘包和拆包问题. 怎么解决? • 消息定长度,传输的数据大小固定长度,例如每段的长度固定为100字节,如果不够空位补空格 • 在数据包尾部添加特殊分隔符,比如下划线,中划线等 • 将消息分为消息头和

  • Spring Security和自定义filter的冲突导致多执行的解决方案

    问题描述: 使用Spring Security时,在WebSecurityConfig中需要通过@bean注解注入Security的filter对象,但是不知是不是因为spring boot框架的原因还是什么未知原因,导致在这里注入,就会多注入一次这个对象,导致filter链走完之后,又会回到这个filter中再执行一次. @Bean public JwtAuthenticationTokenFilter authenticationTokenFilterBean() throws Except

  • IE6中使用position导致页面变形的解决方案(js代码)

    如图所示: 解决方案: 1.缩放窗体时先得到内容左边的空白宽度. $("#nav").offset().left; 得到内容区左边的空白宽度. 2.得到整个窗体的宽度(注意:桌面分辨率为基准,少了加上来). 3.用桌面分辨率的宽度-页面内容区的宽度/2,就可以得到一边多余的宽度. 4.如果得到的值跟$("#nav").offset().left;得到值不同,则可以调到两值相同. 复制代码 代码如下: var ietest=function() { if ($.bro

随机推荐