搜索
首页Javajava教程 java实现定时任务 Schedule

1.java定时任务可以借助 java.util.Timer 来实现

import java.util.Calendar;  
import java.util.Date;  
import java.util.Timer;  
import java.util.TimerTask;  
  
public class Test {  
    public static void main(String[] args) {  
        //timer1();  
        timer2();  
        //timer3();  
        //timer4();  
    }  
  
    // 第一种方法:设定指定任务task在指定时间time执行 schedule(TimerTask task, Date time)  
    public static void timer1() {  
        Timer timer = new Timer();  
        timer.schedule(new TimerTask() {  
            public void run() {  
                System.out.println("-------设定要指定任务--------");  
            }  
        }, 2000);// 设定指定的时间time,此处为2000毫秒  
    }  
  
    // 第二种方法:设定指定任务task在指定延迟delay后进行固定延迟peroid的执行  
    // schedule(TimerTask task, long delay, long period)  
    public static void timer2() {  
        Timer timer = new Timer();  
        timer.schedule(new TimerTask() {  
            public void run() {  
                System.out.println("-------设定要指定任务--------");  
            }  
        }, 1000, 1000);  
    }  
  
    // 第三种方法:设定指定任务task在指定延迟delay后进行固定频率peroid的执行。  
    // scheduleAtFixedRate(TimerTask task, long delay, long period)  
    public static void timer3() {  
        Timer timer = new Timer();  
        timer.scheduleAtFixedRate(new TimerTask() {  
            public void run() {  
                System.out.println("-------设定要指定任务--------");  
            }  
        }, 1000, 2000);  
    }  
     
    // 第四种方法:安排指定的任务task在指定的时间firstTime开始进行重复的固定速率period执行.  
    // Timer.scheduleAtFixedRate(TimerTask task,Date firstTime,long period)  
    public static void timer4() {  
        Calendar calendar = Calendar.getInstance();  
        calendar.set(Calendar.HOUR_OF_DAY, 12); // 控制时  
        calendar.set(Calendar.MINUTE, 0);       // 控制分  
        calendar.set(Calendar.SECOND, 0);       // 控制秒  
  
        Date time = calendar.getTime();         // 得出执行任务的时间,此处为今天的12:00:00  
  
        Timer timer = new Timer();  
        timer.scheduleAtFixedRate(new TimerTask() {  
            public void run() {  
                System.out.println("-------设定要指定任务--------");  
            }  
        }, time, 1000 * 60 * 60 * 24);// 这里设定将延时每天固定执行  
    }  
}

2. Java定时任务可以用线程的等待来实现

/**  
 * 普通thread  
 * 这是最常见的,创建一个thread,然后让它在while循环里一直运行着,  
 * 通过sleep方法来达到定时任务的效果。这样可以快速简单的实现,代码如下:  
 * @author GT  
 *  
 */    
public class Task1 {    
    public static void main(String[] args) {    
        // run in a second    
        final long timeInterval = 1000;    
        Runnable runnable = new Runnable() {    
            public void run() {    
                while (true) {    
                    // ------- code for task to run    
                    System.out.println("Hello !!");    
                    // ------- ends here    
                    try {    
                        Thread.sleep(timeInterval);    
                    } catch (InterruptedException e) {    
                        e.printStackTrace();    
                    }    
                }    
            }    
        };    
        Thread thread = new Thread(runnable);    
        thread.start();    
    }    
}

3.Java可以用java.util.concurrent.ScheduledExecutorService 来实现定时任务

import java.util.concurrent.Executors;    
import java.util.concurrent.ScheduledExecutorService;    
import java.util.concurrent.TimeUnit;    
    
/**  
 *   
 *   
 * ScheduledExecutorService是从Java SE5的java.util.concurrent里,做为并发工具类被引进的,这是最理想的定时任务实现方式。   
 * 相比于上两个方法,它有以下好处:  
 * 1>相比于Timer的单线程,它是通过线程池的方式来执行任务的   
 * 2>可以很灵活的去设定第一次执行任务delay时间  
 * 3>提供了良好的约定,以便设定执行的时间间隔  
 *   
 * 下面是实现代码,我们通过ScheduledExecutorService#scheduleAtFixedRate展示这个例子,通过代码里参数的控制,首次执行加了delay时间。  
 *   
 *   
 * @author GT  
 *   
 */    
public class Task3 {    
    public static void main(String[] args) {    
        Runnable runnable = new Runnable() {    
            public void run() {    
                // task to run goes here    
                System.out.println("Hello !!");    
            }    
        };    
        ScheduledExecutorService service = Executors    
                .newSingleThreadScheduledExecutor();    
        // 第二个参数为首次执行的延时时间,第三个参数为定时执行的间隔时间    
        service.scheduleAtFixedRate(runnable, 10, 1, TimeUnit.SECONDS);    
    }    
}

4. 定时任务之-Quartz使用篇

Quartz是OpenSymphony开源组织在Job scheduling领域又一个开源项目,它可以与J2EE与J2SE应用程序相结合也可以单独使用。Quartz可以用来创建简单或为运行十个,百个,甚至是好几万个Jobs这样复杂的日程序表。Jobs可以做成标准的Java组件或 EJBs。



CronTrigger配置格式:
格式: [秒] [分] [小时] [日] [月] [周] [年]

序号    说明    是否必填    允许填写的值    允许的通配符    

1    秒    是    0-59    , - * /    

2    分    是    0-59    , - * /    

3    小时    是    0-23    , - * /    

4    日    是    1-31    , - * ? / L W    

5    月    是    1-12 or JAN-DEC    , - * /    

6    周    是    1-7 or SUN-SAT    , - * ? / L #    

7    年    否    empty 或 1970-2099    , - * /    

通配符说明:
* 表示所有值. 例如:在分的字段上设置 "*",表示每一分钟都会触发。
? 表示不指定值。使用的场景为不需要关心当前设置这个字段的值。例如:要在每月的10号触发一个操作,但不关心是周几,所以需要周位置的那个字段设置为"?" 具体设置为 0 0 0 10* ?
- 表示区间。例如 在小时上设置 "10-12",表示 10,11,12点都会触发。
, 表示指定多个值,例如在周字段上设置 "MON,WED,FRI" 表示周一,周三和周五触发
/ 用于递增触发。如在秒上面设置"5/15" 表示从5秒开始,每增15秒触发(5,20,35,50)。在月字段上设置'1/3'所示每月1号开始,每隔三天触发一次。
L 表示最后的意思。在日字段设置上,表示当月的最后一天(依据当前月份,如果是二月还会依据是否是润年[leap]), 在周字段上表示星期六,相当于"7"或"SAT"。如果在"L"前加上数字,则表示该数据的最后一个。例如在周字段上设置"6L"这样的格式,则表示“本月最后一个星期五"
W 表示离指定日期的最近那个工作日(周一至周五). 例如在日字段上设置"15W",表示离每月15号最近的那个工作日触发。如果15号正好是周六,则找最近的周五(14号)触发, 如果15号是周未,则找最近的下周一(16号)触发.如果15号正好在工作日(周一至周五),则就在该天触发。如果指定格式为 "1W",它则表示每月1号往后最近的工作日触发。如果1号正是周六,则将在3号下周一触发。(注,"W"前只能设置具体的数字,不允许区间"-").

小提示    

'L'和 'W'可以一组合使用。如果在日字段上设置"LW",则表示在本月的最后一个工作日触发(一般指发工资 ) 

   

# 序号(表示每月的第几个周几),例如在周字段上设置"6#3"表示在每月的第三个周六.注意如果指定"#5",正好第五周没有周六,则不会触发该配置(用在母亲节和父亲节再合适不过了)

小提示    周字段的设置,若使用英文字母是不区分大小写的 MON 与mon相同.    


常用示例:

0 0 12 * * ?    每天12点触发    

0 15 10 ? * *    每天10点15分触发    

0 15 10 * * ?    每天10点15分触发    

0 15 10 * * ? *    每天10点15分触发    

0 15 10 * * ? 2005    2005年每天10点15分触发    

0 * 14 * * ?    每天下午的 2点到2点59分每分触发    

0 0/5 14 * * ?    每天下午的 2点到2点59分(整点开始,每隔5分触发)    

0 0/5 14,18 * * ?    每天下午的 2点到2点59分(整点开始,每隔5分触发)每天下午的 18点到18点59分(整点开始,每隔5分触发)    

0 0-5 14 * * ?    每天下午的 2点到2点05分每分触发    

0 10,44 14 ? 3 WED    3月分每周三下午的 2点10分和2点44分触发    

0 15 10 ? * MON-FRI    从周一到周五每天上午的10点15分触发    

0 15 10 15 * ?    每月15号上午10点15分触发    

0 15 10 L * ?    每月最后一天的10点15分触发    

0 15 10 ? * 6L    每月最后一周的星期五的10点15分触发    

0 15 10 ? * 6L 2002-2005    从2002年到2005年每月最后一周的星期五的10点15分触发    

0 15 10 ? * 6#3    每月的第三周的星期五开始触发    

0 0 12 1/5 * ?    每月的第一个中午开始每隔5天触发一次    

0 11 11 11 11 ?    每年的11月11号 11点11分触发(光棍节)    

经过封装的管理类:

import java.text.ParseException;    
    
import org.quartz.CronTrigger;    
import org.quartz.JobDetail;    
import org.quartz.Scheduler;    
import org.quartz.SchedulerException;    
import org.quartz.SchedulerFactory;    
import org.quartz.impl.StdSchedulerFactory;    
    
/**  
 * 定时任务管理类  
 *  
 */    
public class QuartzManager {    
    private static SchedulerFactory gSchedulerFactory = new StdSchedulerFactory();    
    private static String JOB_GROUP_NAME = "EXTJWEB_JOBGROUP_NAME";    
    private static String TRIGGER_GROUP_NAME = "EXTJWEB_TRIGGERGROUP_NAME";    
    
    /**  
     * 添加一个定时任务,使用默认的任务组名,触发器名,触发器组名  
     *  
     * @param jobName  
     *            任务名  
     * @param jobClass  
     *            任务  
     * @param time  
     *            时间设置,参考quartz说明文档  
     * @throws SchedulerException  
     * @throws ParseException  
     */    
    public static void addJob(String jobName, String jobClass, String time) {    
        try {    
            Scheduler sched = gSchedulerFactory.getScheduler();    
            JobDetail jobDetail = new JobDetail(jobName, JOB_GROUP_NAME, Class.forName(jobClass));// 任务名,任务组,任务执行类    
            // 触发器    
            CronTrigger trigger = new CronTrigger(jobName, TRIGGER_GROUP_NAME);// 触发器名,触发器组    
            trigger.setCronExpression(time);// 触发器时间设定    
            sched.scheduleJob(jobDetail, trigger);    
            // 启动    
            if (!sched.isShutdown()){    
                sched.start();    
            }    
        } catch (Exception e) {    
            e.printStackTrace();    
            throw new RuntimeException(e);    
        }    
    }    
    
    /**  
     * 添加一个定时任务  
     *  
     * @param jobName  
     *            任务名  
     * @param jobGroupName  
     *            任务组名  
     * @param triggerName  
     *            触发器名  
     * @param triggerGroupName  
     *            触发器组名  
     * @param jobClass  
     *            任务  
     * @param time  
     *            时间设置,参考quartz说明文档  
     * @throws SchedulerException  
     * @throws ParseException  
     */    
    public static void addJob(String jobName, String jobGroupName,    
            String triggerName, String triggerGroupName, String jobClass, String time){    
        try {    
            Scheduler sched = gSchedulerFactory.getScheduler();    
            JobDetail jobDetail = new JobDetail(jobName, jobGroupName, Class.forName(jobClass));// 任务名,任务组,任务执行类    
            // 触发器    
            CronTrigger trigger = new CronTrigger(triggerName, triggerGroupName);// 触发器名,触发器组    
            trigger.setCronExpression(time);// 触发器时间设定    
            sched.scheduleJob(jobDetail, trigger);    
        } catch (Exception e) {    
            e.printStackTrace();    
            throw new RuntimeException(e);    
        }    
    }    
    
    /**  
     * 修改一个任务的触发时间(使用默认的任务组名,触发器名,触发器组名)  
     *  
     * @param jobName  
     * @param time  
     */    
    public static void modifyJobTime(String jobName, String time) {    
        try {    
            Scheduler sched = gSchedulerFactory.getScheduler();    
            CronTrigger trigger = (CronTrigger) sched.getTrigger(jobName, TRIGGER_GROUP_NAME);    
            if(trigger == null) {    
                return;    
            }    
            String oldTime = trigger.getCronExpression();    
            if (!oldTime.equalsIgnoreCase(time)) {    
                JobDetail jobDetail = sched.getJobDetail(jobName, JOB_GROUP_NAME);    
                Class objJobClass = jobDetail.getJobClass();    
                String jobClass = objJobClass.getName();    
                removeJob(jobName);    
    
                addJob(jobName, jobClass, time);    
            }    
        } catch (Exception e) {    
            e.printStackTrace();    
            throw new RuntimeException(e);    
        }    
    }    
    
    /**  
     * 修改一个任务的触发时间  
     *  
     * @param triggerName  
     * @param triggerGroupName  
     * @param time  
     */    
    public static void modifyJobTime(String triggerName,    
            String triggerGroupName, String time) {    
        try {    
            Scheduler sched = gSchedulerFactory.getScheduler();    
            CronTrigger trigger = (CronTrigger) sched.getTrigger(triggerName, triggerGroupName);    
            if(trigger == null) {    
                return;    
            }    
            String oldTime = trigger.getCronExpression();    
            if (!oldTime.equalsIgnoreCase(time)) {    
                CronTrigger ct = (CronTrigger) trigger;    
                // 修改时间    
                ct.setCronExpression(time);    
                // 重启触发器    
                sched.resumeTrigger(triggerName, triggerGroupName);    
            }    
        } catch (Exception e) {    
            e.printStackTrace();    
            throw new RuntimeException(e);    
        }    
    }    
    
    /**  
     * 移除一个任务(使用默认的任务组名,触发器名,触发器组名)  
     *  
     * @param jobName  
     */    
    public static void removeJob(String jobName) {    
        try {    
            Scheduler sched = gSchedulerFactory.getScheduler();    
            sched.pauseTrigger(jobName, TRIGGER_GROUP_NAME);// 停止触发器    
            sched.unscheduleJob(jobName, TRIGGER_GROUP_NAME);// 移除触发器    
            sched.deleteJob(jobName, JOB_GROUP_NAME);// 删除任务    
        } catch (Exception e) {    
            e.printStackTrace();    
            throw new RuntimeException(e);    
        }    
    }    
    
    /**  
     * 移除一个任务  
     *  
     * @param jobName  
     * @param jobGroupName  
     * @param triggerName  
     * @param triggerGroupName  
     */    
    public static void removeJob(String jobName, String jobGroupName,    
            String triggerName, String triggerGroupName) {    
        try {    
            Scheduler sched = gSchedulerFactory.getScheduler();    
            sched.pauseTrigger(triggerName, triggerGroupName);// 停止触发器    
            sched.unscheduleJob(triggerName, triggerGroupName);// 移除触发器    
            sched.deleteJob(jobName, jobGroupName);// 删除任务    
        } catch (Exception e) {    
            e.printStackTrace();    
            throw new RuntimeException(e);    
        }    
    }    
    
    /**  
     * 启动所有定时任务  
     */    
    public static void startJobs() {    
        try {    
            Scheduler sched = gSchedulerFactory.getScheduler();    
            sched.start();    
        } catch (Exception e) {    
            e.printStackTrace();    
            throw new RuntimeException(e);    
        }    
    }    
    
    /**  
     * 关闭所有定时任务  
     */    
    public static void shutdownJobs() {    
        try {    
            Scheduler sched = gSchedulerFactory.getScheduler();    
            if(!sched.isShutdown()) {    
                sched.shutdown();    
            }    
        } catch (Exception e) {    
            e.printStackTrace();    
            throw new RuntimeException(e);    
        }    
    }    
}

简单实现Schedule的Quartz的例子


 第一步:引包

  要使用Quartz,必须要引入以下这几个包:

  1、log4j-1.2.16

  2、quartz-2.1.7

  3、slf4j-api-1.6.1.jar

  4、slf4j-log4j12-1.6.1.jar

  这些包都在下载的Quartz包里面包含着,因此没有必要为寻找这几个包而头疼。

  第二步:创建要被定执行的任务类

  这一步也很简单,只需要创建一个实现了org.quartz.Job接口的类,并实现这个接口的唯一一个方法execute(JobExecutionContext arg0) throws JobExecutionException即可。如:

import java.text.SimpleDateFormat;   
   
import java.util.Date;   
   
import org.quartz.Job;   
import org.quartz.JobExecutionContext;   
import org.quartz.JobExecutionException;   
   
public class myJob implements Job {   
   
    @Override   
    public void execute(JobExecutionContext arg0) throws JobExecutionException {   
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss SSS");   
        System.out.println(sdf.format(new Date()));   
    }   
   
}

第三步:创建任务调度,并执行

import java.text.SimpleDateFormat;  
import java.util.Date;  
  
import org.quartz.CronTrigger;  
import org.quartz.JobDetail;  
import org.quartz.Scheduler;  
import org.quartz.SchedulerFactory;  
import org.quartz.impl.StdSchedulerFactory;  
  
public class Test {  
    public void go() throws Exception {  
        // 首先,必需要取得一个Scheduler的引用  
        SchedulerFactory sf = new StdSchedulerFactory();  
        Scheduler sched = sf.getScheduler();  
        String time="0 51 11 ? * *";  
        // jobs可以在scheduled的sched.start()方法前被调用  
  
        // job 1将每隔20秒执行一次  
        JobDetail job = new JobDetail("job1", "group1", myJob.class);  
        CronTrigger trigger = new CronTrigger("trigger1", "group1");  
        trigger.setCronExpression(time);  
        Date ft = sched.scheduleJob(job, trigger);  
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss SSS");  
        System.out.println(  
                job.getKey() + " 已被安排执行于: " + sdf.format(ft) + ",并且以如下重复规则重复执行: " + trigger.getCronExpression());  
  
        // job 2将每2分钟执行一次(在该分钟的第15秒)  
        job = new JobDetail("job2", "group1",myJob.class);  
        trigger = new CronTrigger("trigger2", "group1");  
        trigger.setCronExpression(time);  
        ft = sched.scheduleJob(job, trigger);  
        System.out.println(  
                job.getKey() + " 已被安排执行于: " + sdf.format(ft) + ",并且以如下重复规则重复执行: " + trigger.getCronExpression());  
  
        // 开始执行,start()方法被调用后,计时器就开始工作,计时调度中允许放入N个Job  
        sched.start();  
        try {  
            // 主线程等待一分钟  
            Thread.sleep(60L * 1000L);  
        } catch (Exception e) {  
        }  
        // 关闭定时调度,定时器不再工作  
        sched.shutdown(true);  
    }  
  
    public static void main(String[] args) throws Exception {  
  
        Test test = new Test();  
        test.go();  
    }  
  
}



更多 java实现定时任务 Schedule相关文章请关注PHP中文网!


声明
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系admin@php.cn
JVM性能与其他语言JVM性能与其他语言May 14, 2025 am 12:16 AM

JVM'SperformanceIsCompetitiveWithOtherRuntimes,operingabalanceOfspeed,安全性和生产性。1)JVMUSESJITCOMPILATIONFORDYNAMICOPTIMIZAIZATIONS.2)c提供NativePernativePerformanceButlanceButlactsjvm'ssafetyFeatures.3)

Java平台独立性:使用示例Java平台独立性:使用示例May 14, 2025 am 12:14 AM

JavaachievesPlatFormIndependencEthroughTheJavavIrtualMachine(JVM),允许CodeTorunonAnyPlatFormWithAjvm.1)codeisscompiledIntobytecode,notmachine-specificodificcode.2)bytecodeisisteredbytheybytheybytheybythejvm,enablingcross-platerssectectectectectross-eenablingcrossectectectectectection.2)

JVM架构:深入研究Java虚拟机JVM架构:深入研究Java虚拟机May 14, 2025 am 12:12 AM

TheJVMisanabstractcomputingmachinecrucialforrunningJavaprogramsduetoitsplatform-independentarchitecture.Itincludes:1)ClassLoaderforloadingclasses,2)RuntimeDataAreafordatastorage,3)ExecutionEnginewithInterpreter,JITCompiler,andGarbageCollectorforbytec

JVM:JVM与操作系统有关吗?JVM:JVM与操作系统有关吗?May 14, 2025 am 12:11 AM

JVMhasacloserelationshipwiththeOSasittranslatesJavabytecodeintomachine-specificinstructions,managesmemory,andhandlesgarbagecollection.ThisrelationshipallowsJavatorunonvariousOSenvironments,butitalsopresentschallengeslikedifferentJVMbehaviorsandOS-spe

Java:写一次,在任何地方跑步(WORA) - 深入了解平台独立性Java:写一次,在任何地方跑步(WORA) - 深入了解平台独立性May 14, 2025 am 12:05 AM

Java实现“一次编写,到处运行”通过编译成字节码并在Java虚拟机(JVM)上运行。1)编写Java代码并编译成字节码。2)字节码在任何安装了JVM的平台上运行。3)使用Java原生接口(JNI)处理平台特定功能。尽管存在挑战,如JVM一致性和平台特定库的使用,但WORA大大提高了开发效率和部署灵活性。

Java平台独立性:与不同的操作系统的兼容性Java平台独立性:与不同的操作系统的兼容性May 13, 2025 am 12:11 AM

JavaachievesPlatFormIndependencethroughTheJavavIrtualMachine(JVM),允许Codetorunondifferentoperatingsystemsswithoutmodification.thejvmcompilesjavacodeintoplatform-interploplatform-interpectentbybyteentbytybyteentbybytecode,whatittheninternterninterpretsandectectececutesoneonthepecificos,atrafficteyos,Afferctinginginginginginginginginginginginginginginginginginginginginginginginginginginginginginginginginginginginginginginginginginginging

什么功能使Java仍然强大什么功能使Java仍然强大May 13, 2025 am 12:05 AM

JavaispoperfulduetoitsplatFormitiondence,对象与偏见,RichstandardLibrary,PerformanceCapabilities和StrongsecurityFeatures.1)Platform-dimplighandependectionceallowsenceallowsenceallowsenceallowsencationSapplicationStornanyDevicesupportingJava.2)

顶级Java功能:开发人员的综合指南顶级Java功能:开发人员的综合指南May 13, 2025 am 12:04 AM

Java的顶级功能包括:1)面向对象编程,支持多态性,提升代码的灵活性和可维护性;2)异常处理机制,通过try-catch-finally块提高代码的鲁棒性;3)垃圾回收,简化内存管理;4)泛型,增强类型安全性;5)ambda表达式和函数式编程,使代码更简洁和表达性强;6)丰富的标准库,提供优化过的数据结构和算法。

See all articles

热AI工具

Undresser.AI Undress

Undresser.AI Undress

人工智能驱动的应用程序,用于创建逼真的裸体照片

AI Clothes Remover

AI Clothes Remover

用于从照片中去除衣服的在线人工智能工具。

Undress AI Tool

Undress AI Tool

免费脱衣服图片

Clothoff.io

Clothoff.io

AI脱衣机

Video Face Swap

Video Face Swap

使用我们完全免费的人工智能换脸工具轻松在任何视频中换脸!

热门文章

热工具

MinGW - 适用于 Windows 的极简 GNU

MinGW - 适用于 Windows 的极简 GNU

这个项目正在迁移到osdn.net/projects/mingw的过程中,你可以继续在那里关注我们。MinGW:GNU编译器集合(GCC)的本地Windows移植版本,可自由分发的导入库和用于构建本地Windows应用程序的头文件;包括对MSVC运行时的扩展,以支持C99功能。MinGW的所有软件都可以在64位Windows平台上运行。

安全考试浏览器

安全考试浏览器

Safe Exam Browser是一个安全的浏览器环境,用于安全地进行在线考试。该软件将任何计算机变成一个安全的工作站。它控制对任何实用工具的访问,并防止学生使用未经授权的资源。

DVWA

DVWA

Damn Vulnerable Web App (DVWA) 是一个PHP/MySQL的Web应用程序,非常容易受到攻击。它的主要目标是成为安全专业人员在合法环境中测试自己的技能和工具的辅助工具,帮助Web开发人员更好地理解保护Web应用程序的过程,并帮助教师/学生在课堂环境中教授/学习Web应用程序安全。DVWA的目标是通过简单直接的界面练习一些最常见的Web漏洞,难度各不相同。请注意,该软件中

Dreamweaver Mac版

Dreamweaver Mac版

视觉化网页开发工具

EditPlus 中文破解版

EditPlus 中文破解版

体积小,语法高亮,不支持代码提示功能