java代码启动jetty_jetty启动web项目源码分析

jetty是做什么的?

jetty是HTTP服务,HTTP客户端,和javax.servlet的容器。它本身被设计成嵌入式模式,应该将jetty集成到自己的应用,jetty本身可以实例化,能像任何POJO一样使用,用jetty就相当于把Http服务塞进了自己的应用

jetty的口号“Don't deploy your application in Jetty, deploy Jetty in your application.”

启动jetty java -jar start.jar。

运行jetty java -jar start.jar等效于 java -jar start.jar etc/jetty.xml[默认的jetty配置文件]

启动jetty若需要的更多参数,可以统一通过 start.ini 文件来配置

#===========================================================

# Jetty start.ini example

#-----------------------------------------------------------

OPTIONS=Server

etc/jetty.xml

etc/jetty-http.xml

复制代码官网启动Jetty

OPTIONS:指定构建过程中这个目录下面的所有jar都需要添加

etc/jetty.xml:它会添加到启动start.jar命令的后头

在start.ini中同时可以指定JVM的参数,只是必须添加 --exec

#===========================================================

# Jetty start.jar arguments

#-----------------------------------------------------------

--exec

-Xmx512m

-XX:OnOutOfMemoryError='kill -3 %p'

-Dcom.sun.management.jmxremote

OPTIONS=Server,jmx,resources

etc/jetty-jmx.xml

etc/jetty.xml

etc/jetty-ssl.xml

复制代码这么做是因为这里添加的JVM 参数并没有影响start.jar的启动,而是另起一个新的JVM,会加上这些参数来运行

Jetty的启动start.jar分析

主要逻辑在Main.java中

以包含java的参数运行为例

// execute Jetty in another JVM

if (args.isExec()){

//获取参数

CommandLineBuilder cmd = args.getMainArgs(true);

...

ProcessBuilder pbuilder = new ProcessBuilder(cmd.getArgs());

StartLog.endStartLog();

final Process process = pbuilder.start();

...

process.waitFor();

System.exit(0); // exit JVM when child process ends.

return;

}

复制代码

提取参数的过程中,对于非JPMS,会在最后添加

cmd.addRawArg("-cp");

cmd.addRawArg(classpath.toString());

cmd.addRawArg(getMainClassname());

复制代码

可以追踪MainClassname得到

private static final String MAIN_CLASS = "org.eclipse.jetty.xml.XmlConfiguration";

复制代码

后续新建一个进程,真正的去运行目的程序

pid = forkAndExec(launchMechanism.ordinal() + 1, //获取系统类型

helperpath, //对于java来说就是获取 java 命令地址

prog,

argBlock, argc,

envBlock, envc,

dir,

fds,

redirectErrorStream);

复制代码

XmlConfiguration启动

主要就是加载所有的xml文件,然后运行实现了LifeCycle接口的方法

List objects = new ArrayList<>(args.length);

for (int i = 0; i < args.length; i++)

{

if (!args[i].toLowerCase(Locale.ENGLISH).endsWith(".properties") && (args[i].indexOf('=')<0))

{

XmlConfiguration configuration = new XmlConfiguration(Resource.newResource(args[i]).getURI().toURL());

if (last != null)

configuration.getIdMap().putAll(last.getIdMap());

if (properties.size() > 0)

{

Map props = new HashMap<>();

for (Object key : properties.keySet())

{

props.put(key.toString(),String.valueOf(properties.get(key)));

}

configuration.getProperties().putAll(props);

}

Object obj = configuration.configure();

if (obj!=null && !objects.contains(obj))

objects.add(obj);

last = configuration;

}

}

// For all objects created by XmlConfigurations, start them if they are lifecycles.

for (Object obj : objects)

{

if (obj instanceof LifeCycle)

{

LifeCycle lc = (LifeCycle)obj;

if (!lc.isRunning())

lc.start(); //运行

}

}

复制代码对应着jetty.xml中的配置,他就是Server的start方法

jetty.xml文件

它是默认的jetty配置文件,主要包括:

服务器的类和全局选项

连接池(最大最小线程数)

连接器(端口,超时时间,缓冲区,协议等)

处理器(handler structure,可用默认的处理器或者上下文处理搜集器contextHandlerCollections)

发布管理器(用来扫描要发布的webapp和上下文)

登录服务(做权限检查)

请求日志

jetty支持多配置文件,每一个配置文件中通过指定要初始化的服务器实例,ID来标识,每个ID都会在同一个JVM中创建一个新的服务,如果在多个配置文件中用同一个ID,这些所有的配置都会用到同一个服务上

配置文件一般样式

// 根元素,指定以下配置是给那个类,一般在jetty.xml中server,或者jetty-web.xml中的WebAppContext

// setter方法调用的标识。name属性用来标识setter的方法名,如果这个方法没有找到,就把name中值当做字段来使用。如果有属性class表明这个set方法是静态方法

demo

//初始化对象,class决定new对象的类型,需要写全路径类名,没有用则调用默认的构造函数

10

xyz

//调用对象的某个方法,name属性表明确确调用的方法的名字

false

//引用之前已经生成对象的id

20

//调用当前对象的get方法,同set

demo2

复制代码

它相当于java代码

com.acme.Foo foo = new com.acme.Foo();

foo.setName("demo");

com.acme.Bar bar = new com.acme.Bar(true);

bar.setWibble(10);

bar.setWobble("xyz");

bar.setParent(foo);

bar.init(false);

foo.setNested(bar);

bar.setWibble(20);

bar.getParent().setName("demo2");

复制代码

web项目中的一般配置

web服务指定的服务类一般为 org.eclipse.jetty.server.Server,然后构建对应的实例

ThreadPool。

Connector。

Handler。

这也是jetty整个架构的体现,Connector用来接收连接,Handler用来处理request和response

5769687c0ff2d9c517ef08e9c957dfec.png

QueuedThreadPool

jetty的线程池默认使用的就是 QueuedThreadPool,它的构造函数如下

public QueuedThreadPool(@Name("maxThreads") int maxThreads, @Name("minThreads") int minThreads, @Name("idleTimeout") int idleTimeout, @Name("reservedThreads") int reservedThreads, @Name("queue") BlockingQueue queue, @Name("threadGroup") ThreadGroup threadGroup)

{

if (maxThreads < minThreads) {

throw new IllegalArgumentException("max threads ("+maxThreads+") less than min threads ("

+minThreads+")");

}

setMinThreads(minThreads);

setMaxThreads(maxThreads);

setIdleTimeout(idleTimeout);

setStopTimeout(5000);

setReservedThreads(reservedThreads);

if (queue==null)

{

int capacity=Math.max(_minThreads, 8);

queue=new BlockingArrayQueue<>(capacity, capacity);

}

_jobs=queue;

_threadGroup=threadGroup;

setThreadPoolBudget(new ThreadPoolBudget(this));

}

复制代码

本质上也是使用最大线程最小线程阻塞队列来实现

ServerConnector

public ServerConnector(

@Name("server") Server server,

@Name("executor") Executor executor,

@Name("scheduler") Scheduler scheduler,

@Name("bufferPool") ByteBufferPool bufferPool,

@Name("acceptors") int acceptors,

@Name("selectors") int selectors,

@Name("factories") ConnectionFactory... factories)

{

super(server,executor,scheduler,bufferPool,acceptors,factories);

_manager = newSelectorManager(getExecutor(), getScheduler(),selectors);

addBean(_manager, true);//在ServerConnector启动的过程中,会被启动

setAcceptorPriorityDelta(-2);

}

复制代码factories:默认使用HttpConnectionFactory

acceptors:表示用来接收新的TCP/IP连接的线程个数int cores = ProcessorUtils.availableProcessors();

if (acceptors < 0)

acceptors=Math.max(1, Math.min(4,cores/8));

if (acceptors > cores)

LOG.warn("Acceptors should be <= availableProcessors: " + this);

_acceptors = new Thread[acceptors];

复制代码

WebAppContext

处理web请求使用的handler,一般使用默认的构造函数,通过set方法来实例化对应的属性。在jetty.xml中比如指定属性configurationClasses一般取值如下

org.eclipse.jetty.webapp.WebInfConfiguration

org.eclipse.jetty.webapp.WebXmlConfiguration

org.eclipse.jetty.webapp.MetaInfConfiguration

org.eclipse.jetty.webapp.FragmentConfiguration

org.eclipse.jetty.plus.webapp.EnvConfiguration

org.eclipse.jetty.plus.webapp.PlusConfiguration

org.eclipse.jetty.annotations.AnnotationConfiguration

org.eclipse.jetty.webapp.JettyWebXmlConfiguration

org.eclipse.jetty.webapp.TagLibConfiguration

复制代码

比如web有两个WebInfConfiguration和WebXmlConfiguration,从名字可以感受到,WebInfConfiguration就是对应web项目中的WEB-INF目录,而WebXmlConfiguration就是对应着web.xml文件

Server启动

Server类是Jetty的HTTP Servlet服务器,它实现了LifeCycle接口。调用的start实现真正执行的就是Server自身的doStart

//AbstractLifeCycle中

@Override

public final void start() throws Exception

{

synchronized (_lock)

{

...

doStart();

...

}

}

复制代码类似的后续的所有相关LifeCycle的start启动,其实就是调用实现了它的类的doStart()方法

Server本身启动

protected void doStart() throws Exception

{

...

//1. 保证JVM自己挂掉的时候,对应的Jetty进程也会关掉

ShutdownMonitor.register(this);

...

//2. 按照Server自己添加的bean的顺序,来一个个的启动他们

super.doStart();

...

//3. 启动connector

for (Connector connector : _connectors)

{

try

{

connector.start();

}

catch(Throwable e)

{

mex.add(e);

}

}

...

}

复制代码

bean启动

对于server来说,它的bean会在每次调用对应的set方法都会执行,包括ThreadPool和Handler

//ContainerLifeCycle中

public boolean addBean(Object o)

{

if (o instanceof LifeCycle)

{

LifeCycle l = (LifeCycle)o;

//尚未启动的统一为AUTO

return addBean(o,l.isRunning()?Managed.UNMANAGED:Managed.AUTO);

}

return addBean(o,Managed.POJO);

}

复制代码

执行对应bean的启动

//ContainerLifeCycle中

protected void doStart() throws Exception

{

...

// start our managed and auto beans

for (Bean b : _beans)

{

if (b._bean instanceof LifeCycle)

{

LifeCycle l = (LifeCycle)b._bean;

switch(b._managed)

{

case MANAGED:

if (!l.isRunning())

start(l);

break;

case AUTO:

if (l.isRunning())

unmanage(b);

else

{

manage(b);

start(l);

}

break;

default:

break;

}

}

}

super.doStart();

}

复制代码

对于web来说,一定会配置一个handerWebAppContext来加载对应的web.xml文件

下面着重介绍 WebAppContext

QueuedThreadPool启动

@Override

protected void doStart() throws Exception

{

_tryExecutor = new ReservedThreadExecutor(this,_reservedThreads);

addBean(_tryExecutor);

super.doStart();

_threadsStarted.set(0);

startThreads(_minThreads);

}

private boolean startThreads(int threadsToStart)

{

while (threadsToStart > 0 && isRunning())

{

...

Thread thread = newThread(_runnable);

thread.setDaemon(isDaemon());

thread.setPriority(getThreadsPriority());

thread.setName(_name + "-" + thread.getId());

_threads.add(thread);

_lastShrink.set(System.nanoTime());

thread.start();

started = true;

--threadsToStart;

...

}

return true;

}

复制代码

可以看到它会直接调用去启动最小的线程数

org.eclipse.jetty.server.ServerConnector

Jetty9中它是主要的实现连接TCP/IP的类。可以在父类中找到对应dostart

//AbstractNetworkConnector

protected void doStart() throws Exception

{

open();

super.doStart();

}

//ServerConnector

public void open() throws IOException

{

if (_acceptChannel == null)

{

_acceptChannel = openAcceptChannel();

_acceptChannel.configureBlocking(true);//阻塞接收连接的channel

_localPort = _acceptChannel.socket().getLocalPort();

if (_localPort <= 0)

throw new IOException("Server channel not bound");

addBean(_acceptChannel);

}

}

复制代码

再向父类执行

//AbstractConnector

protected void doStart() throws Exception

{

...

//1. 选择连接的类型

_defaultConnectionFactory = getConnectionFactory(_defaultProtocol);

...

SslConnectionFactory ssl = getConnectionFactory(SslConnectionFactory.class);

....

//2. 启动自己的bean

super.doStart();

...

//3. 启动接收请求的线程

for (int i = 0; i < _acceptors.length; i++)

{

Acceptor a = new Acceptor(i);

addBean(a);

getExecutor().execute(a);

}

...

}

复制代码

启动自己的bean

在构造函数执行的时候,bean中添加了SelectorManager,它的实例是一个ServerConnectorManager,执行的构造函数如下

protected SelectorManager(Executor executor, Scheduler scheduler, int selectors)

{

if (selectors <= 0)

selectors = defaultSelectors(executor);

this.executor = executor;

this.scheduler = scheduler;

_selectors = new ManagedSelector[selectors];

_selectorIndexUpdate = index -> (index+1)%_selectors.length;

}

复制代码executor用来处理选中的EndPoint

scheduler处理与时间相关的事件

selectors实际就是包装了Java的Selector

启动过程其实就是去创建约定个数的ManagedSelector,它本身维护了一个Jave的Selector,一个Deque

protected void doStart() throws Exception

{

...

for (int i = 0; i < _selectors.length; i++)

{

ManagedSelector selector = newSelector(i);

_selectors[i] = selector;

addBean(selector);

}

//执行自己的bean启动

super.doStart();

}

//新建的ManagedSelector

public ManagedSelector(SelectorManager selectorManager, int id)

{

_selectorManager = selectorManager;

_id = id;

SelectorProducer producer = new SelectorProducer();

Executor executor = selectorManager.getExecutor();

//producer就是SelectorProducer,后续用到

_strategy = new EatWhatYouKill(producer,executor);

addBean(_strategy,true);

setStopTimeout(5000);

}

复制代码

再次看到ManagedSelector的启动

@Override

protected void doStart() throws Exception

{

//1. EatWhatYouKill本身并没有做特别的doStart实现

super.doStart();

//2.获取一个JavaSelector

_selector = _selectorManager.newSelector();

// The producer used by the strategies will never

// be idle (either produces a task or blocks).

// The normal strategy obtains the produced task, schedules

// a new thread to produce more, runs the task and then exits.

//3.执行EatWhatYouKill的produce方法

_selectorManager.execute(_strategy::produce);

// Set started only if we really are started

//4.往Deque中塞一个Start事件,实质就是运行起来就标志这Selector启动了

Start start = new Start();

submit(start);

start._started.await();

}

复制代码

这里的_selectorManager.execute(_strategy::produce);即去获取对应的连接建立后,处理连接事件

接收请求

Acceptor就是集成了Runnable,它的核心就是调用accept方法,对应就是ServerConnector的实现

@Override

public void accept(int acceptorID) throws IOException

{

ServerSocketChannel serverChannel = _acceptChannel;

if (serverChannel != null && serverChannel.isOpen())

{

SocketChannel channel = serverChannel.accept();//等待连接的到来

accepted(channel);

}

}

复制代码

WebAppContext

对于web项目来说,处理请求的一般使用WebAppContext。

WebAppContext是用来协助其它的handlers的构建和配置,以实现标准的web应用配置。它继承了ServletContextHandler,ServletContextHandler则支持标准的通过web.xml配置的session、security,listeners,filter,servlet和JSP

ServletContextHandler拥有 ServletHandler字段,并继承了ContextHandler

WebAppContext同时也实现了LifeCycle类,它的doStart方法核心

protected void doStart() throws Exception{

...

preConfigure();

super.doStart();

postConfigure();

...

}

复制代码

预加载

public void preConfigure() throws Exception

{

// 加载所有的xml文件

loadConfigurations();

...

for (Configuration configuration : _configurations)

{

//对每个设置的要加载的配置进行处理,比如`WebInfConfiguration`和`WebXmlConfiguration`

LOG.debug("preConfigure {} with {}",this,configuration);

configuration.preConfigure(this);

}

}

复制代码

WebInfConfiguration预加载

public void preConfigure(final WebAppContext context) throws Exception

{

//1. 创建Temp目录

resolveTempDirectory(context);

//2. 进行一些解压缩的工作,比如对war进行解压缩

unpack (context);

//3. 找到容器下面classpath上的jar

findAndFilterContainerPaths(context);

//4. 找到没有在 /WEB-INF/lib下面的jar

findAndFilterWebAppPaths(context);

//No pattern to appy to classes, just add to metadata

context.getMetaData().setWebInfClassesDirs(findClassDirs(context));

}

复制代码

它主要是处理了/WEB-INF 目录的相关工作

WebXmlConfiguration预加载

@Override

public void preConfigure (WebAppContext context) throws Exception

{

//parse webdefault.xml 这里就是获取默认的文件

String defaultsDescriptor = context.getDefaultsDescriptor();

if (defaultsDescriptor != null && defaultsDescriptor.length() > 0)

{

Resource dftResource = Resource.newSystemResource(defaultsDescriptor);

if (dftResource == null)

dftResource = context.newResource(defaultsDescriptor);

context.getMetaData().setDefaults (dftResource);

}

//parse, but don't process web.xml

//查找web.xml文件

Resource webxml = findWebXml(context);

...

}

protected Resource findWebXml(WebAppContext context) throws IOException, MalformedURLException

{

...

//获取web-inf目录

Resource web_inf = context.getWebInf();

if (web_inf != null && web_inf.isDirectory())

{

// do web.xml file

Resource web = web_inf.addPath("web.xml");

...

}

return null;

}复制代码

这里就可以确确实实看到web.xml被加载了

调用父类的doStart

沿着路径网上可以看到,在处理了一些类加载器之后

//ContextHandler

protected void doStart() throws Exception

{

...

startContext();

...

}

复制代码

在WebAppContext中对应实现如下

protected void startContext()

throws Exception

{

//1. 调用对应的配置类的配置方法

configure();

//2. 解析xml

_metadata.resolve(this);

//3. 文件加载结束启动web

startWebapp();

}

复制代码

WebXmlConfiguration.configure

它的配置则是加载了一个标签处理器

public void configure (WebAppContext context) throws Exception

{

...

context.getMetaData().addDescriptorProcessor(new StandardDescriptorProcessor());

}

//StandardDescriptorProcessor构造函数如下

public StandardDescriptorProcessor ()

{

try

{

registerVisitor("context-param", this.getClass().getMethod("visitContextParam", __signature));

registerVisitor("display-name", this.getClass().getMethod("visitDisplayName", __signature));

registerVisitor("servlet", this.getClass().getMethod("visitServlet", __signature));

registerVisitor("servlet-mapping", this.getClass().getMethod("visitServletMapping", __signature));

registerVisitor("session-config", this.getClass().getMethod("visitSessionConfig", __signature));

registerVisitor("mime-mapping", this.getClass().getMethod("visitMimeMapping", __signature));

registerVisitor("welcome-file-list", this.getClass().getMethod("visitWelcomeFileList", __signature));

registerVisitor("locale-encoding-mapping-list", this.getClass().getMethod("visitLocaleEncodingList", __signature));

registerVisitor("error-page", this.getClass().getMethod("visitErrorPage", __signature));

registerVisitor("taglib", this.getClass().getMethod("visitTagLib", __signature));

registerVisitor("jsp-config", this.getClass().getMethod("visitJspConfig", __signature));

registerVisitor("security-constraint", this.getClass().getMethod("visitSecurityConstraint", __signature));

registerVisitor("login-config", this.getClass().getMethod("visitLoginConfig", __signature));

registerVisitor("security-role", this.getClass().getMethod("visitSecurityRole", __signature));

registerVisitor("filter", this.getClass().getMethod("visitFilter", __signature));

registerVisitor("filter-mapping", this.getClass().getMethod("visitFilterMapping", __signature));

registerVisitor("listener", this.getClass().getMethod("visitListener", __signature));

registerVisitor("distributable", this.getClass().getMethod("visitDistributable", __signature));

registerVisitor("deny-uncovered-http-methods", this.getClass().getMethod("visitDenyUncoveredHttpMethods", __signature));

}

catch (Exception e)

{

throw new IllegalStateException(e);

}

}

复制代码

可以看到这些标签也就是平常写web.xml所用到的

解析web.xml

对应resolve则是获取描述符处理器一个个的去处理对应的处理器,以web.xml的处理器来说就是StandardDescriptorProcessor.process

//StandardDescriptorProcessor父类IterativeDescriptorProcessor中

public void process(WebAppContext context, Descriptor descriptor)

throws Exception

{

...

XmlParser.Node root = descriptor.getRoot();

Iterator> iter = root.iterator();

XmlParser.Node node = null;

while (iter.hasNext())

{

Object o = iter.next();

if (!(o instanceof XmlParser.Node)) continue;

node = (XmlParser.Node) o;

visit(context, descriptor, node);

}

...

}

复制代码

它会一个节点的遍历并调用对应的visit方法,StandardDescriptorProcessor中对一个存在着相应的visit4adc19b2d055089766a93f8ffc8c9694.png

以为例

public void visitListener(WebAppContext context, Descriptor descriptor, XmlParser.Node node)

{

//读取配置的listerclass的名字

String className = node.getString("listener-class", false, true);

EventListener listener = null;

try

{

if (className != null && className.length()> 0)

{

//Servlet Spec 3.0 p 74

//存在重复的名字不会构建重复的实例

for (ListenerHolder holder : context.getServletHandler().getListeners())

{

if (holder.getClassName().equals(className))

return;

}

((WebDescriptor)descriptor).addClassName(className);

//创建一个持有Listerner的类

ListenerHolder h = context.getServletHandler().newListenerHolder(new Source (Source.Origin.DESCRIPTOR, descriptor.getResource().toString()));

//设置持有的类名,即web.xml中配置的

h.setClassName(className);

//使得ServletHandler持有listener

context.getServletHandler().addListener(h);

context.getMetaData().setOrigin(className+".listener", descriptor);

}

}

catch (Exception e)

{

LOG.warn("Could not instantiate listener " + className, e);

return;

}

}

复制代码类似的ServletHandler会持有 private ServletHolder[] _servlets=new ServletHolder[0]; private FilterHolder[] _filters=new FilterHolder[0];

启动Web

它会调用父类的startContext

protected void startContext() throws Exception

{

ServletContainerInitializerCaller sciBean = getBean(ServletContainerInitializerCaller.class);

if (sciBean!=null)

//1. 调用实现了接口ServletContainerInitializerCaller的start方法

sciBean.start();

if (_servletHandler != null)

{

//Ensure listener instances are created, added to ContextHandler

if(_servletHandler.getListeners() != null)

{

for (ListenerHolder holder:_servletHandler.getListeners())

{

//获取持有listener的holder,这里实际内部实现就通过反射去创建对应listener的class对象

holder.start();

//we need to pass in the context because the ServletHandler has not

//yet got a reference to the ServletContext (happens in super.startContext)

//对class对象进行初始化

holder.initialize(_scontext);

//将新建的Listener加入contextHandler的_eventListeners,做后续启动用

addEventListener(holder.getListener());

}

}

}

//调用父类的startContext

super.startContext();

// 启动ServletHandler中的filter,servlets,listiners

if (_servletHandler != null)

_servletHandler.initialize();

}

复制代码

父类startContext最关键的在于

protected void startContext() throws Exception

{

...

if (!_servletContextListeners.isEmpty())

{

ServletContextEvent event = new ServletContextEvent(_scontext);

for (ServletContextListener listener : _servletContextListeners)

{

callContextInitialized(listener,event);//调用对应配置的listener的contextInitialized方法

_destroySerletContextListeners.add(listener);

}

}

}

复制代码

最后执行所有相关的servlet,filter的holder启动

public void initialize()

throws Exception

{

MultiException mx = new MultiException();

Stream.concat(Stream.concat(

Arrays.stream(_filters),

Arrays.stream(_servlets).sorted()),

Arrays.stream(_listeners))

.forEach(h->{

try

{

if (!h.isStarted())

{

h.start();

h.initialize();

}

}

catch (Throwable e)

{

LOG.debug(Log.EXCEPTION, e);

mx.add(e);

}

});

mx.ifExceptionThrow();

}

复制代码

以ServletHolder为例,它的实现类似于Listener,先通过反射获取对应的类,然后新建对象,最后调用servlet的init方法

private synchronized void initServlet()

throws ServletException

{

...

if (_servlet==null)

_servlet=newInstance();

...

// Handle configuring servlets that implement org.apache.jasper.servlet.JspServlet

if (isJspServlet())

{

initJspServlet();

detectJspContainer();

}

else if (_forcedPath != null)

detectJspContainer();

initMultiPart();

...

//调用Servlet的init方法

_servlet.init(_config);

...

}

复制代码

后置加载

WebInfConfiguration和WebXmlConfiguration没有实现,什么都不做

附jetty8配合maven使用

pom中配置

org.mortbay.jetty

jetty-maven-plugin

jettyVersion

9999

8888

a

10

/

复制代码

这样可以本地使用命令行 mvn jetty:run 运行jetty服务了

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值