This commit updates URLs to prefer the https protocol. Redirects are not followed to avoid accidentally expanding intentionally shortened URLs (i.e. if using a URL shortener). These URLs were unable to be fixed. Please review them to see if they can be manually resolved. * [ ] http://aopalliance.sourceforge.net (200) with 1 occurrences could not be migrated: ([https](https://aopalliance.sourceforge.net) result AnnotatedConnectException). * [ ] http://classdoc.sourceforge.net (200) with 1 occurrences could not be migrated: ([https](https://classdoc.sourceforge.net) result AnnotatedConnectException). * [ ] http://code.jquery.com:80/jquery.js (200) with 1 occurrences could not be migrated: ([https](https://code.jquery.com:80/jquery.js) result SSLException). * [ ] http://dbunit.sourceforge.net/ (200) with 1 occurrences could not be migrated: ([https](https://dbunit.sourceforge.net/) result AnnotatedConnectException). * [ ] http://docs.groovy-lang.org/docs/next/html/documentation/template-engines.html (200) with 1 occurrences could not be migrated: ([https](https://docs.groovy-lang.org/docs/next/html/documentation/template-engines.html) result SSLProtocolException). * [ ] http://domain3.com (200) with 6 occurrences could not be migrated: ([https](https://domain3.com) result ConnectTimeoutException). * [ ] http://forum.thymeleaf.org/Thymeleaf-3-0-8-JUST-PUBLISHED-td4030687.html (200) with 1 occurrences could not be migrated: ([https](https://forum.thymeleaf.org/Thymeleaf-3-0-8-JUST-PUBLISHED-td4030687.html) result SSLHandshakeException). * [ ] http://gregfranko.com/jquery.tocify.js/ (200) with 1 occurrences could not be migrated: ([https](https://gregfranko.com/jquery.tocify.js/) result SSLHandshakeException). * [ ] http://grinder.sourceforge.net/ (200) with 1 occurrences could not be migrated: ([https](https://grinder.sourceforge.net/) result AnnotatedConnectException). * [ ] http://groovy-lang.org/templating.html (200) with 3 occurrences could not be migrated: ([https](https://groovy-lang.org/templating.html) result SSLProtocolException). * [ ] http://hsqldb.org (200) with 1 occurrences could not be migrated: ([https](https://hsqldb.org) result SSLHandshakeException). * [ ] http://htmlunit.sourceforge.net/ (200) with 3 occurrences could not be migrated: ([https](https://htmlunit.sourceforge.net/) result AnnotatedConnectException). * [ ] http://htmlunit.sourceforge.net/gettingStarted.html (200) with 1 occurrences could not be migrated: ([https](https://htmlunit.sourceforge.net/gettingStarted.html) result AnnotatedConnectException). * [ ] http://htmlunit.sourceforge.net/javascript.html (200) with 1 occurrences could not be migrated: ([https](https://htmlunit.sourceforge.net/javascript.html) result AnnotatedConnectException). * [ ] http://javadiff.cvs.sourceforge.net/ (200) with 3 occurrences could not be migrated: ([https](https://javadiff.cvs.sourceforge.net/) result SSLHandshakeException). * [ ] http://jibx.sourceforge.net/ (200) with 2 occurrences could not be migrated: ([https](https://jibx.sourceforge.net/) result AnnotatedConnectException). * [ ] http://joda-time.sourceforge.net (200) with 1 occurrences could not be migrated: ([https](https://joda-time.sourceforge.net) result AnnotatedConnectException). * [ ] http://jotm.objectweb.org/ (200) with 1 occurrences could not be migrated: ([https](https://jotm.objectweb.org/) result NotSslRecordException). * [ ] http://json-b.net/ (200) with 1 occurrences could not be migrated: ([https](https://json-b.net/) result SSLHandshakeException). * [ ] http://mx4j.sourceforge.net (200) with 1 occurrences could not be migrated: ([https](https://mx4j.sourceforge.net) result AnnotatedConnectException). * [ ] http://mx4j.sourceforge.net/ (200) with 1 occurrences could not be migrated: ([https](https://mx4j.sourceforge.net/) result AnnotatedConnectException). * [ ] http://mydomain3.com (200) with 9 occurrences could not be migrated: ([https](https://mydomain3.com) result ConnectTimeoutException). * [ ] http://objenesis.org (200) with 1 occurrences could not be migrated: ([https](https://objenesis.org) result SSLHandshakeException). * [ ] http://objenesis.org/tutorial.html (200) with 1 occurrences could not be migrated: ([https](https://objenesis.org/tutorial.html) result SSLHandshakeException). * [ ] http://other.com/ (200) with 2 occurrences could not be migrated: ([https](https://other.com/) result SSLHandshakeException). * [ ] http://reactivex.io/ (200) with 1 occurrences could not be migrated: ([https](https://reactivex.io/) result SSLHandshakeException). * [ ] http://reactivex.io/documentation/operators.html (200) with 1 occurrences could not be migrated: ([https](https://reactivex.io/documentation/operators.html) result SSLHandshakeException). * [ ] http://url.somewhereelse.com (200) with 4 occurrences could not be migrated: ([https](https://url.somewhereelse.com) result SSLHandshakeException). * [ ] http://www.beanshell.org (200) with 1 occurrences could not be migrated: ([https](https://www.beanshell.org) result SSLHandshakeException). * [ ] http://www.beanshell.org/ (200) with 1 occurrences could not be migrated: ([https](https://www.beanshell.org/) result SSLHandshakeException). * [ ] http://www.doclet.com (200) with 1 occurrences could not be migrated: ([https](https://www.doclet.com) result AnnotatedConnectException). * [ ] http://www.gebish.org/ (200) with 1 occurrences could not be migrated: ([https](https://www.gebish.org/) result SSLHandshakeException). * [ ] http://www.gebish.org/manual/current/ (200) with 2 occurrences could not be migrated: ([https](https://www.gebish.org/manual/current/) result SSLHandshakeException). * [ ] http://www.groovy-lang.org/ (200) with 2 occurrences could not be migrated: ([https](https://www.groovy-lang.org/) result SSLProtocolException). * [ ] http://www.groovy-lang.org/operators.html (200) with 2 occurrences could not be migrated: ([https](https://www.groovy-lang.org/operators.html) result SSLProtocolException). * [ ] http://www.hsqldb.org (200) with 1 occurrences could not be migrated: ([https](https://www.hsqldb.org) result SSLHandshakeException). * [ ] http://www.jensgulden.de (200) with 1 occurrences could not be migrated: ([https](https://www.jensgulden.de) result AnnotatedConnectException). * [ ] http://www.mockobjects.com (200) with 2 occurrences could not be migrated: ([https](https://www.mockobjects.com) result ClosedChannelException). * [ ] http://www.mockobjects.com/ (200) with 1 occurrences could not be migrated: ([https](https://www.mockobjects.com/) result ClosedChannelException). * [ ] http://xmlunit.sourceforge.net/ (200) with 2 occurrences could not be migrated: ([https](https://xmlunit.sourceforge.net/) result AnnotatedConnectException). * [ ] http://192.168.0.1/mvc-showcase (301) with 1 occurrences could not be migrated: ([https](https://192.168.0.1/mvc-showcase) result ConnectTimeoutException). * [ ] http://www.easymock.org (301) with 2 occurrences could not be migrated: ([https](https://www.easymock.org) result SSLHandshakeException). * [ ] http://www.easymock.org/ (301) with 1 occurrences could not be migrated: ([https](https://www.easymock.org/) result SSLHandshakeException). * [ ] http://www.jmock.org/ (301) with 1 occurrences could not be migrated: ([https](https://www.jmock.org/) result SSLHandshakeException). * [ ] http://foo.com (301) with 4 occurrences could not be migrated: ([https](https://foo.com) result SSLHandshakeException). * [ ] http://foo.com/ (301) with 1 occurrences could not be migrated: ([https](https://foo.com/) result SSLHandshakeException). * [ ] http://foo.com/a (301) with 1 occurrences could not be migrated: ([https](https://foo.com/a) result SSLHandshakeException). * [ ] http://foo.com/a?foo=bar&foo=baz (301) with 4 occurrences could not be migrated: ([https](https://foo.com/a?foo=bar&foo=baz) result SSLHandshakeException). * [ ] http://foo.com/bar (301) with 5 occurrences could not be migrated: ([https](https://foo.com/bar) result SSLHandshakeException). * [ ] http://foo.com/baz (301) with 1 occurrences could not be migrated: ([https](https://foo.com/baz) result SSLHandshakeException). * [ ] http://foo.com/rest/books/6.json (301) with 1 occurrences could not be migrated: ([https](https://foo.com/rest/books/6.json) result SSLHandshakeException). * [ ] http://foo.com/rest/books/6/pages/1.json (301) with 1 occurrences could not be migrated: ([https](https://foo.com/rest/books/6/pages/1.json) result SSLHandshakeException). * [ ] http://foo.com/v1 (301) with 1 occurrences could not be migrated: ([https](https://foo.com/v1) result SSLHandshakeException). * [ ] http://foo.com/v1/123 (301) with 1 occurrences could not be migrated: ([https](https://foo.com/v1/123) result SSLHandshakeException). * [ ] http://foo.com/v1/bar (301) with 1 occurrences could not be migrated: ([https](https://foo.com/v1/bar) result SSLHandshakeException). * [ ] http://foo.com/v1?id=123 (301) with 2 occurrences could not be migrated: ([https](https://foo.com/v1?id=123) result SSLHandshakeException). * [ ] http://foo.com/wrong (301) with 1 occurrences could not be migrated: ([https](https://foo.com/wrong) result SSLHandshakeException). * [ ] http://mydomain4.com (301) with 3 occurrences could not be migrated: ([https](https://mydomain4.com) result SSLHandshakeException). * [ ] http://www.caucho.com/hessian (302) with 6 occurrences could not be migrated: ([https](https://www.caucho.com/hessian) result SSLHandshakeException). * [ ] http://mydomain2.com (302) with 22 occurrences could not be migrated: ([https](https://mydomain2.com) result ConnectTimeoutException). * [ ] http://www.jdiff.org (302) with 1 occurrences could not be migrated: ([https](https://www.jdiff.org) result ConnectTimeoutException). * [ ] http://example.com:80/test/this/here (404) with 3 occurrences could not be migrated: ([https](https://example.com:80/test/this/here) result NotSslRecordException). * [ ] http://www.foo.com/schema/component/component.xsd (404) with 1 occurrences could not be migrated: ([https](https://www.foo.com/schema/component/component.xsd) result SSLHandshakeException). * [ ] http://www.foo.com/schema/jcache (404) with 2 occurrences could not be migrated: ([https](https://www.foo.com/schema/jcache) result SSLHandshakeException). * [ ] http://www.gebish.org/manual/current/testing.html (404) with 1 occurrences could not be migrated: ([https](https://www.gebish.org/manual/current/testing.html) result SSLHandshakeException). * [ ] http://www.mycompany.com/schema/myns/myns.xsd (404) with 1 occurrences could not be migrated: ([https](https://www.mycompany.com/schema/myns/myns.xsd) result ConnectTimeoutException). * [ ] http://xunitpatterns.com/Test%20Spy.html (404) with 1 occurrences could not be migrated: ([https](https://xunitpatterns.com/Test%20Spy.html) result AnnotatedConnectException). These URLs were fixed, but the https status was not OK. However, the https status was the same as the http request or http redirected to an https URL, so they were migrated. Your review is recommended. * [ ] http://bugs.sun.com/view_bug.do?bug_id=6342411 (302) with 1 occurrences migrated to: https://bugs.java.com/view_bug.do?bug_id=6342411 ([https](https://bugs.sun.com/view_bug.do?bug_id=6342411) result SSLHandshakeException). * [ ] http://bugs.sun.com/view_bug.do?bug_id=7023180 (302) with 2 occurrences migrated to: https://bugs.java.com/view_bug.do?bug_id=7023180 ([https](https://bugs.sun.com/view_bug.do?bug_id=7023180) result SSLHandshakeException). * [ ] http://www.freemarker.org (301) with 5 occurrences migrated to: https://freemarker.apache.org/ ([https](https://www.freemarker.org) result ConnectTimeoutException). * [ ] http://sockjs.org (303) with 2 occurrences migrated to: https://github.com/sockjs/sockjs-client ([https](https://sockjs.org) result AnnotatedConnectException). * [ ] http://network.pivotal.io/open-source (301) with 1 occurrences migrated to: https://network.pivotal.io/open-source ([https](https://network.pivotal.io/open-source) result ReadTimeoutException). * [ ] http://aaa.org:8080/a (301) with 1 occurrences migrated to: https://www.aaa.org/articles/ ([https](https://aaa.org:8080/a) result NotSslRecordException). * [ ] http://aaa.org:8080/b/c/d (301) with 1 occurrences migrated to: https://www.aaa.org/b/c/d ([https](https://aaa.org:8080/b/c/d) result ReadTimeoutException). * [ ] http://www.caucho.com/ (302) with 1 occurrences migrated to: https://www.caucho.com/ ([https](https://www.caucho.com/) result SSLHandshakeException). * [ ] http://www.caucho.com (302) with 1 occurrences migrated to: https://www.caucho.com/ ([https](https://www.caucho.com) result SSLHandshakeException). * [ ] http://jruby.org (301) with 4 occurrences migrated to: https://www.jruby.org ([https](https://jruby.org) result SSLHandshakeException). * [ ] http://jruby.org/ (301) with 1 occurrences migrated to: https://www.jruby.org ([https](https://jruby.org/) result SSLHandshakeException). * [ ] http://bbb.org:9090/b (AnnotatedConnectException) with 2 occurrences migrated to: https://bbb.org:9090/b ([https](https://bbb.org:9090/b) result AnnotatedConnectException). * [ ] http://192.168.28.42/1.jsp (ConnectTimeoutException) with 3 occurrences migrated to: https://192.168.28.42/1.jsp ([https](https://192.168.28.42/1.jsp) result ConnectTimeoutException). * [ ] http://example.com:8080/ (ConnectTimeoutException) with 1 occurrences migrated to: https://example.com:8080/ ([https](https://example.com:8080/) result ConnectTimeoutException). * [ ] http://example.com:8080/bar (ConnectTimeoutException) with 5 occurrences migrated to: https://example.com:8080/bar ([https](https://example.com:8080/bar) result ConnectTimeoutException). * [ ] http://example.org:9090 (ConnectTimeoutException) with 2 occurrences migrated to: https://example.org:9090 ([https](https://example.org:9090) result ConnectTimeoutException). * [ ] http://example.org:9090/base (ConnectTimeoutException) with 12 occurrences migrated to: https://example.org:9090/base ([https](https://example.org:9090/base) result ConnectTimeoutException). * [ ] http://example.org:9090/base/people (ConnectTimeoutException) with 2 occurrences migrated to: https://example.org:9090/base/people ([https](https://example.org:9090/base/people) result ConnectTimeoutException). * [ ] http://example.org:9090/base/something/1/foo (ConnectTimeoutException) with 2 occurrences migrated to: https://example.org:9090/base/something/1/foo ([https](https://example.org:9090/base/something/1/foo) result ConnectTimeoutException). * [ ] http://example.org:9090/base/something/else (ConnectTimeoutException) with 2 occurrences migrated to: https://example.org:9090/base/something/else ([https](https://example.org:9090/base/something/else) result ConnectTimeoutException). * [ ] http://example.org:9999/base (ConnectTimeoutException) with 1 occurrences migrated to: https://example.org:9999/base ([https](https://example.org:9999/base) result ConnectTimeoutException). * [ ] http://example.org:9999/base/people/123/addresses/DE (ConnectTimeoutException) with 1 occurrences migrated to: https://example.org:9999/base/people/123/addresses/DE ([https](https://example.org:9999/base/people/123/addresses/DE) result ConnectTimeoutException). * [ ] http://example.org:9999/next (ConnectTimeoutException) with 1 occurrences migrated to: https://example.org:9999/next ([https](https://example.org:9999/next) result ConnectTimeoutException). * [ ] http://example.org:9999/path?q=foo (ConnectTimeoutException) with 1 occurrences migrated to: https://example.org:9999/path?q=foo ([https](https://example.org:9999/path?q=foo) result ConnectTimeoutException). * [ ] http://foo.com:8080/v1/bar?id=123 (ConnectTimeoutException) with 1 occurrences migrated to: https://foo.com:8080/v1/bar?id=123 ([https](https://foo.com:8080/v1/bar?id=123) result ConnectTimeoutException). * [ ] http://mydomain1.com:123 (ConnectTimeoutException) with 1 occurrences migrated to: https://mydomain1.com:123 ([https](https://mydomain1.com:123) result ConnectTimeoutException). * [ ] http://site1.com (ConnectTimeoutException) with 13 occurrences migrated to: https://site1.com ([https](https://site1.com) result ConnectTimeoutException). * [ ] http://wwws.sun.com/software/xml/developers/diffmk/ (ConnectTimeoutException) with 1 occurrences migrated to: https://wwws.sun.com/software/xml/developers/diffmk/ ([https](https://wwws.sun.com/software/xml/developers/diffmk/) result ConnectTimeoutException). * [ ] http://www.w3.org/TR/html4/loose.dtd (ReadTimeoutException) with 5 occurrences migrated to: https://www.w3.org/TR/html4/loose.dtd ([https](https://www.w3.org/TR/html4/loose.dtd) result ReadTimeoutException). * [ ] http://DOMAIN2.com (UnknownHostException) with 2 occurrences migrated to: https://DOMAIN2.com ([https](https://DOMAIN2.com) result UnknownHostException). * [ ] http://a.example.org/mvc-showcase (UnknownHostException) with 1 occurrences migrated to: https://a.example.org/mvc-showcase ([https](https://a.example.org/mvc-showcase) result UnknownHostException). * [ ] http://arjen:foobar@java.sun.com:80 (UnknownHostException) with 1 occurrences migrated to: https://arjen:foobar@java.sun.com:80 ([https](https://arjen:foobar@java.sun.com:80) result UnknownHostException). * [ ] http://dev.bar.com (UnknownHostException) with 3 occurrences migrated to: https://dev.bar.com ([https](https://dev.bar.com) result UnknownHostException). * [ ] http://domain1.com (UnknownHostException) with 25 occurrences migrated to: https://domain1.com ([https](https://domain1.com) result UnknownHostException). * [ ] http://domain1.com/test.html (UnknownHostException) with 4 occurrences migrated to: https://domain1.com/test.html ([https](https://domain1.com/test.html) result UnknownHostException). * [ ] http://domain2.com (UnknownHostException) with 92 occurrences migrated to: https://domain2.com ([https](https://domain2.com) result UnknownHostException). * [ ] http://example.com",HttpMethod.GET (UnknownHostException) with 6 occurrences migrated to: https://example.com",HttpMethod.GET ([https](https://example.com",HttpMethod.GET) result UnknownHostException). * [ ] http://foo.bar.com (UnknownHostException) with 4 occurrences migrated to: https://foo.bar.com ([https](https://foo.bar.com) result UnknownHostException). * [ ] http://joe.openid.example.org/ (UnknownHostException) with 2 occurrences migrated to: https://joe.openid.example.org/ ([https](https://joe.openid.example.org/) result UnknownHostException). * [ ] http://myserver/logo.png (UnknownHostException) with 1 occurrences migrated to: https://myserver/logo.png ([https](https://myserver/logo.png) result UnknownHostException). * [ ] http://samples.springframework.org/order (UnknownHostException) with 2 occurrences migrated to: https://samples.springframework.org/order ([https](https://samples.springframework.org/order) result UnknownHostException). * [ ] http://url.somewhere.com (UnknownHostException) with 25 occurrences migrated to: https://url.somewhere.com ([https](https://url.somewhere.com) result UnknownHostException). * [ ] http://url.somewhere.com/path (UnknownHostException) with 1 occurrences migrated to: https://url.somewhere.com/path ([https](https://url.somewhere.com/path) result UnknownHostException). * [ ] http://url.somewhere.com/path?id=1 (UnknownHostException) with 1 occurrences migrated to: https://url.somewhere.com/path?id=1 ([https](https://url.somewhere.com/path?id=1) result UnknownHostException). * [ ] http://url.somewhere.com/test.htm (UnknownHostException) with 2 occurrences migrated to: https://url.somewhere.com/test.htm ([https](https://url.somewhere.com/test.htm) result UnknownHostException). * [ ] http://url.somewhere.com?a=b&c=d (UnknownHostException) with 1 occurrences migrated to: https://url.somewhere.com?a=b&c=d ([https](https://url.somewhere.com?a=b&c=d) result UnknownHostException). * [ ] http://url.somewhere.com?foo= (UnknownHostException) with 2 occurrences migrated to: https://url.somewhere.com?foo= ([https](https://url.somewhere.com?foo=) result UnknownHostException). * [ ] http://url.somewhere.com?foo=bar (UnknownHostException) with 4 occurrences migrated to: https://url.somewhere.com?foo=bar ([https](https://url.somewhere.com?foo=bar) result UnknownHostException). * [ ] http://url.somewhere.com?foo=bar&a=b&c=d (UnknownHostException) with 2 occurrences migrated to: https://url.somewhere.com?foo=bar&a=b&c=d ([https](https://url.somewhere.com?foo=bar&a=b&c=d) result UnknownHostException). * [ ] http://wiki.fasterxml.com/JacksonHome (UnknownHostException) with 3 occurrences migrated to: https://wiki.fasterxml.com/JacksonHome ([https](https://wiki.fasterxml.com/JacksonHome) result UnknownHostException). * [ ] http://wiki.fasterxml.com/JacksonJsonViews (UnknownHostException) with 4 occurrences migrated to: https://wiki.fasterxml.com/JacksonJsonViews ([https](https://wiki.fasterxml.com/JacksonJsonViews) result UnknownHostException). * [ ] http://x.y.z (UnknownHostException) with 2 occurrences migrated to: https://x.y.z ([https](https://x.y.z) result UnknownHostException). * [ ] http://abc.com/v1 (301) with 1 occurrences migrated to: https://abc.go.com/v1 ([https](https://abc.com/v1) result 404). * [ ] http://abc.com/v1/accounts/43 (301) with 1 occurrences migrated to: https://abc.go.com/v1/accounts/43 ([https](https://abc.com/v1/accounts/43) result 404). * [ ] http://abc.com/v1/accounts?q=12 (301) with 1 occurrences migrated to: https://abc.go.com/v1/accounts?q=12 ([https](https://abc.com/v1/accounts?q=12) result 404). * [ ] http://abc.com/v2/accounts?q=12 (301) with 1 occurrences migrated to: https://abc.go.com/v2/accounts?q=12 ([https](https://abc.com/v2/accounts?q=12) result 404). * [ ] http://docs.spring.io/v1/bar (301) with 1 occurrences migrated to: https://docs.spring.io/v1/bar ([https](https://docs.spring.io/v1/bar) result 404). * [ ] http://example.com/-foo (404) with 2 occurrences migrated to: https://example.com/-foo ([https](https://example.com/-foo) result 404). * [ ] http://example.com/1 (404) with 1 occurrences migrated to: https://example.com/1 ([https](https://example.com/1) result 404). * [ ] http://example.com/1/2 (404) with 2 occurrences migrated to: https://example.com/1/2 ([https](https://example.com/1/2) result 404). * [ ] http://example.com/2 (404) with 1 occurrences migrated to: https://example.com/2 ([https](https://example.com/2) result 404). * [ ] http://example.com/a%20b?q=a%2Bb (404) with 1 occurrences migrated to: https://example.com/a%20b?q=a%2Bb ([https](https://example.com/a%20b?q=a%2Bb) result 404). * [ ] http://example.com/abc/ (404) with 1 occurrences migrated to: https://example.com/abc/ ([https](https://example.com/abc/) result 404). * [ ] http://example.com/abc/x/y (404) with 1 occurrences migrated to: https://example.com/abc/x/y ([https](https://example.com/abc/x/y) result 404). * [ ] http://example.com/abc/x/y/z (404) with 3 occurrences migrated to: https://example.com/abc/x/y/z ([https](https://example.com/abc/x/y/z) result 404). * [ ] http://example.com/app/login/authenticate (404) with 1 occurrences migrated to: https://example.com/app/login/authenticate ([https](https://example.com/app/login/authenticate) result 404). * [ ] http://example.com/arbitrary/path (404) with 1 occurrences migrated to: https://example.com/arbitrary/path ([https](https://example.com/arbitrary/path) result 404). * [ ] http://example.com/bar (404) with 6 occurrences migrated to: https://example.com/bar ([https](https://example.com/bar) result 404). * [ ] http://example.com/context.xml (404) with 4 occurrences migrated to: https://example.com/context.xml ([https](https://example.com/context.xml) result 404). * [ ] http://example.com/example (404) with 6 occurrences migrated to: https://example.com/example ([https](https://example.com/example) result 404). * [ ] http://example.com/example/?name (404) with 1 occurrences migrated to: https://example.com/example/?name ([https](https://example.com/example/?name) result 404). * [ ] http://example.com/example/?name= (404) with 1 occurrences migrated to: https://example.com/example/?name= ([https](https://example.com/example/?name=) result 404). * [ ] http://example.com/example/?name=%20 (404) with 1 occurrences migrated to: https://example.com/example/?name=%20 ([https](https://example.com/example/?name=%20) result 404). * [ ] http://example.com/example/?name=row%5B0%5D (404) with 1 occurrences migrated to: https://example.com/example/?name=row%5B0%5D ([https](https://example.com/example/?name=row%5B0%5D) result 404). * [ ] http://example.com/example/?name=value (404) with 1 occurrences migrated to: https://example.com/example/?name=value ([https](https://example.com/example/?name=value) result 404). * [ ] http://example.com/example/?name=value¶m2=value+2 (404) with 1 occurrences migrated to: https://example.com/example/?name=value¶m2=value+2 ([https](https://example.com/example/?name=value¶m2=value+2) result 404). * [ ] http://example.com/example/?row%5B0%5D=value (404) with 1 occurrences migrated to: https://example.com/example/?row%5B0%5D=value ([https](https://example.com/example/?row%5B0%5D=value) result 404). * [ ] http://example.com/foo (404) with 5 occurrences migrated to: https://example.com/foo ([https](https://example.com/foo) result 404). * [ ] http://example.com/foo/../bar (404) with 1 occurrences migrated to: https://example.com/foo/../bar ([https](https://example.com/foo/../bar) result 404). * [ ] http://example.com/foo/foo2?bar (404) with 1 occurrences migrated to: https://example.com/foo/foo2?bar ([https](https://example.com/foo/foo2?bar) result 404). * [ ] http://example.com/foo?bar (404) with 4 occurrences migrated to: https://example.com/foo?bar ([https](https://example.com/foo?bar) result 404). * [ ] http://example.com/foo?bar= (404) with 2 occurrences migrated to: https://example.com/foo?bar= ([https](https://example.com/foo?bar=) result 404). * [ ] http://example.com/foo?bar=baz (404) with 2 occurrences migrated to: https://example.com/foo?bar=baz ([https](https://example.com/foo?bar=baz) result 404). * [ ] http://example.com/foo?baz=42 (404) with 1 occurrences migrated to: https://example.com/foo?baz=42 ([https](https://example.com/foo?baz=42) result 404). * [ ] http://example.com/foo?foo=bar&baz=qux (404) with 2 occurrences migrated to: https://example.com/foo?foo=bar&baz=qux ([https](https://example.com/foo?foo=bar&baz=qux) result 404). * [ ] http://example.com/hotel (404) with 3 occurrences migrated to: https://example.com/hotel ([https](https://example.com/hotel) result 404). * [ ] http://example.com/hotel%20list (404) with 1 occurrences migrated to: https://example.com/hotel%20list ([https](https://example.com/hotel%20list) result 404). * [ ] http://example.com/hotel%20list/Z (404) with 1 occurrences migrated to: https://example.com/hotel%20list/Z ([https](https://example.com/hotel%20list/Z) result 404). * [ ] http://example.com/hotel%20list/Z%C3%BCrich (404) with 3 occurrences migrated to: https://example.com/hotel%20list/Z%C3%BCrich ([https](https://example.com/hotel%20list/Z%C3%BCrich) result 404). * [ ] http://example.com/hotels (404) with 3 occurrences migrated to: https://example.com/hotels ([https](https://example.com/hotels) result 404). * [ ] http://example.com/hotels/ (404) with 15 occurrences migrated to: https://example.com/hotels/ ([https](https://example.com/hotels/) result 404). * [ ] http://example.com/hotels/1/bookings/42 (404) with 1 occurrences migrated to: https://example.com/hotels/1/bookings/42 ([https](https://example.com/hotels/1/bookings/42) result 404). * [ ] http://example.com/hotels/1/pic/pics%2Flogo.png/size/150x150 (404) with 2 occurrences migrated to: https://example.com/hotels/1/pic/pics%2Flogo.png/size/150x150 ([https](https://example.com/hotels/1/pic/pics%2Flogo.png/size/150x150) result 404). * [ ] http://example.com/hotels/1/pic/pics/logo.png (404) with 1 occurrences migrated to: https://example.com/hotels/1/pic/pics/logo.png ([https](https://example.com/hotels/1/pic/pics/logo.png) result 404). * [ ] http://example.com/hotels/42/bookings/21 (404) with 1 occurrences migrated to: https://example.com/hotels/42/bookings/21 ([https](https://example.com/hotels/42/bookings/21) result 404). * [ ] http://example.com/hotels/42/bookings/42 (404) with 1 occurrences migrated to: https://example.com/hotels/42/bookings/42 ([https](https://example.com/hotels/42/bookings/42) result 404). * [ ] http://example.com/hotels/Rest%20%26%20Relax/bookings/42 (404) with 2 occurrences migrated to: https://example.com/hotels/Rest%20%26%20Relax/bookings/42 ([https](https://example.com/hotels/Rest%20%26%20Relax/bookings/42) result 404). * [ ] http://example.com/info (404) with 2 occurrences migrated to: https://example.com/info ([https](https://example.com/info) result 404). * [ ] http://example.com/jquery-1.11.0.min.js (404) with 2 occurrences migrated to: https://example.com/jquery-1.11.0.min.js ([https](https://example.com/jquery-1.11.0.min.js) result 404). * [ ] http://example.com/match (404) with 1 occurrences migrated to: https://example.com/match ([https](https://example.com/match) result 404). * [ ] http://example.com/myFileUpload (404) with 1 occurrences migrated to: https://example.com/myFileUpload ([https](https://example.com/myFileUpload) result 404). * [ ] http://example.com/myForm (404) with 1 occurrences migrated to: https://example.com/myForm ([https](https://example.com/myForm) result 404). * [ ] http://example.com/path (404) with 6 occurrences migrated to: https://example.com/path ([https](https://example.com/path) result 404). * [ ] http://example.com/path?query (404) with 2 occurrences migrated to: https://example.com/path?query ([https](https://example.com/path?query) result 404). * [ ] http://example.com/path?query=foo (404) with 1 occurrences migrated to: https://example.com/path?query=foo ([https](https://example.com/path?query=foo) result 404). * [ ] http://example.com/people (404) with 1 occurrences migrated to: https://example.com/people ([https](https://example.com/people) result 404). * [ ] http://example.com/people/ (404) with 1 occurrences migrated to: https://example.com/people/ ([https](https://example.com/people/) result 404). * [ ] http://example.com/prefix/path (404) with 2 occurrences migrated to: https://example.com/prefix/path ([https](https://example.com/prefix/path) result 404). * [ ] http://example.com/resource (404) with 5 occurrences migrated to: https://example.com/resource ([https](https://example.com/resource) result 404). * [ ] http://example.com/resource?access_token=123 (404) with 1 occurrences migrated to: https://example.com/resource?access_token=123 ([https](https://example.com/resource?access_token=123) result 404). * [ ] http://example.com/spaces (404) with 1 occurrences migrated to: https://example.com/spaces ([https](https://example.com/spaces) result 404). * [ ] http://example.com/spaces%20and%20%E2%82%AC (404) with 3 occurrences migrated to: https://example.com/spaces%20and%20%E2%82%AC ([https](https://example.com/spaces%20and%20%E2%82%AC) result 404). * [ ] http://example.com/spring/ (404) with 1 occurrences migrated to: https://example.com/spring/ ([https](https://example.com/spring/) result 404). * [ ] http://example.com/test/this/here (404) with 1 occurrences migrated to: https://example.com/test/this/here ([https](https://example.com/test/this/here) result 404). * [ ] http://example.com/upload (404) with 1 occurrences migrated to: https://example.com/upload ([https](https://example.com/upload) result 404). * [ ] http://example.com/user (404) with 1 occurrences migrated to: https://example.com/user ([https](https://example.com/user) result 404). * [ ] http://example.org/fonts/css (404) with 6 occurrences migrated to: https://example.org/fonts/css ([https](https://example.org/fonts/css) result 404). * [ ] http://example.org/foo/page.html (404) with 1 occurrences migrated to: https://example.org/foo/page.html ([https](https://example.org/foo/page.html) result 404). * [ ] http://example.org/image.png (404) with 4 occurrences migrated to: https://example.org/image.png ([https](https://example.org/image.png) result 404). * [ ] http://example.org/music/people (404) with 4 occurrences migrated to: https://example.org/music/people ([https](https://example.org/music/people) result 404). * [ ] http://example.org/page.cache.html (404) with 1 occurrences migrated to: https://example.org/page.cache.html ([https](https://example.org/page.cache.html) result 404). * [ ] http://example.org/path (404) with 3 occurrences migrated to: https://example.org/path ([https](https://example.org/path) result 404). * [ ] http://springframework.org/oxm-test (404) with 2 occurrences migrated to: https://springframework.org/oxm-test ([https](https://springframework.org/oxm-test) result 404). * [ ] http://aaa.org:8080/app/b/c/d (301) with 1 occurrences migrated to: https://www.aaa.org/app/b/c/d ([https](https://aaa.org:8080/app/b/c/d) result 404). * [ ] http://www.example.com/favicon.ico (404) with 2 occurrences migrated to: https://www.example.com/favicon.ico ([https](https://www.example.com/favicon.ico) result 404). * [ ] http://www.example.com/foo/bar (404) with 1 occurrences migrated to: https://www.example.com/foo/bar ([https](https://www.example.com/foo/bar) result 404). * [ ] http://www.example.com/hotels (404) with 2 occurrences migrated to: https://www.example.com/hotels ([https](https://www.example.com/hotels) result 404). * [ ] http://www.example.com/user/ (404) with 4 occurrences migrated to: https://www.example.com/user/ ([https](https://www.example.com/user/) result 404). * [ ] http://www.example.com/user/john%3Bdoe/dashboard (404) with 4 occurrences migrated to: https://www.example.com/user/john%3Bdoe/dashboard ([https](https://www.example.com/user/john%3Bdoe/dashboard) result 404). * [ ] http://www.example.com/user/john;doe/dashboard (404) with 2 occurrences migrated to: https://www.example.com/user/john;doe/dashboard ([https](https://www.example.com/user/john;doe/dashboard) result 404). * [ ] http://www.ibm.com/developerworks/websphere/library/techarticles/0404_tang/0404_tang.html (301) with 1 occurrences migrated to: https://www.ibm.com/developerworks/websphere/library/techarticles/0404_tang/0404_tang.html ([https](https://www.ibm.com/developerworks/websphere/library/techarticles/0404_tang/0404_tang.html) result 404). * [ ] http://mydomain1.com:80/path (301) with 2 occurrences migrated to: https://www.mydomain1.com/path ([https](https://mydomain1.com:80/path) result 404). * [ ] http://www.quartz-scheduler.org/documentation/quartz-2.1.x/tutorials/tutorial-lesson-03 (404) with 1 occurrences migrated to: https://www.quartz-scheduler.org/documentation/quartz-2.1.x/tutorials/tutorial-lesson-03 ([https](https://www.quartz-scheduler.org/documentation/quartz-2.1.x/tutorials/tutorial-lesson-03) result 404). * [ ] http://www.utoronto.ca/webdocs/HTMLdocs/Book/Book-3ed/appb/mimetype.html (301) with 1 occurrences migrated to: https://www.utoronto.ca/webdocs/HTMLdocs/Book/Book-3ed/appb/mimetype.html ([https](https://www.utoronto.ca/webdocs/HTMLdocs/Book/Book-3ed/appb/mimetype.html) result 404). * [ ] http://xml.apache.org/xslt (404) with 3 occurrences migrated to: https://xml.apache.org/xslt ([https](https://xml.apache.org/xslt) result 404). * [ ] http://myhost.com/resource/path/myTemplate.txt (500) with 1 occurrences migrated to: https://myhost.com/resource/path/myTemplate.txt ([https](https://myhost.com/resource/path/myTemplate.txt) result 500). * [ ] http://myhost.com/some/arbitrary/path (500) with 1 occurrences migrated to: https://myhost.com/some/arbitrary/path ([https](https://myhost.com/some/arbitrary/path) result 500). These URLs were switched to an https URL with a 2xx status. While the status was successful, your review is still recommended. * [ ] http://activemq.apache.org/delete-inactive-destinations.html with 1 occurrences migrated to: https://activemq.apache.org/delete-inactive-destinations.html ([https](https://activemq.apache.org/delete-inactive-destinations.html) result 200). * [ ] http://activemq.apache.org/stomp.html with 1 occurrences migrated to: https://activemq.apache.org/stomp.html ([https](https://activemq.apache.org/stomp.html) result 200). * [ ] http://ant.apache.org with 1 occurrences migrated to: https://ant.apache.org ([https](https://ant.apache.org) result 200). * [ ] http://asciidoctor.org/ with 1 occurrences migrated to: https://asciidoctor.org/ ([https](https://asciidoctor.org/) result 200). * [ ] http://asciidoctor.org/docs/editing-asciidoc-with-live-preview/ with 2 occurrences migrated to: https://asciidoctor.org/docs/editing-asciidoc-with-live-preview/ ([https](https://asciidoctor.org/docs/editing-asciidoc-with-live-preview/) result 200). * [ ] http://beanvalidation.org/ with 1 occurrences migrated to: https://beanvalidation.org/ ([https](https://beanvalidation.org/) result 200). * [ ] http://caniuse.com/ with 2 occurrences migrated to: https://caniuse.com/ ([https](https://caniuse.com/) result 200). * [ ] http://castor-data-binding.github.io/castor/reference-guides/1.3.3/html-single/index.html with 2 occurrences migrated to: https://castor-data-binding.github.io/castor/reference-guides/1.3.3/html-single/index.html ([https](https://castor-data-binding.github.io/castor/reference-guides/1.3.3/html-single/index.html) result 200). * [ ] http://cbor.io/ with 1 occurrences migrated to: https://cbor.io/ ([https](https://cbor.io/) result 200). * [ ] http://code.jquery.com/jquery-1.11.0.min.js with 2 occurrences migrated to: https://code.jquery.com/jquery-1.11.0.min.js ([https](https://code.jquery.com/jquery-1.11.0.min.js) result 200). * [ ] http://code.jquery.com/jquery.js with 1 occurrences migrated to: https://code.jquery.com/jquery.js ([https](https://code.jquery.com/jquery.js) result 200). * [ ] http://commons.apache.org/proper/commons-lang/ with 2 occurrences migrated to: https://commons.apache.org/proper/commons-lang/ ([https](https://commons.apache.org/proper/commons-lang/) result 200). * [ ] http://docs.oracle.com/javase/6/docs/api/java/beans/package-summary.html with 1 occurrences migrated to: https://docs.oracle.com/javase/6/docs/api/java/beans/package-summary.html ([https](https://docs.oracle.com/javase/6/docs/api/java/beans/package-summary.html) result 200). * [ ] http://docs.oracle.com/javase/6/docs/api/java/io/Serializable.html with 1 occurrences migrated to: https://docs.oracle.com/javase/6/docs/api/java/io/Serializable.html ([https](https://docs.oracle.com/javase/6/docs/api/java/io/Serializable.html) result 200). * [ ] http://docs.oracle.com/javase/6/docs/api/java/lang/instrument/package-summary.html with 2 occurrences migrated to: https://docs.oracle.com/javase/6/docs/api/java/lang/instrument/package-summary.html ([https](https://docs.oracle.com/javase/6/docs/api/java/lang/instrument/package-summary.html) result 200). * [ ] http://docs.oracle.com/javase/6/docs/api/java/util/concurrent/ThreadPoolExecutor.html with 1 occurrences migrated to: https://docs.oracle.com/javase/6/docs/api/java/util/concurrent/ThreadPoolExecutor.html ([https](https://docs.oracle.com/javase/6/docs/api/java/util/concurrent/ThreadPoolExecutor.html) result 200). * [ ] http://docs.oracle.com/javase/8/docs/api/javax/script/ScriptEngineFactory.html with 2 occurrences migrated to: https://docs.oracle.com/javase/8/docs/api/javax/script/ScriptEngineFactory.html ([https](https://docs.oracle.com/javase/8/docs/api/javax/script/ScriptEngineFactory.html) result 200). * [ ] http://docs.oracle.com/javase/tutorial/javabeans/advanced/customization.html with 1 occurrences migrated to: https://docs.oracle.com/javase/tutorial/javabeans/advanced/customization.html ([https](https://docs.oracle.com/javase/tutorial/javabeans/advanced/customization.html) result 200). * [ ] http://docs.oracle.com/javase/tutorial/javabeans/writing/properties.html with 1 occurrences migrated to: https://docs.oracle.com/javase/tutorial/javabeans/writing/properties.html ([https](https://docs.oracle.com/javase/tutorial/javabeans/writing/properties.html) result 200). * [ ] http://docs.seleniumhq.org/ with 1 occurrences migrated to: https://docs.seleniumhq.org/ ([https](https://docs.seleniumhq.org/) result 200). * [ ] http://docs.seleniumhq.org/projects/webdriver/ with 1 occurrences migrated to: https://docs.seleniumhq.org/projects/webdriver/ ([https](https://docs.seleniumhq.org/projects/webdriver/) result 200). * [ ] http://docs.spring.io/spring-framework/docs/current/javadoc-api/ with 1 occurrences migrated to: https://docs.spring.io/spring-framework/docs/current/javadoc-api/ ([https](https://docs.spring.io/spring-framework/docs/current/javadoc-api/) result 200). * [ ] http://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/web/servlet/view/script/ScriptTemplateView.html with 1 occurrences migrated to: https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/web/servlet/view/script/ScriptTemplateView.html ([https](https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/web/servlet/view/script/ScriptTemplateView.html) result 200). * [ ] http://docs.spring.io/spring-framework/docs/current/spring-framework-reference/ with 1 occurrences migrated to: https://docs.spring.io/spring-framework/docs/current/spring-framework-reference/ ([https](https://docs.spring.io/spring-framework/docs/current/spring-framework-reference/) result 200). * [ ] http://docs.spring.io/spring-session/docs/current/reference/html5/ with 1 occurrences migrated to: https://docs.spring.io/spring-session/docs/current/reference/html5/ ([https](https://docs.spring.io/spring-session/docs/current/reference/html5/) result 200). * [ ] http://en.wikipedia.org/wiki/Adapter_pattern with 1 occurrences migrated to: https://en.wikipedia.org/wiki/Adapter_pattern ([https](https://en.wikipedia.org/wiki/Adapter_pattern) result 200). * [ ] http://en.wikipedia.org/wiki/Halting_Problem with 1 occurrences migrated to: https://en.wikipedia.org/wiki/Halting_Problem ([https](https://en.wikipedia.org/wiki/Halting_Problem) result 200). * [ ] http://en.wikipedia.org/wiki/JSONP with 3 occurrences migrated to: https://en.wikipedia.org/wiki/JSONP ([https](https://en.wikipedia.org/wiki/JSONP) result 200). * [ ] http://en.wikipedia.org/wiki/List_of_HTTP_status_codes with 1 occurrences migrated to: https://en.wikipedia.org/wiki/List_of_HTTP_status_codes ([https](https://en.wikipedia.org/wiki/List_of_HTTP_status_codes) result 200). * [ ] http://en.wikipedia.org/wiki/MIME with 1 occurrences migrated to: https://en.wikipedia.org/wiki/MIME ([https](https://en.wikipedia.org/wiki/MIME) result 200). * [ ] http://en.wikipedia.org/wiki/Mock_Object with 1 occurrences migrated to: https://en.wikipedia.org/wiki/Mock_Object ([https](https://en.wikipedia.org/wiki/Mock_Object) result 200). * [ ] http://en.wikipedia.org/wiki/Polyfill with 2 occurrences migrated to: https://en.wikipedia.org/wiki/Polyfill ([https](https://en.wikipedia.org/wiki/Polyfill) result 200). * [ ] http://example.com with 139 occurrences migrated to: https://example.com ([https](https://example.com) result 200). * [ ] http://example.com/ with 16 occurrences migrated to: https://example.com/ ([https](https://example.com/) result 200). * [ ] http://example.com?foo with 2 occurrences migrated to: https://example.com?foo ([https](https://example.com?foo) result 200). * [ ] http://example.com?foo=bar with 8 occurrences migrated to: https://example.com?foo=bar ([https](https://example.com?foo=bar) result 200). * [ ] http://example.com?foo=bar@baz with 1 occurrences migrated to: https://example.com?foo=bar@baz ([https](https://example.com?foo=bar@baz) result 200). * [ ] http://example.org with 16 occurrences migrated to: https://example.org ([https](https://example.org) result 200). * [ ] http://example.org/ with 1 occurrences migrated to: https://example.org/ ([https](https://example.org/) result 200). * [ ] http://fasterxml.github.io/jackson-databind/javadoc/2.6/com/fasterxml/jackson/databind/DeserializationFeature.html with 2 occurrences migrated to: https://fasterxml.github.io/jackson-databind/javadoc/2.6/com/fasterxml/jackson/databind/DeserializationFeature.html ([https](https://fasterxml.github.io/jackson-databind/javadoc/2.6/com/fasterxml/jackson/databind/DeserializationFeature.html) result 200). * [ ] http://fasterxml.github.io/jackson-databind/javadoc/2.6/com/fasterxml/jackson/databind/MapperFeature.html with 2 occurrences migrated to: https://fasterxml.github.io/jackson-databind/javadoc/2.6/com/fasterxml/jackson/databind/MapperFeature.html ([https](https://fasterxml.github.io/jackson-databind/javadoc/2.6/com/fasterxml/jackson/databind/MapperFeature.html) result 200). * [ ] http://h2database.com with 1 occurrences migrated to: https://h2database.com ([https](https://h2database.com) result 200). * [ ] http://handlebarsjs.com with 2 occurrences migrated to: https://handlebarsjs.com ([https](https://handlebarsjs.com) result 200). * [ ] http://handlebarsjs.com/ with 2 occurrences migrated to: https://handlebarsjs.com/ ([https](https://handlebarsjs.com/) result 200). * [ ] http://hc.apache.org/httpcomponents-asyncclient-dev/ with 1 occurrences migrated to: https://hc.apache.org/httpcomponents-asyncclient-dev/ ([https](https://hc.apache.org/httpcomponents-asyncclient-dev/) result 200). * [ ] http://hc.apache.org/httpcomponents-client-ga/ with 2 occurrences migrated to: https://hc.apache.org/httpcomponents-client-ga/ ([https](https://hc.apache.org/httpcomponents-client-ga/) result 200). * [ ] http://hc.apache.org/httpcomponents-client-ga/httpclient/ with 1 occurrences migrated to: https://hc.apache.org/httpcomponents-client-ga/httpclient/ ([https](https://hc.apache.org/httpcomponents-client-ga/httpclient/) result 200). * [ ] http://www.hibernate.org/ (301) with 1 occurrences migrated to: https://hibernate.org/ ([https](https://www.hibernate.org/) result 200). * [ ] http://www.hibernate.org (301) with 1 occurrences migrated to: https://hibernate.org/ ([https](https://www.hibernate.org) result 200). * [ ] http://jcp.org/aboutJava/communityprocess/final/jsr003/index3.html with 1 occurrences migrated to: https://jcp.org/aboutJava/communityprocess/final/jsr003/index3.html ([https](https://jcp.org/aboutJava/communityprocess/final/jsr003/index3.html) result 200). * [ ] http://jcp.org/aboutJava/communityprocess/final/jsr160/index.html with 1 occurrences migrated to: https://jcp.org/aboutJava/communityprocess/final/jsr160/index.html ([https](https://jcp.org/aboutJava/communityprocess/final/jsr160/index.html) result 200). * [ ] http://jcp.org/en/jsr/detail?id=107 with 1 occurrences migrated to: https://jcp.org/en/jsr/detail?id=107 ([https](https://jcp.org/en/jsr/detail?id=107) result 200). * [ ] http://joel-costigliola.github.io/assertj/ with 2 occurrences migrated to: https://joel-costigliola.github.io/assertj/ ([https](https://joel-costigliola.github.io/assertj/) result 200). * [ ] http://jsonassert.skyscreamer.org with 1 occurrences migrated to: https://jsonassert.skyscreamer.org ([https](https://jsonassert.skyscreamer.org) result 200). * [ ] http://jsonassert.skyscreamer.org/ with 5 occurrences migrated to: https://jsonassert.skyscreamer.org/ ([https](https://jsonassert.skyscreamer.org/) result 200). * [ ] http://junit.org/junit5/docs/current/user-guide/ with 2 occurrences migrated to: https://junit.org/junit5/docs/current/user-guide/ ([https](https://junit.org/junit5/docs/current/user-guide/) result 200). * [ ] http://kotlinlang.org/ with 2 occurrences migrated to: https://kotlinlang.org/ ([https](https://kotlinlang.org/) result 200). * [ ] http://kotlinlang.org/docs/reference/ with 1 occurrences migrated to: https://kotlinlang.org/docs/reference/ ([https](https://kotlinlang.org/docs/reference/) result 200). * [ ] http://kotlinlang.org/docs/reference/classes.html with 3 occurrences migrated to: https://kotlinlang.org/docs/reference/classes.html ([https](https://kotlinlang.org/docs/reference/classes.html) result 200). * [ ] http://mydomain.com with 5 occurrences migrated to: https://mydomain.com ([https](https://mydomain.com) result 200). * [ ] http://myfaces.apache.org/ with 1 occurrences migrated to: https://myfaces.apache.org/ ([https](https://myfaces.apache.org/) result 200). * [ ] http://myfaces.apache.org/orchestra/ with 1 occurrences migrated to: https://myfaces.apache.org/orchestra/ ([https](https://myfaces.apache.org/orchestra/) result 200). * [ ] http://netty.io/ with 1 occurrences migrated to: https://netty.io/ ([https](https://netty.io/) result 200). * [ ] http://openjdk.java.net/jeps/290 with 1 occurrences migrated to: https://openjdk.java.net/jeps/290 ([https](https://openjdk.java.net/jeps/290) result 200). * [ ] http://openjdk.java.net/projects/nashorn/ with 10 occurrences migrated to: https://openjdk.java.net/projects/nashorn/ ([https](https://openjdk.java.net/projects/nashorn/) result 200). * [ ] http://pivotal.io/security/cve-2015-5211 with 1 occurrences migrated to: https://pivotal.io/security/cve-2015-5211 ([https](https://pivotal.io/security/cve-2015-5211) result 200). * [ ] http://poi.apache.org with 3 occurrences migrated to: https://poi.apache.org ([https](https://poi.apache.org) result 200). * [ ] http://projects.spring.io/spring-boot/ with 1 occurrences migrated to: https://projects.spring.io/spring-boot/ ([https](https://projects.spring.io/spring-boot/) result 200). * [ ] http://projects.spring.io/spring-data/ with 1 occurrences migrated to: https://projects.spring.io/spring-data/ ([https](https://projects.spring.io/spring-data/) result 200). * [ ] http://projects.spring.io/spring-framework/ with 1 occurrences migrated to: https://projects.spring.io/spring-framework/ ([https](https://projects.spring.io/spring-framework/) result 200). * [ ] http://projects.spring.io/spring-integration/ with 1 occurrences migrated to: https://projects.spring.io/spring-integration/ ([https](https://projects.spring.io/spring-integration/) result 200). * [ ] http://projects.spring.io/spring-security/ with 3 occurrences migrated to: https://projects.spring.io/spring-security/ ([https](https://projects.spring.io/spring-security/) result 200). * [ ] http://repo1.maven.org/maven2/javax/inject/javax.inject/1/ with 2 occurrences migrated to: https://repo1.maven.org/maven2/javax/inject/javax.inject/1/ ([https](https://repo1.maven.org/maven2/javax/inject/javax.inject/1/) result 200). * [ ] http://schema.spring.io/mvc/spring-mvc.xsd with 1 occurrences migrated to: https://schema.spring.io/mvc/spring-mvc.xsd ([https](https://schema.spring.io/mvc/spring-mvc.xsd) result 200). * [ ] http://search.maven.org/ with 2 occurrences migrated to: https://search.maven.org/ ([https](https://search.maven.org/) result 200). * [ ] http://sockjs.github.io/sockjs-protocol/sockjs-protocol-0.3.3.html with 3 occurrences migrated to: https://sockjs.github.io/sockjs-protocol/sockjs-protocol-0.3.3.html ([https](https://sockjs.github.io/sockjs-protocol/sockjs-protocol-0.3.3.html) result 200). * [ ] http://sourceforge.net with 1 occurrences migrated to: https://sourceforge.net ([https](https://sourceforge.net) result 200). * [ ] http://sourceforge.net/projects/javadiff/ with 1 occurrences migrated to: https://sourceforge.net/projects/javadiff/ ([https](https://sourceforge.net/projects/javadiff/) result 200). * [ ] http://sourceforge.net/sflogo.php?group_id=37160 with 1 occurrences migrated to: https://sourceforge.net/sflogo.php?group_id=37160 ([https](https://sourceforge.net/sflogo.php?group_id=37160) result 200). * [ ] http://spring.io/blog/ with 1 occurrences migrated to: https://spring.io/blog/ ([https](https://spring.io/blog/) result 200). * [ ] http://spring.io/blog/category/news with 1 occurrences migrated to: https://spring.io/blog/category/news ([https](https://spring.io/blog/category/news) result 200). * [ ] http://square.github.io/okhttp/ with 1 occurrences migrated to: https://square.github.io/okhttp/ ([https](https://square.github.io/okhttp/) result 200). * [ ] http://stackoverflow.com/questions/1481251/what-does-document-domain-document-domain-do with 2 occurrences migrated to: https://stackoverflow.com/questions/1481251/what-does-document-domain-document-domain-do ([https](https://stackoverflow.com/questions/1481251/what-does-document-domain-document-domain-do) result 200). * [ ] http://stackoverflow.com/questions/19394570/java-jre-7u45-breaks-classloader-getresources with 2 occurrences migrated to: https://stackoverflow.com/questions/19394570/java-jre-7u45-breaks-classloader-getresources ([https](https://stackoverflow.com/questions/19394570/java-jre-7u45-breaks-classloader-getresources) result 200). * [ ] http://stackoverflow.com/questions/22986109/testing-spring-managed-servlet with 1 occurrences migrated to: https://stackoverflow.com/questions/22986109/testing-spring-managed-servlet ([https](https://stackoverflow.com/questions/22986109/testing-spring-managed-servlet) result 200). * [ ] http://stackoverflow.com/questions/24725438/what-could-cause-a-class-implementing-applicationlistenercontextrefreshedevent with 1 occurrences migrated to: https://stackoverflow.com/questions/24725438/what-could-cause-a-class-implementing-applicationlistenercontextrefreshedevent ([https](https://stackoverflow.com/questions/24725438/what-could-cause-a-class-implementing-applicationlistenercontextrefreshedevent) result 200). * [ ] http://stackoverflow.com/questions/35733344/can-contextconfiguration-in-a-custom-annotation-be-merged with 1 occurrences migrated to: https://stackoverflow.com/questions/35733344/can-contextconfiguration-in-a-custom-annotation-be-merged ([https](https://stackoverflow.com/questions/35733344/can-contextconfiguration-in-a-custom-annotation-be-merged) result 200). * [ ] http://start.spring.io/ with 2 occurrences migrated to: https://start.spring.io/ ([https](https://start.spring.io/) result 200). * [ ] http://stas-blogspot.blogspot.com/2010/03/java-bridge-methods-explained.html with 1 occurrences migrated to: https://stas-blogspot.blogspot.com/2010/03/java-bridge-methods-explained.html ([https](https://stas-blogspot.blogspot.com/2010/03/java-bridge-methods-explained.html) result 200). * [ ] http://stomp.github.io/stomp-specification-1.2.html with 9 occurrences migrated to: https://stomp.github.io/stomp-specification-1.2.html ([https](https://stomp.github.io/stomp-specification-1.2.html) result 200). * [ ] http://struts.apache.org with 1 occurrences migrated to: https://struts.apache.org ([https](https://struts.apache.org) result 200). * [ ] http://struts.apache.org/ with 1 occurrences migrated to: https://struts.apache.org/ ([https](https://struts.apache.org/) result 200). * [ ] http://tapestry.apache.org/ with 2 occurrences migrated to: https://tapestry.apache.org/ ([https](https://tapestry.apache.org/) result 200). * [ ] http://tiles.apache.org with 4 occurrences migrated to: https://tiles.apache.org ([https](https://tiles.apache.org) result 200). * [ ] http://tiles.apache.org/framework/dependency-management.html with 1 occurrences migrated to: https://tiles.apache.org/framework/dependency-management.html ([https](https://tiles.apache.org/framework/dependency-management.html) result 200). * [ ] http://tomcat.apache.org/ with 2 occurrences migrated to: https://tomcat.apache.org/ ([https](https://tomcat.apache.org/) result 200). * [ ] http://tomcat.apache.org/tomcat-6.0-doc/config/context.html with 1 occurrences migrated to: https://tomcat.apache.org/tomcat-6.0-doc/config/context.html ([https](https://tomcat.apache.org/tomcat-6.0-doc/config/context.html) result 200). * [ ] http://tools.ietf.org/html/rfc1945 with 1 occurrences migrated to: https://tools.ietf.org/html/rfc1945 ([https](https://tools.ietf.org/html/rfc1945) result 200). * [ ] http://tools.ietf.org/html/rfc2109 with 2 occurrences migrated to: https://tools.ietf.org/html/rfc2109 ([https](https://tools.ietf.org/html/rfc2109) result 200). * [ ] http://tools.ietf.org/html/rfc2295 with 1 occurrences migrated to: https://tools.ietf.org/html/rfc2295 ([https](https://tools.ietf.org/html/rfc2295) result 200). * [ ] http://tools.ietf.org/html/rfc2324 with 1 occurrences migrated to: https://tools.ietf.org/html/rfc2324 ([https](https://tools.ietf.org/html/rfc2324) result 200). * [ ] http://tools.ietf.org/html/rfc2518 with 1 occurrences migrated to: https://tools.ietf.org/html/rfc2518 ([https](https://tools.ietf.org/html/rfc2518) result 200). * [ ] http://tools.ietf.org/html/rfc2616 with 3 occurrences migrated to: https://tools.ietf.org/html/rfc2616 ([https](https://tools.ietf.org/html/rfc2616) result 200). * [ ] http://tools.ietf.org/html/rfc2774 with 1 occurrences migrated to: https://tools.ietf.org/html/rfc2774 ([https](https://tools.ietf.org/html/rfc2774) result 200). * [ ] http://tools.ietf.org/html/rfc2817 with 1 occurrences migrated to: https://tools.ietf.org/html/rfc2817 ([https](https://tools.ietf.org/html/rfc2817) result 200). * [ ] http://tools.ietf.org/html/rfc2965 with 1 occurrences migrated to: https://tools.ietf.org/html/rfc2965 ([https](https://tools.ietf.org/html/rfc2965) result 200). * [ ] http://tools.ietf.org/html/rfc3229 with 1 occurrences migrated to: https://tools.ietf.org/html/rfc3229 ([https](https://tools.ietf.org/html/rfc3229) result 200). * [ ] http://tools.ietf.org/html/rfc3986 with 4 occurrences migrated to: https://tools.ietf.org/html/rfc3986 ([https](https://tools.ietf.org/html/rfc3986) result 200). * [ ] http://tools.ietf.org/html/rfc4918 with 5 occurrences migrated to: https://tools.ietf.org/html/rfc4918 ([https](https://tools.ietf.org/html/rfc4918) result 200). * [ ] http://tools.ietf.org/html/rfc5842 with 2 occurrences migrated to: https://tools.ietf.org/html/rfc5842 ([https](https://tools.ietf.org/html/rfc5842) result 200). * [ ] http://tools.ietf.org/html/rfc5988 with 1 occurrences migrated to: https://tools.ietf.org/html/rfc5988 ([https](https://tools.ietf.org/html/rfc5988) result 200). * [ ] http://tools.ietf.org/html/rfc6202 with 1 occurrences migrated to: https://tools.ietf.org/html/rfc6202 ([https](https://tools.ietf.org/html/rfc6202) result 200). * [ ] http://tools.ietf.org/html/rfc6266 with 1 occurrences migrated to: https://tools.ietf.org/html/rfc6266 ([https](https://tools.ietf.org/html/rfc6266) result 200). * [ ] http://tools.ietf.org/html/rfc6454 with 1 occurrences migrated to: https://tools.ietf.org/html/rfc6454 ([https](https://tools.ietf.org/html/rfc6454) result 200). * [ ] http://tools.ietf.org/html/rfc6455 with 2 occurrences migrated to: https://tools.ietf.org/html/rfc6455 ([https](https://tools.ietf.org/html/rfc6455) result 200). * [ ] http://tools.ietf.org/html/rfc6585 with 4 occurrences migrated to: https://tools.ietf.org/html/rfc6585 ([https](https://tools.ietf.org/html/rfc6585) result 200). * [ ] http://tools.ietf.org/html/rfc7230 with 10 occurrences migrated to: https://tools.ietf.org/html/rfc7230 ([https](https://tools.ietf.org/html/rfc7230) result 200). * [ ] http://tools.ietf.org/html/rfc7231 with 56 occurrences migrated to: https://tools.ietf.org/html/rfc7231 ([https](https://tools.ietf.org/html/rfc7231) result 200). * [ ] http://tools.ietf.org/html/rfc7232 with 8 occurrences migrated to: https://tools.ietf.org/html/rfc7232 ([https](https://tools.ietf.org/html/rfc7232) result 200). * [ ] http://tools.ietf.org/html/rfc7233 with 12 occurrences migrated to: https://tools.ietf.org/html/rfc7233 ([https](https://tools.ietf.org/html/rfc7233) result 200). * [ ] http://tools.ietf.org/html/rfc7234 with 5 occurrences migrated to: https://tools.ietf.org/html/rfc7234 ([https](https://tools.ietf.org/html/rfc7234) result 200). * [ ] http://tools.ietf.org/html/rfc7235 with 6 occurrences migrated to: https://tools.ietf.org/html/rfc7235 ([https](https://tools.ietf.org/html/rfc7235) result 200). * [ ] http://tools.ietf.org/html/rfc7238 with 1 occurrences migrated to: https://tools.ietf.org/html/rfc7238 ([https](https://tools.ietf.org/html/rfc7238) result 200). * [ ] http://tools.ietf.org/html/rfc7239 with 12 occurrences migrated to: https://tools.ietf.org/html/rfc7239 ([https](https://tools.ietf.org/html/rfc7239) result 200). * [ ] http://tools.ietf.org/rfcdiff?difftype=--hwdiff&url2=draft-ietf-webdav-protocol-06.txt with 3 occurrences migrated to: https://tools.ietf.org/rfcdiff?difftype=--hwdiff&url2=draft-ietf-webdav-protocol-06.txt ([https](https://tools.ietf.org/rfcdiff?difftype=--hwdiff&url2=draft-ietf-webdav-protocol-06.txt) result 200). * [ ] http://unlicense.org with 1 occurrences migrated to: https://unlicense.org ([https](https://unlicense.org) result 200). * [ ] http://www.amazon.com/exec/obidos/tg/detail/-/0764543857/ with 10 occurrences migrated to: https://www.amazon.com/exec/obidos/tg/detail/-/0764543857/ ([https](https://www.amazon.com/exec/obidos/tg/detail/-/0764543857/) result 200). * [ ] http://www.apache.org with 1 occurrences migrated to: https://www.apache.org ([https](https://www.apache.org) result 200). * [ ] http://www.apple.com/DTDs/PropertyList-1.0.dtd with 6 occurrences migrated to: https://www.apple.com/DTDs/PropertyList-1.0.dtd ([https](https://www.apple.com/DTDs/PropertyList-1.0.dtd) result 200). * [ ] http://www.atomikos.com/ with 1 occurrences migrated to: https://www.atomikos.com/ ([https](https://www.atomikos.com/) result 200). * [ ] http://www.baeldung.com/kotlin-null-safety with 1 occurrences migrated to: https://www.baeldung.com/kotlin-null-safety ([https](https://www.baeldung.com/kotlin-null-safety) result 200). * [ ] http://www.eclipse.org/ajdt/ with 1 occurrences migrated to: https://www.eclipse.org/ajdt/ ([https](https://www.eclipse.org/ajdt/) result 200). * [ ] http://www.eclipse.org/aspectj/doc/next/progguide/index.html with 1 occurrences migrated to: https://www.eclipse.org/aspectj/doc/next/progguide/index.html ([https](https://www.eclipse.org/aspectj/doc/next/progguide/index.html) result 200). * [ ] http://www.eclipse.org/aspectj/doc/next/progguide/semantics-joinPoints.html with 1 occurrences migrated to: https://www.eclipse.org/aspectj/doc/next/progguide/semantics-joinPoints.html ([https](https://www.eclipse.org/aspectj/doc/next/progguide/semantics-joinPoints.html) result 200). * [ ] http://www.eclipse.org/aspectj/doc/released/adk15notebook/index.html with 1 occurrences migrated to: https://www.eclipse.org/aspectj/doc/released/adk15notebook/index.html ([https](https://www.eclipse.org/aspectj/doc/released/adk15notebook/index.html) result 200). * [ ] http://www.eclipse.org/aspectj/doc/released/devguide/antTasks.html with 1 occurrences migrated to: https://www.eclipse.org/aspectj/doc/released/devguide/antTasks.html ([https](https://www.eclipse.org/aspectj/doc/released/devguide/antTasks.html) result 200). * [ ] http://www.eclipse.org/aspectj/doc/released/devguide/index.html with 1 occurrences migrated to: https://www.eclipse.org/aspectj/doc/released/devguide/index.html ([https](https://www.eclipse.org/aspectj/doc/released/devguide/index.html) result 200). * [ ] http://www.eclipse.org/aspectj/doc/released/devguide/ltw-configuration.html with 1 occurrences migrated to: https://www.eclipse.org/aspectj/doc/released/devguide/ltw-configuration.html ([https](https://www.eclipse.org/aspectj/doc/released/devguide/ltw-configuration.html) result 200). * [ ] http://www.eclipse.org/aspectj/doc/released/devguide/ltw.html with 1 occurrences migrated to: https://www.eclipse.org/aspectj/doc/released/devguide/ltw.html ([https](https://www.eclipse.org/aspectj/doc/released/devguide/ltw.html) result 200). * [ ] http://www.eclipse.org/aspectj/doc/released/progguide/index.html with 1 occurrences migrated to: https://www.eclipse.org/aspectj/doc/released/progguide/index.html ([https](https://www.eclipse.org/aspectj/doc/released/progguide/index.html) result 200). * [ ] http://www.eclipse.org/aspectj/doc/released/progguide/semantics-pointcuts.html with 1 occurrences migrated to: https://www.eclipse.org/aspectj/doc/released/progguide/semantics-pointcuts.html ([https](https://www.eclipse.org/aspectj/doc/released/progguide/semantics-pointcuts.html) result 200). * [ ] http://www.eclipse.org/aspectj/dtd/aspectj.dtd with 1 occurrences migrated to: https://www.eclipse.org/aspectj/dtd/aspectj.dtd ([https](https://www.eclipse.org/aspectj/dtd/aspectj.dtd) result 200). * [ ] http://www.eclipse.org/jetty/ with 1 occurrences migrated to: https://www.eclipse.org/jetty/ ([https](https://www.eclipse.org/jetty/) result 200). * [ ] http://ehcache.sourceforge.net (301) with 1 occurrences migrated to: https://www.ehcache.org/ ([https](https://ehcache.sourceforge.net) result 200). * [ ] http://ehcache.org/ (301) with 1 occurrences migrated to: https://www.ehcache.org/ ([https](https://ehcache.org/) result 200). * [ ] http://www.embeddedjs.com/ with 2 occurrences migrated to: https://www.embeddedjs.com/ ([https](https://www.embeddedjs.com/) result 200). * [ ] http://www.enterpriseintegrationpatterns.com with 1 occurrences migrated to: https://www.enterpriseintegrationpatterns.com ([https](https://www.enterpriseintegrationpatterns.com) result 200). * [ ] http://www.example.org/?param=aGVsbG9Xb3JsZA%3D%3D with 1 occurrences migrated to: https://www.example.org/?param=aGVsbG9Xb3JsZA%3D%3D ([https](https://www.example.org/?param=aGVsbG9Xb3JsZA%3D%3D) result 200). * [ ] http://www.google.com with 2 occurrences migrated to: https://www.google.com ([https](https://www.google.com) result 200). * [ ] http://www.h2database.com with 1 occurrences migrated to: https://www.h2database.com ([https](https://www.h2database.com) result 200). * [ ] http://www.h2database.com/html/grammar.html with 1 occurrences migrated to: https://www.h2database.com/html/grammar.html ([https](https://www.h2database.com/html/grammar.html) result 200). * [ ] http://www.ietf.org with 1 occurrences migrated to: https://www.ietf.org ([https](https://www.ietf.org) result 200). * [ ] http://www.ietf.org/rfc/rfc1867.txt with 1 occurrences migrated to: https://www.ietf.org/rfc/rfc1867.txt ([https](https://www.ietf.org/rfc/rfc1867.txt) result 200). * [ ] http://www.ietf.org/rfc/rfc2396.txt with 2 occurrences migrated to: https://www.ietf.org/rfc/rfc2396.txt ([https](https://www.ietf.org/rfc/rfc2396.txt) result 200). * [ ] http://www.ietf.org/rfc/rfc3986.txt with 11 occurrences migrated to: https://www.ietf.org/rfc/rfc3986.txt ([https](https://www.ietf.org/rfc/rfc3986.txt) result 200). * [ ] http://www.infoq.com/ with 1 occurrences migrated to: https://www.infoq.com/ ([https](https://www.infoq.com/) result 200). * [ ] http://www.infoq.com/minibooks/JTDS with 1 occurrences migrated to: https://www.infoq.com/minibooks/JTDS ([https](https://www.infoq.com/minibooks/JTDS) result 200). * [ ] http://www.jetbrains.com/idea/ with 1 occurrences migrated to: https://www.jetbrains.com/idea/ ([https](https://www.jetbrains.com/idea/) result 200). * [ ] http://www.json.org/ with 1 occurrences migrated to: https://www.json.org/ ([https](https://www.json.org/) result 200). * [ ] http://www.jython.org with 2 occurrences migrated to: https://www.jython.org ([https](https://www.jython.org) result 200). * [ ] http://www.jython.org/ with 2 occurrences migrated to: https://www.jython.org/ ([https](https://www.jython.org/) result 200). * [ ] http://www.lowagie.com/iText with 2 occurrences migrated to: https://www.lowagie.com/iText ([https](https://www.lowagie.com/iText) result 200). * [ ] http://www.manpagez.com/man/5/crontab/ with 1 occurrences migrated to: https://www.manpagez.com/man/5/crontab/ ([https](https://www.manpagez.com/man/5/crontab/) result 200). * [ ] http://mydomain1.com:80/ (301) with 2 occurrences migrated to: https://www.mydomain1.com/ ([https](https://mydomain1.com:80/) result 200). * [ ] http://mydomain1.com:80 (301) with 1 occurrences migrated to: https://www.mydomain1.com/ ([https](https://mydomain1.com:80) result 200). * [ ] http://www.oracle.com/technetwork/articles/javaee/jpa-137156.html with 1 occurrences migrated to: https://www.oracle.com/technetwork/articles/javaee/jpa-137156.html ([https](https://www.oracle.com/technetwork/articles/javaee/jpa-137156.html) result 200). * [ ] http://www.oracle.com/technetwork/java/javaee/javaserverfaces-139869.html with 1 occurrences migrated to: https://www.oracle.com/technetwork/java/javaee/javaserverfaces-139869.html ([https](https://www.oracle.com/technetwork/java/javaee/javaserverfaces-139869.html) result 200). * [ ] http://www.oracle.com/technetwork/java/javase/documentation/spec-136004.html with 1 occurrences migrated to: https://www.oracle.com/technetwork/java/javase/documentation/spec-136004.html ([https](https://www.oracle.com/technetwork/java/javase/documentation/spec-136004.html) result 200). * [ ] http://www.oracle.com/technetwork/java/javase/tech/download-jsp-141676.html with 1 occurrences migrated to: https://www.oracle.com/technetwork/java/javase/tech/download-jsp-141676.html ([https](https://www.oracle.com/technetwork/java/javase/tech/download-jsp-141676.html) result 200). * [ ] http://www.oracle.com/technetwork/java/javase/tech/javamanagement-140525.html with 1 occurrences migrated to: https://www.oracle.com/technetwork/java/javase/tech/javamanagement-140525.html ([https](https://www.oracle.com/technetwork/java/javase/tech/javamanagement-140525.html) result 200). * [ ] http://www.oracle.com/technetwork/middleware/weblogic/overview/index-085209.html with 1 occurrences migrated to: https://www.oracle.com/technetwork/middleware/weblogic/overview/index-085209.html ([https](https://www.oracle.com/technetwork/middleware/weblogic/overview/index-085209.html) result 200). * [ ] http://www.ow2.org/ with 1 occurrences migrated to: https://www.ow2.org/ ([https](https://www.ow2.org/) result 200). * [ ] http://www.owasp.org/index.php/HTTPOnly with 4 occurrences migrated to: https://www.owasp.org/index.php/HTTPOnly ([https](https://www.owasp.org/index.php/HTTPOnly) result 200). * [ ] http://quartz-scheduler.org (301) with 3 occurrences migrated to: https://www.quartz-scheduler.org/ ([https](https://quartz-scheduler.org) result 200). * [ ] http://www.rabbitmq.com/stomp.html with 1 occurrences migrated to: https://www.rabbitmq.com/stomp.html ([https](https://www.rabbitmq.com/stomp.html) result 200). * [ ] http://www.reactive-streams.org with 1 occurrences migrated to: https://www.reactive-streams.org ([https](https://www.reactive-streams.org) result 200). * [ ] http://www.reactive-streams.org/ with 2 occurrences migrated to: https://www.reactive-streams.org/ ([https](https://www.reactive-streams.org/) result 200). * [ ] http://www.reactive-streams.org/reactive-streams-1.0.1-javadoc/org/reactivestreams/Publisher.html with 1 occurrences migrated to: https://www.reactive-streams.org/reactive-streams-1.0.1-javadoc/org/reactivestreams/Publisher.html ([https](https://www.reactive-streams.org/reactive-streams-1.0.1-javadoc/org/reactivestreams/Publisher.html) result 200). * [ ] http://www.reactive-streams.org/reactive-streams-1.0.1-javadoc/org/reactivestreams/Subscriber.html with 1 occurrences migrated to: https://www.reactive-streams.org/reactive-streams-1.0.1-javadoc/org/reactivestreams/Subscriber.html ([https](https://www.reactive-streams.org/reactive-streams-1.0.1-javadoc/org/reactivestreams/Subscriber.html) result 200). * [ ] http://seleniumhq.org/projects/webdriver/ (301) with 1 occurrences migrated to: https://www.seleniumhq.org ([https](https://seleniumhq.org/projects/webdriver/) result 200). * [ ] http://www.springbyexample.org/examples/custom-thread-scope-module.html with 1 occurrences migrated to: https://www.springbyexample.org/examples/custom-thread-scope-module.html ([https](https://www.springbyexample.org/examples/custom-thread-scope-module.html) result 200). * [ ] http://www.springframework.org/dtd/spring-beans-2.0.dtd with 3 occurrences migrated to: https://www.springframework.org/dtd/spring-beans-2.0.dtd ([https](https://www.springframework.org/dtd/spring-beans-2.0.dtd) result 200). * [ ] http://www.springframework.org/schema/aop/spring-aop.xsd with 12 occurrences migrated to: https://www.springframework.org/schema/aop/spring-aop.xsd ([https](https://www.springframework.org/schema/aop/spring-aop.xsd) result 200). * [ ] http://www.springframework.org/schema/beans/spring-beans.xsd with 79 occurrences migrated to: https://www.springframework.org/schema/beans/spring-beans.xsd ([https](https://www.springframework.org/schema/beans/spring-beans.xsd) result 200). * [ ] http://www.springframework.org/schema/cache/spring-cache.xsd with 2 occurrences migrated to: https://www.springframework.org/schema/cache/spring-cache.xsd ([https](https://www.springframework.org/schema/cache/spring-cache.xsd) result 200). * [ ] http://www.springframework.org/schema/context/spring-context.xsd with 13 occurrences migrated to: https://www.springframework.org/schema/context/spring-context.xsd ([https](https://www.springframework.org/schema/context/spring-context.xsd) result 200). * [ ] http://www.springframework.org/schema/jdbc/spring-jdbc.xsd with 1 occurrences migrated to: https://www.springframework.org/schema/jdbc/spring-jdbc.xsd ([https](https://www.springframework.org/schema/jdbc/spring-jdbc.xsd) result 200). * [ ] http://www.springframework.org/schema/jee/spring-jee.xsd with 2 occurrences migrated to: https://www.springframework.org/schema/jee/spring-jee.xsd ([https](https://www.springframework.org/schema/jee/spring-jee.xsd) result 200). * [ ] http://www.springframework.org/schema/jms/spring-jms.xsd with 2 occurrences migrated to: https://www.springframework.org/schema/jms/spring-jms.xsd ([https](https://www.springframework.org/schema/jms/spring-jms.xsd) result 200). * [ ] http://www.springframework.org/schema/lang/spring-lang.xsd with 4 occurrences migrated to: https://www.springframework.org/schema/lang/spring-lang.xsd ([https](https://www.springframework.org/schema/lang/spring-lang.xsd) result 200). * [ ] http://www.springframework.org/schema/mvc/spring-mvc.xsd with 3 occurrences migrated to: https://www.springframework.org/schema/mvc/spring-mvc.xsd ([https](https://www.springframework.org/schema/mvc/spring-mvc.xsd) result 200). * [ ] http://www.springframework.org/schema/oxm/spring-oxm.xsd with 1 occurrences migrated to: https://www.springframework.org/schema/oxm/spring-oxm.xsd ([https](https://www.springframework.org/schema/oxm/spring-oxm.xsd) result 200). * [ ] http://www.springframework.org/schema/tool/spring-tool.xsd with 13 occurrences migrated to: https://www.springframework.org/schema/tool/spring-tool.xsd ([https](https://www.springframework.org/schema/tool/spring-tool.xsd) result 200). * [ ] http://www.springframework.org/schema/tx/spring-tx.xsd with 7 occurrences migrated to: https://www.springframework.org/schema/tx/spring-tx.xsd ([https](https://www.springframework.org/schema/tx/spring-tx.xsd) result 200). * [ ] http://www.springframework.org/schema/util/spring-util.xsd with 1 occurrences migrated to: https://www.springframework.org/schema/util/spring-util.xsd ([https](https://www.springframework.org/schema/util/spring-util.xsd) result 200). * [ ] http://www.springframework.org/schema/websocket/spring-websocket.xsd with 11 occurrences migrated to: https://www.springframework.org/schema/websocket/spring-websocket.xsd ([https](https://www.springframework.org/schema/websocket/spring-websocket.xsd) result 200). * [ ] http://www.stuartellis.eu/articles/erb/ (301) with 2 occurrences migrated to: https://www.stuartellis.name/articles/erb/ ([https](https://www.stuartellis.eu/articles/erb/) result 200). * [ ] http://www.thymeleaf.org/ with 2 occurrences migrated to: https://www.thymeleaf.org/ ([https](https://www.thymeleaf.org/) result 200). * [ ] http://www.thymeleaf.org/documentation.html with 2 occurrences migrated to: https://www.thymeleaf.org/documentation.html ([https](https://www.thymeleaf.org/documentation.html) result 200). * [ ] http://www.w3.org/2000/xmlns/ with 5 occurrences migrated to: https://www.w3.org/2000/xmlns/ ([https](https://www.w3.org/2000/xmlns/) result 200). * [ ] http://www.w3.org/2004/08/xop/include with 2 occurrences migrated to: https://www.w3.org/2004/08/xop/include ([https](https://www.w3.org/2004/08/xop/include) result 200). * [ ] http://www.w3.org/DesignIssues/MatrixURIs.html with 2 occurrences migrated to: https://www.w3.org/DesignIssues/MatrixURIs.html ([https](https://www.w3.org/DesignIssues/MatrixURIs.html) result 200). * [ ] http://www.w3.org/TR/2004/WD-soap12-mtom-20040608/ with 2 occurrences migrated to: https://www.w3.org/TR/2004/WD-soap12-mtom-20040608/ ([https](https://www.w3.org/TR/2004/WD-soap12-mtom-20040608/) result 200). * [ ] http://www.w3.org/TR/2005/REC-xop10-20050125/ with 6 occurrences migrated to: https://www.w3.org/TR/2005/REC-xop10-20050125/ ([https](https://www.w3.org/TR/2005/REC-xop10-20050125/) result 200). * [ ] http://www.w3.org/TR/cors/ with 19 occurrences migrated to: https://www.w3.org/TR/cors/ ([https](https://www.w3.org/TR/cors/) result 200). * [ ] http://www.w3.org/TR/eventsource/ with 2 occurrences migrated to: https://www.w3.org/TR/eventsource/ ([https](https://www.w3.org/TR/eventsource/) result 200). * [ ] http://www.w3.org/TR/xhtml1/ with 1 occurrences migrated to: https://www.w3.org/TR/xhtml1/ ([https](https://www.w3.org/TR/xhtml1/) result 200). * [ ] http://www.w3.org/TR/xhtml1/diffs.html with 1 occurrences migrated to: https://www.w3.org/TR/xhtml1/diffs.html ([https](https://www.w3.org/TR/xhtml1/diffs.html) result 200). * [ ] http://www.w3.org/TR/xhtml1/dtds.html with 1 occurrences migrated to: https://www.w3.org/TR/xhtml1/dtds.html ([https](https://www.w3.org/TR/xhtml1/dtds.html) result 200). * [ ] http://www.webjars.org with 4 occurrences migrated to: https://www.webjars.org ([https](https://www.webjars.org) result 200). * [ ] http://www.webjars.org/ with 2 occurrences migrated to: https://www.webjars.org/ ([https](https://www.webjars.org/) result 200). * [ ] http://www.webjars.org/documentation with 2 occurrences migrated to: https://www.webjars.org/documentation ([https](https://www.webjars.org/documentation) result 200). * [ ] http://www.wildfly.org/ with 1 occurrences migrated to: https://www.wildfly.org/ ([https](https://www.wildfly.org/) result 200). * [ ] http://x-stream.github.io/ with 2 occurrences migrated to: https://x-stream.github.io/ ([https](https://x-stream.github.io/) result 200). * [ ] http://youtrack.jetbrains.com/issue/IDEA-53476 with 1 occurrences migrated to: https://youtrack.jetbrains.com/issue/IDEA-53476 ([https](https://youtrack.jetbrains.com/issue/IDEA-53476) result 200). * [ ] http://youtrack.jetbrains.com/issue/IDEA-64446 with 1 occurrences migrated to: https://youtrack.jetbrains.com/issue/IDEA-64446 ([https](https://youtrack.jetbrains.com/issue/IDEA-64446) result 200). * [ ] http://asm.ow2.org with 1 occurrences migrated to: https://asm.ow2.org ([https](https://asm.ow2.org) result 301). * [ ] http://bit.ly/TRlX2r with 1 occurrences migrated to: https://bit.ly/TRlX2r ([https](https://bit.ly/TRlX2r) result 301). * [ ] http://bit.ly/UzccDt with 1 occurrences migrated to: https://bit.ly/UzccDt ([https](https://bit.ly/UzccDt) result 301). * [ ] http://bit.ly/p9rIvx with 1 occurrences migrated to: https://bit.ly/p9rIvx ([https](https://bit.ly/p9rIvx) result 301). * [ ] http://bit.ly/qUwvwz with 1 occurrences migrated to: https://bit.ly/qUwvwz ([https](https://bit.ly/qUwvwz) result 301). * [ ] http://blogs.msdn.com/b/ieinternals/archive/2010/05/13/xdomainrequest-restrictions-limitations-and-workarounds.aspx with 1 occurrences migrated to: https://blogs.msdn.com/b/ieinternals/archive/2010/05/13/xdomainrequest-restrictions-limitations-and-workarounds.aspx ([https](https://blogs.msdn.com/b/ieinternals/archive/2010/05/13/xdomainrequest-restrictions-limitations-and-workarounds.aspx) result 301). * [ ] http://castor-data-binding.github.io/castor with 1 occurrences migrated to: https://castor-data-binding.github.io/castor ([https](https://castor-data-binding.github.io/castor) result 301). * [ ] http://code.google.com/p/beanshell2/ with 1 occurrences migrated to: https://code.google.com/p/beanshell2/ ([https](https://code.google.com/p/beanshell2/) result 301). * [ ] http://code.google.com/p/browsersec/wiki/Part2 with 1 occurrences migrated to: https://code.google.com/p/browsersec/wiki/Part2 ([https](https://code.google.com/p/browsersec/wiki/Part2) result 301). * [ ] http://code.google.com/p/gears/wiki/ResumableHttpRequestsProposal with 1 occurrences migrated to: https://code.google.com/p/gears/wiki/ResumableHttpRequestsProposal ([https](https://code.google.com/p/gears/wiki/ResumableHttpRequestsProposal) result 301). * [ ] http://code.google.com/p/xml-matchers/ with 3 occurrences migrated to: https://code.google.com/p/xml-matchers/ ([https](https://code.google.com/p/xml-matchers/) result 301). * [ ] http://commons.apache.org/proper/commons-dbcp with 2 occurrences migrated to: https://commons.apache.org/proper/commons-dbcp ([https](https://commons.apache.org/proper/commons-dbcp) result 301). * [ ] http://commons.apache.org/proper/commons-fileupload with 2 occurrences migrated to: https://commons.apache.org/proper/commons-fileupload ([https](https://commons.apache.org/proper/commons-fileupload) result 301). * [ ] http://confluence.jetbrains.net/display/IDEADEV/Gradle+integration with 1 occurrences migrated to: https://confluence.jetbrains.net/display/IDEADEV/Gradle+integration ([https](https://confluence.jetbrains.net/display/IDEADEV/Gradle+integration) result 301). * [ ] http://contributor-covenant.org with 1 occurrences migrated to: https://contributor-covenant.org ([https](https://contributor-covenant.org) result 301). * [ ] http://contributor-covenant.org/version/1/3/0/ with 1 occurrences migrated to: https://contributor-covenant.org/version/1/3/0/ ([https](https://contributor-covenant.org/version/1/3/0/) result 301). * [ ] http://db.apache.org/derby with 2 occurrences migrated to: https://db.apache.org/derby ([https](https://db.apache.org/derby) result 301). * [ ] http://dev.w3.org/html5/eventsource/ with 2 occurrences migrated to: https://dev.w3.org/html5/eventsource/ ([https](https://dev.w3.org/html5/eventsource/) result 301). * [ ] http://domain.com with 24 occurrences migrated to: https://domain.com ([https](https://domain.com) result 301). * [ ] http://domain.com/ with 1 occurrences migrated to: https://domain.com/ ([https](https://domain.com/) result 301). * [ ] http://en.wikipedia.org/wiki/Cache_ with 1 occurrences migrated to: https://en.wikipedia.org/wiki/Cache_ ([https](https://en.wikipedia.org/wiki/Cache_) result 301). * [ ] http://facebook.github.io/react/ with 2 occurrences migrated to: https://facebook.github.io/react/ ([https](https://facebook.github.io/react/) result 301). * [ ] http://forum.springframework.org/showthread.php?t=41350 with 1 occurrences migrated to: https://forum.springframework.org/showthread.php?t=41350 ([https](https://forum.springframework.org/showthread.php?t=41350) result 301). * [ ] http://github.com/SpringSource/spring-framework with 1 occurrences migrated to: https://github.com/SpringSource/spring-framework ([https](https://github.com/SpringSource/spring-framework) result 301). * [ ] http://hdiv.org/ with 1 occurrences migrated to: https://hdiv.org/ ([https](https://hdiv.org/) result 301). * [ ] http://jakarta.apache.org/commons/fileupload with 1 occurrences migrated to: https://jakarta.apache.org/commons/fileupload ([https](https://jakarta.apache.org/commons/fileupload) result 301). * [ ] http://jira.springframework.org/browse/SPR-5708 with 1 occurrences migrated to: https://jira.springframework.org/browse/SPR-5708 ([https](https://jira.springframework.org/browse/SPR-5708) result 301). * [ ] http://jira.springframework.org/browse/SPR-6124 with 1 occurrences migrated to: https://jira.springframework.org/browse/SPR-6124 ([https](https://jira.springframework.org/browse/SPR-6124) result 301). * [ ] http://jira.springframework.org/browse/SPR-6128 with 1 occurrences migrated to: https://jira.springframework.org/browse/SPR-6128 ([https](https://jira.springframework.org/browse/SPR-6128) result 301). * [ ] http://jira.springframework.org/browse/SPR-7064 with 1 occurrences migrated to: https://jira.springframework.org/browse/SPR-7064 ([https](https://jira.springframework.org/browse/SPR-7064) result 301). * [ ] http://maven.apache.org/plugins/maven-surefire-plugin/test-mojo.html with 1 occurrences migrated to: https://maven.apache.org/plugins/maven-surefire-plugin/test-mojo.html ([https](https://maven.apache.org/plugins/maven-surefire-plugin/test-mojo.html) result 301). * [ ] http://mockito.org/ (301) with 1 occurrences migrated to: https://mockito.github.io ([https](https://mockito.org/) result 301). * [ ] http://mydomain1.com with 36 occurrences migrated to: https://mydomain1.com ([https](https://mydomain1.com) result 301). * [ ] http://mydomain1.com/ with 2 occurrences migrated to: https://mydomain1.com/ ([https](https://mydomain1.com/) result 301). * [ ] http://mydomain1.com/path with 2 occurrences migrated to: https://mydomain1.com/path ([https](https://mydomain1.com/path) result 301). * [ ] http://opensource.atlassian.com/projects/spring/browse/SPR-2660 with 1 occurrences migrated to: https://opensource.atlassian.com/projects/spring/browse/SPR-2660 ([https](https://opensource.atlassian.com/projects/spring/browse/SPR-2660) result 301). * [ ] http://opensource.atlassian.com/projects/spring/browse/SPR-2754 with 1 occurrences migrated to: https://opensource.atlassian.com/projects/spring/browse/SPR-2754 ([https](https://opensource.atlassian.com/projects/spring/browse/SPR-2754) result 301). * [ ] http://opensource.atlassian.com/projects/spring/browse/SPR-2789 with 1 occurrences migrated to: https://opensource.atlassian.com/projects/spring/browse/SPR-2789 ([https](https://opensource.atlassian.com/projects/spring/browse/SPR-2789) result 301). * [ ] http://opensource.atlassian.com/projects/spring/browse/SPR-3127 with 1 occurrences migrated to: https://opensource.atlassian.com/projects/spring/browse/SPR-3127 ([https](https://opensource.atlassian.com/projects/spring/browse/SPR-3127) result 301). * [ ] http://opensource.atlassian.com/projects/spring/browse/SPR-3775 with 1 occurrences migrated to: https://opensource.atlassian.com/projects/spring/browse/SPR-3775 ([https](https://opensource.atlassian.com/projects/spring/browse/SPR-3775) result 301). * [ ] http://opensource.atlassian.com/projects/spring/browse/SPR-3880 with 1 occurrences migrated to: https://opensource.atlassian.com/projects/spring/browse/SPR-3880 ([https](https://opensource.atlassian.com/projects/spring/browse/SPR-3880) result 301). * [ ] http://opensource.atlassian.com/projects/spring/browse/SPR-3896 with 7 occurrences migrated to: https://opensource.atlassian.com/projects/spring/browse/SPR-3896 ([https](https://opensource.atlassian.com/projects/spring/browse/SPR-3896) result 301). * [ ] http://opensource.atlassian.com/projects/spring/browse/SPR-3949 with 1 occurrences migrated to: https://opensource.atlassian.com/projects/spring/browse/SPR-3949 ([https](https://opensource.atlassian.com/projects/spring/browse/SPR-3949) result 301). * [ ] http://opensource.atlassian.com/projects/spring/browse/SPR-4008 with 2 occurrences migrated to: https://opensource.atlassian.com/projects/spring/browse/SPR-4008 ([https](https://opensource.atlassian.com/projects/spring/browse/SPR-4008) result 301). * [ ] http://opensource.atlassian.com/projects/spring/browse/SPR-4040 with 2 occurrences migrated to: https://opensource.atlassian.com/projects/spring/browse/SPR-4040 ([https](https://opensource.atlassian.com/projects/spring/browse/SPR-4040) result 301). * [ ] http://projects.spring.io/spring-webflow with 1 occurrences migrated to: https://projects.spring.io/spring-webflow ([https](https://projects.spring.io/spring-webflow) result 301). * [ ] http://publib.boulder.ibm.com/infocenter/wasinfo/v5r1//topic/com.ibm.websphere.base.doc/info/aes/ae/rdat_extiapi.html with 1 occurrences migrated to: https://publib.boulder.ibm.com/infocenter/wasinfo/v5r1//topic/com.ibm.websphere.base.doc/info/aes/ae/rdat_extiapi.html ([https](https://publib.boulder.ibm.com/infocenter/wasinfo/v5r1//topic/com.ibm.websphere.base.doc/info/aes/ae/rdat_extiapi.html) result 301). * [ ] http://publib.boulder.ibm.com/infocenter/wasinfo/v6r0/topic/com.ibm.websphere.javadoc.doc/public_html/api/com/ibm/websphere/rsadapter/JDBCConnectionSpec.html with 1 occurrences migrated to: https://publib.boulder.ibm.com/infocenter/wasinfo/v6r0/topic/com.ibm.websphere.javadoc.doc/public_html/api/com/ibm/websphere/rsadapter/JDBCConnectionSpec.html ([https](https://publib.boulder.ibm.com/infocenter/wasinfo/v6r0/topic/com.ibm.websphere.javadoc.doc/public_html/api/com/ibm/websphere/rsadapter/JDBCConnectionSpec.html) result 301). * [ ] http://site2.com with 3 occurrences migrated to: https://site2.com ([https](https://site2.com) result 301). * [ ] http://slack.kotlinlang.org/ with 2 occurrences migrated to: https://slack.kotlinlang.org/ ([https](https://slack.kotlinlang.org/) result 301). * [ ] http://sourceforge.net/projects/c3p0 with 2 occurrences migrated to: https://sourceforge.net/projects/c3p0 ([https](https://sourceforge.net/projects/c3p0) result 301). * [ ] http://sourceforge.net/projects/javadiff with 1 occurrences migrated to: https://sourceforge.net/projects/javadiff ([https](https://sourceforge.net/projects/javadiff) result 301). * [ ] http://springframework.org with 2 occurrences migrated to: https://springframework.org ([https](https://springframework.org) result 301). * [ ] http://www-01.ibm.com/software/webservers/appserv/was/ with 1 occurrences migrated to: https://www-01.ibm.com/software/webservers/appserv/was/ ([https](https://www-01.ibm.com/software/webservers/appserv/was/) result 301). * [ ] http://www.atomenabled.org/developers/syndication/ with 1 occurrences migrated to: https://www.atomenabled.org/developers/syndication/ ([https](https://www.atomenabled.org/developers/syndication/) result 301). * [ ] http://www.eclipse.org/aspectj with 2 occurrences migrated to: https://www.eclipse.org/aspectj ([https](https://www.eclipse.org/aspectj) result 301). * [ ] http://www.google.com/ig/calculator?q=1USD=?EUR with 1 occurrences migrated to: https://www.google.com/ig/calculator?q=1USD=?EUR ([https](https://www.google.com/ig/calculator?q=1USD=?EUR) result 301). * [ ] http://www.javaworld.com/javaworld/jw-01-2009/jw-01-spring-transactions.html with 1 occurrences migrated to: https://www.javaworld.com/javaworld/jw-01-2009/jw-01-spring-transactions.html ([https](https://www.javaworld.com/javaworld/jw-01-2009/jw-01-spring-transactions.html) result 301). * [ ] http://www.jboss.org/jbossas/ with 1 occurrences migrated to: https://www.jboss.org/jbossas/ ([https](https://www.jboss.org/jbossas/) result 301). * [ ] http://www.junit.org/ with 1 occurrences migrated to: https://www.junit.org/ ([https](https://www.junit.org/) result 301). * [ ] http://www.mnot.net/cache_docs with 1 occurrences migrated to: https://www.mnot.net/cache_docs ([https](https://www.mnot.net/cache_docs) result 301). * [ ] http://www.sf.net/home.view?siteLanguage=nl with 1 occurrences migrated to: https://www.sf.net/home.view?siteLanguage=nl ([https](https://www.sf.net/home.view?siteLanguage=nl) result 301). * [ ] http://www.springframework.org with 7 occurrences migrated to: https://www.springframework.org ([https](https://www.springframework.org) result 301). * [ ] http://www.springframework.org/ with 3 occurrences migrated to: https://www.springframework.org/ ([https](https://www.springframework.org/) result 301). * [ ] http://xyz.com/path with 2 occurrences migrated to: https://xyz.com/path ([https](https://xyz.com/path) result 301). * [ ] http://commons.apache.org/codec/ with 1 occurrences migrated to: https://commons.apache.org/codec/ ([https](https://commons.apache.org/codec/) result 302). * [ ] http://commons.apache.org/logging with 2 occurrences migrated to: https://commons.apache.org/logging ([https](https://commons.apache.org/logging) result 302). * [ ] http://download.oracle.com/javaee/6/api/javax/inject/Scope.html with 1 occurrences migrated to: https://download.oracle.com/javaee/6/api/javax/inject/Scope.html ([https](https://download.oracle.com/javaee/6/api/javax/inject/Scope.html) result 302). * [ ] http://download.oracle.com/javase/1.5.0/docs/api/java/lang/System.html with 4 occurrences migrated to: https://download.oracle.com/javase/1.5.0/docs/api/java/lang/System.html ([https](https://download.oracle.com/javase/1.5.0/docs/api/java/lang/System.html) result 302). * [ ] http://download.oracle.com/javase/6/docs/api/java/beans/ConstructorProperties.html with 1 occurrences migrated to: https://download.oracle.com/javase/6/docs/api/java/beans/ConstructorProperties.html ([https](https://download.oracle.com/javase/6/docs/api/java/beans/ConstructorProperties.html) result 302). * [ ] http://download.oracle.com/javase/6/docs/technotes/guides/jar/jar.html with 1 occurrences migrated to: https://download.oracle.com/javase/6/docs/technotes/guides/jar/jar.html ([https](https://download.oracle.com/javase/6/docs/technotes/guides/jar/jar.html) result 302). * [ ] http://download.oracle.com/javase/jndi/tutorial/beyond/misc/policy.html with 1 occurrences migrated to: https://download.oracle.com/javase/jndi/tutorial/beyond/misc/policy.html ([https](https://download.oracle.com/javase/jndi/tutorial/beyond/misc/policy.html) result 302). * [ ] http://gafter.blogspot.nl/2006/12/super-type-tokens.html with 1 occurrences migrated to: https://gafter.blogspot.nl/2006/12/super-type-tokens.html ([https](https://gafter.blogspot.nl/2006/12/super-type-tokens.html) result 302). * [ ] http://git-scm.com/book/en/Distributed-Git-Contributing-to-a-Project with 1 occurrences migrated to: https://git-scm.com/book/en/Distributed-Git-Contributing-to-a-Project ([https](https://git-scm.com/book/en/Distributed-Git-Contributing-to-a-Project) result 302). * [ ] http://git-scm.com/book/en/Git-Tools-Rewriting-History with 1 occurrences migrated to: https://git-scm.com/book/en/Git-Tools-Rewriting-History ([https](https://git-scm.com/book/en/Git-Tools-Rewriting-History) result 302). * [ ] http://glassfish.dev.java.net/ with 1 occurrences migrated to: https://glassfish.dev.java.net/ ([https](https://glassfish.dev.java.net/) result 302). * [ ] http://java.sun.com with 3 occurrences migrated to: https://java.sun.com ([https](https://java.sun.com) result 302). * [ ] http://java.sun.com/docs/books/jls/third_edition/html/conversions.html with 1 occurrences migrated to: https://java.sun.com/docs/books/jls/third_edition/html/conversions.html ([https](https://java.sun.com/docs/books/jls/third_edition/html/conversions.html) result 302). * [ ] http://java.sun.com/docs/books/jls/third_edition/html/expressions.html with 1 occurrences migrated to: https://java.sun.com/docs/books/jls/third_edition/html/expressions.html ([https](https://java.sun.com/docs/books/jls/third_edition/html/expressions.html) result 302). * [ ] http://java.sun.com/j2se/ with 2 occurrences migrated to: https://java.sun.com/j2se/ ([https](https://java.sun.com/j2se/) result 302). * [ ] http://java.sun.com/j2se/1.4.1/docs/tooldocs/solaris/javadoc.html with 1 occurrences migrated to: https://java.sun.com/j2se/1.4.1/docs/tooldocs/solaris/javadoc.html ([https](https://java.sun.com/j2se/1.4.1/docs/tooldocs/solaris/javadoc.html) result 302). * [ ] http://java.sun.com/j2se/1.4.2/docs/api/java/text/SimpleDateFormat.html with 1 occurrences migrated to: https://java.sun.com/j2se/1.4.2/docs/api/java/text/SimpleDateFormat.html ([https](https://java.sun.com/j2se/1.4.2/docs/api/java/text/SimpleDateFormat.html) result 302). * [ ] http://java.sun.com/j2se/javadoc with 1 occurrences migrated to: https://java.sun.com/j2se/javadoc ([https](https://java.sun.com/j2se/javadoc) result 302). * [ ] http://java.sun.com/j2se/javadoc/ with 1 occurrences migrated to: https://java.sun.com/j2se/javadoc/ ([https](https://java.sun.com/j2se/javadoc/) result 302). * [ ] http://java.sun.com/j2se/javadoc/faq.html with 1 occurrences migrated to: https://java.sun.com/j2se/javadoc/faq.html ([https](https://java.sun.com/j2se/javadoc/faq.html) result 302). * [ ] http://java.sun.com/webservices/jaxb/ with 1 occurrences migrated to: https://java.sun.com/webservices/jaxb/ ([https](https://java.sun.com/webservices/jaxb/) result 302). * [ ] http://java.sun.com/xml/ns/j2ee/connector_1_5.xsd with 1 occurrences migrated to: https://java.sun.com/xml/ns/j2ee/connector_1_5.xsd ([https](https://java.sun.com/xml/ns/j2ee/connector_1_5.xsd) result 302). * [ ] http://java.sun.com/xml/ns/j2ee/web-jsptaglibrary_2_0.xsd with 2 occurrences migrated to: https://java.sun.com/xml/ns/j2ee/web-jsptaglibrary_2_0.xsd ([https](https://java.sun.com/xml/ns/j2ee/web-jsptaglibrary_2_0.xsd) result 302). * [ ] http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd with 2 occurrences migrated to: https://java.sun.com/xml/ns/javaee/web-app_3_0.xsd ([https](https://java.sun.com/xml/ns/javaee/web-app_3_0.xsd) result 302). * [ ] http://sourceforge.net/projects/cglib/files/cglib3/3.0/cglib-3.0.jar/download with 1 occurrences migrated to: https://sourceforge.net/projects/cglib/files/cglib3/3.0/cglib-3.0.jar/download ([https](https://sourceforge.net/projects/cglib/files/cglib3/3.0/cglib-3.0.jar/download) result 302). * [ ] http://other.info/foo/bar (301) with 1 occurrences migrated to: https://weibo.com/otherinfo/foo/bar ([https](https://other.info/foo/bar) result 302). * [ ] http://www.iana.org/assignments/http-status-codes with 1 occurrences migrated to: https://www.iana.org/assignments/http-status-codes ([https](https://www.iana.org/assignments/http-status-codes) result 302). * [ ] http://www.iana.org/assignments/media-types/ with 1 occurrences migrated to: https://www.iana.org/assignments/media-types/ ([https](https://www.iana.org/assignments/media-types/) result 302). * [ ] http://www.sys-con.com/java with 1 occurrences migrated to: https://www.sys-con.com/java ([https](https://www.sys-con.com/java) result 302). These URLs were intentionally ignored. * http://Additional-namespace-name-URI with 1 occurrences * http://Default-namespace-name-URI with 1 occurrences * http://HOST:8080/remoting/AccountService with 1 occurrences * http://Namespace-name-URI with 1 occurrences * http://Unbound-namespace-name-URI with 1 occurrences * http://anotherHost/mvc-showcase with 1 occurrences * http://apache.org/xml/features/disallow-doctype-decl with 14 occurrences * http://barfoo:8888 with 1 occurrences * http://example/ with 1 occurrences * http://fake-service with 7 occurrences * http://foo with 3 occurrences * http://foobar:8088 with 1 occurrences * http://host:port/myApp/myEndpoint/ with 1 occurrences * http://java.sun.com/jsp/jstl/core with 1 occurrences * http://java.sun.com/xml/jaxp/properties/schemaLanguage with 1 occurrences * http://java.sun.com/xml/ns/j2ee with 6 occurrences * http://java.sun.com/xml/ns/javaee with 4 occurrences * http://java.sun.com/xml/ns/persistence with 1 occurrences * http://java.sun.com/xml/stream/properties/report-cdata-event with 1 occurrences * http://jaxws.remoting.springframework.org/ with 4 occurrences * http://localhost with 81 occurrences * http://localhost/ with 14 occurrences * http://localhost/42 with 1 occurrences * http://localhost/?cookie=foo with 1 occurrences * http://localhost/FILE.TXT with 1 occurrences * http://localhost/abc with 1 occurrences * http://localhost/abc/def with 1 occurrences * http://localhost/app%20/path/ with 1 occurrences * http://localhost/app/path%20with%20spaces/ with 1 occurrences * http://localhost/bar with 1 occurrences * http://localhost/context/a with 3 occurrences * http://localhost/contextPath/main/path with 1 occurrences * http://localhost/file.foo with 1 occurrences * http://localhost/file.txt with 1 occurrences * http://localhost/foo%20bar with 1 occurrences * http://localhost/foo/bar with 1 occurrences * http://localhost/forward with 1 occurrences * http://localhost/hotels/42/bookings/21 with 4 occurrences * http://localhost/input with 1 occurrences * http://localhost/jquery-1.11.0.min.js with 5 occurrences * http://localhost/messages/form with 1 occurrences * http://localhost/mvc-showcase with 2 occurrences * http://localhost/mvc-showcase/app with 1 occurrences * http://localhost/mvc-showcase/data/foo%20bar with 1 occurrences * http://localhost/mvc-showcase/data/param with 1 occurrences * http://localhost/mvc-showcase/data/param?foo=123 with 2 occurrences * http://localhost/myapp/main with 1 occurrences * http://localhost/myapp/people/1/addresses/DE with 1 occurrences * http://localhost/path with 3 occurrences * http://localhost/path;a=b/with/semicolon with 1 occurrences * http://localhost/persons with 1 occurrences * http://localhost/prefix with 1 occurrences * http://localhost/prefix/app with 1 occurrences * http://localhost/prefix/bar with 1 occurrences * http://localhost/prefix/mvc-showcase with 3 occurrences * http://localhost/query= with 1 occurrences * http://localhost/query=foo@bar with 1 occurrences * http://localhost/resources/child/response.txt with 1 occurrences * http://localhost/resources/foo with 1 occurrences * http://localhost/resources/response.txt with 1 occurrences * http://localhost/rest/books/6/pages/1.json with 1 occurrences * http://localhost/something/1/foo with 1 occurrences * http://localhost/something/optional-param with 1 occurrences * http://localhost/test with 4 occurrences * http://localhost/test.html with 1 occurrences * http://localhost/test/this with 2 occurrences * http://localhost:7070/example/adminhandle.vm with 1 occurrences * http://localhost:7070/example/bingo.html with 1 occurrences * http://localhost:80/jquery-1.11.0.min.js with 1 occurrences * http://localhost:8080 with 34 occurrences * http://localhost:8080/ with 4 occurrences * http://localhost:8080/OrderService with 3 occurrences * http://localhost:8080/aA/bB/cC with 1 occurrences * http://localhost:8080/context with 1 occurrences * http://localhost:8080/context/myapiresource with 1 occurrences * http://localhost:8080/gamecast/admin/index.html with 1 occurrences * http://localhost:8080/gamecast/display.html with 1 occurrences * http://localhost:8080/gamecast/displayShoppingCart.html with 1 occurrences * http://localhost:8080/jquery-1.11.0.min.js with 1 occurrences * http://localhost:8080/mvc-showcase with 1 occurrences * http://localhost:8080/myapiresource with 1 occurrences * http://localhost:8080/myapp/js/sockjs-client.js with 1 occurrences * http://localhost:8080/path with 2 occurrences * http://localhost:8080/people with 2 occurrences * http://localhost:8080/portfolio with 1 occurrences * http://localhost:8080/spring/ with 2 occurrences * http://localhost:8080/test/ with 2 occurrences * http://localhost:8080/test/print?value=%EA%B0%80+%EB%82%98 with 1 occurrences * http://localhost:8081 with 1 occurrences * http://localhost:8081/ with 1 occurrences * http://localhost:8181 with 1 occurrences * http://localhost:8888/AccountServiceEndpoint?WSDL with 1 occurrences * http://localhost:9000 with 7 occurrences * http://localhost:9090 with 18 occurrences * http://localhost:9090/jquery-1.11.0.min.js with 2 occurrences * http://localhost:9990/mail/messages/form with 1 occurrences * http://localhost:9999/ with 1 occurrences * http://localhost:9999/OrderService?wsdl with 4 occurrences * http://localhosta/testbean with 3 occurrences * http://myserver with 1 occurrences * http://myurl with 12 occurrences * http://remotehost:8080/remoting/AccountService with 2 occurrences * http://samples.springframework.org/flight with 39 occurrences * http://somethingDifferent with 1 occurrences * http://springframework.org/spring-ws with 17 occurrences * http://test.com with 1 occurrences * http://test.com/ with 1 occurrences * http://testng.org/ with 1 occurrences * http://tiles.apache.org/tags-tiles with 1 occurrences * http://www with 1 occurrences * http://www.foo.com/schema/component with 6 occurrences * http://www.mycompany.com/schema/myns with 4 occurrences * http://www.springframework.org/schema/aop with 30 occurrences * http://www.springframework.org/schema/beans with 166 occurrences * http://www.springframework.org/schema/beans/test with 2 occurrences * http://www.springframework.org/schema/c with 1 occurrences * http://www.springframework.org/schema/cache with 5 occurrences * http://www.springframework.org/schema/context with 28 occurrences * http://www.springframework.org/schema/jdbc with 6 occurrences * http://www.springframework.org/schema/jee with 8 occurrences * http://www.springframework.org/schema/jms with 5 occurrences * http://www.springframework.org/schema/lang with 10 occurrences * http://www.springframework.org/schema/mvc with 8 occurrences * http://www.springframework.org/schema/oxm with 4 occurrences * http://www.springframework.org/schema/p with 4 occurrences * http://www.springframework.org/schema/task with 2 occurrences * http://www.springframework.org/schema/tool with 27 occurrences * http://www.springframework.org/schema/tx with 17 occurrences * http://www.springframework.org/schema/tx/spring-tx.xsd__ with 1 occurrences * http://www.springframework.org/schema/util with 8 occurrences * http://www.springframework.org/schema/websocket with 24 occurrences * http://www.springframework.org/spring-ws with 1 occurrences * http://www.springframework.org/tags with 4 occurrences * http://www.springframework.org/tags/form with 2 occurrences * http://www.w3.org/1999/XSL/Transform with 8 occurrences * http://www.w3.org/2001/XMLSchema with 24 occurrences * http://www.w3.org/2001/XMLSchema-instance with 81 occurrences * http://www.w3.org/2005/Atom with 4 occurrences * http://www.w3.org/TR/html4/charset.html with 5 occurrences * http://www.w3.org/TR/html4/sgml/entities.html with 14 occurrences * http://www.w3.org/XML/1998/namespace with 5 occurrences * http://xml.org/sax/features/ with 4 occurrences * http://xml.org/sax/features/external-general-entities with 14 occurrences * http://xml.org/sax/features/is-standalone with 1 occurrences * http://xml.org/sax/features/namespace-prefixes with 13 occurrences * http://xml.org/sax/features/namespaces with 12 occurrences * http://xml.org/sax/features/namespaces-prefixes with 1 occurrences * http://xml.org/sax/properties/lexical-handler with 12 occurrences Closes gh-22679
2459 lines
96 KiB
Plaintext
2459 lines
96 KiB
Plaintext
[[websocket]]
|
|
= WebSockets
|
|
:doc-spring-security: {doc-root}/spring-security/site/docs/current/reference
|
|
[.small]#<<web-reactive.adoc#webflux-websocket,Same in Spring WebFlux>>#
|
|
|
|
This part of the reference documentation covers support for Servlet stack, WebSocket
|
|
messaging that includes raw WebSocket interactions, WebSocket emulation via SockJS, and
|
|
pub-sub messaging via STOMP as a sub-protocol over WebSocket.
|
|
|
|
|
|
include::websocket-intro.adoc[leveloffset=+1]
|
|
|
|
|
|
|
|
[[websocket-server]]
|
|
== WebSocket API
|
|
[.small]#<<web-reactive.adoc#webflux-websocket-server,Same in Spring WebFlux>>#
|
|
|
|
The Spring Framework provides a WebSocket API that can be used to write client and
|
|
server side applications that handle WebSocket messages.
|
|
|
|
|
|
|
|
[[websocket-server-handler]]
|
|
=== WebSocketHandler
|
|
[.small]#<<web-reactive.adoc#webflux-websocket-server-handler,Same in Spring WebFlux>>#
|
|
|
|
Creating a WebSocket server is as simple as implementing `WebSocketHandler` or more
|
|
likely extending either `TextWebSocketHandler` or `BinaryWebSocketHandler`:
|
|
|
|
[source,java,indent=0]
|
|
[subs="verbatim,quotes"]
|
|
----
|
|
import org.springframework.web.socket.WebSocketHandler;
|
|
import org.springframework.web.socket.WebSocketSession;
|
|
import org.springframework.web.socket.TextMessage;
|
|
|
|
public class MyHandler extends TextWebSocketHandler {
|
|
|
|
@Override
|
|
public void handleTextMessage(WebSocketSession session, TextMessage message) {
|
|
// ...
|
|
}
|
|
|
|
}
|
|
----
|
|
|
|
There is dedicated WebSocket Java-config and XML namespace support for mapping the above
|
|
WebSocket handler to a specific URL:
|
|
|
|
[source,java,indent=0]
|
|
[subs="verbatim,quotes"]
|
|
----
|
|
import org.springframework.web.socket.config.annotation.EnableWebSocket;
|
|
import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
|
|
import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry;
|
|
|
|
@Configuration
|
|
@EnableWebSocket
|
|
public class WebSocketConfig implements WebSocketConfigurer {
|
|
|
|
@Override
|
|
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
|
|
registry.addHandler(myHandler(), "/myHandler");
|
|
}
|
|
|
|
@Bean
|
|
public WebSocketHandler myHandler() {
|
|
return new MyHandler();
|
|
}
|
|
|
|
}
|
|
----
|
|
|
|
XML configuration equivalent:
|
|
|
|
[source,xml,indent=0]
|
|
[subs="verbatim,quotes,attributes"]
|
|
----
|
|
<beans xmlns="http://www.springframework.org/schema/beans"
|
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
|
xmlns:websocket="http://www.springframework.org/schema/websocket"
|
|
xsi:schemaLocation="
|
|
http://www.springframework.org/schema/beans
|
|
https://www.springframework.org/schema/beans/spring-beans.xsd
|
|
http://www.springframework.org/schema/websocket
|
|
https://www.springframework.org/schema/websocket/spring-websocket.xsd">
|
|
|
|
<websocket:handlers>
|
|
<websocket:mapping path="/myHandler" handler="myHandler"/>
|
|
</websocket:handlers>
|
|
|
|
<bean id="myHandler" class="org.springframework.samples.MyHandler"/>
|
|
|
|
</beans>
|
|
----
|
|
|
|
The above is for use in Spring MVC applications and should be included in the
|
|
configuration of a <<mvc-servlet,DispatcherServlet>>. However, Spring's WebSocket
|
|
support does not depend on Spring MVC. It is relatively simple to integrate a `WebSocketHandler`
|
|
into other HTTP serving environments with the help of
|
|
{api-spring-framework}/web/socket/server/support/WebSocketHttpRequestHandler.html[WebSocketHttpRequestHandler].
|
|
|
|
|
|
|
|
[[websocket-server-handshake]]
|
|
=== WebSocket Handshake
|
|
[.small]#<<web-reactive.adoc#webflux-websocket-server-handshake,Same in Spring WebFlux>>#
|
|
|
|
The easiest way to customize the initial HTTP WebSocket handshake request is through
|
|
a `HandshakeInterceptor`, which exposes "before" and "after" the handshake methods.
|
|
Such an interceptor can be used to preclude the handshake or to make any attributes
|
|
available to the `WebSocketSession`. For example, there is a built-in interceptor
|
|
for passing HTTP session attributes to the WebSocket session:
|
|
|
|
[source,java,indent=0]
|
|
[subs="verbatim,quotes"]
|
|
----
|
|
@Configuration
|
|
@EnableWebSocket
|
|
public class WebSocketConfig implements WebSocketConfigurer {
|
|
|
|
@Override
|
|
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
|
|
registry.addHandler(new MyHandler(), "/myHandler")
|
|
.addInterceptors(new HttpSessionHandshakeInterceptor());
|
|
}
|
|
|
|
}
|
|
----
|
|
|
|
And the XML configuration equivalent:
|
|
|
|
[source,xml,indent=0]
|
|
[subs="verbatim,quotes,attributes"]
|
|
----
|
|
<beans xmlns="http://www.springframework.org/schema/beans"
|
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
|
xmlns:websocket="http://www.springframework.org/schema/websocket"
|
|
xsi:schemaLocation="
|
|
http://www.springframework.org/schema/beans
|
|
https://www.springframework.org/schema/beans/spring-beans.xsd
|
|
http://www.springframework.org/schema/websocket
|
|
https://www.springframework.org/schema/websocket/spring-websocket.xsd">
|
|
|
|
<websocket:handlers>
|
|
<websocket:mapping path="/myHandler" handler="myHandler"/>
|
|
<websocket:handshake-interceptors>
|
|
<bean class="org.springframework.web.socket.server.support.HttpSessionHandshakeInterceptor"/>
|
|
</websocket:handshake-interceptors>
|
|
</websocket:handlers>
|
|
|
|
<bean id="myHandler" class="org.springframework.samples.MyHandler"/>
|
|
|
|
</beans>
|
|
----
|
|
|
|
A more advanced option is to extend the `DefaultHandshakeHandler` that performs
|
|
the steps of the WebSocket handshake, including validating the client origin,
|
|
negotiating a sub-protocol, and others. An application may also need to use this
|
|
option if it needs to configure a custom `RequestUpgradeStrategy` in order to
|
|
adapt to a WebSocket server engine and version that is not yet supported
|
|
(also see <<websocket-server-deployment>> for more on this subject).
|
|
Both the Java-config and XML namespace make it possible to configure a custom
|
|
`HandshakeHandler`.
|
|
|
|
|
|
[TIP]
|
|
====
|
|
Spring provides a `WebSocketHandlerDecorator` base class that can be used to decorate
|
|
a `WebSocketHandler` with additional behavior. Logging and exception handling
|
|
implementations are provided and added by default when using the WebSocket Java-config
|
|
or XML namespace. The `ExceptionWebSocketHandlerDecorator` catches all uncaught
|
|
exceptions arising from any WebSocketHandler method and closes the WebSocket
|
|
session with status `1011` that indicates a server error.
|
|
====
|
|
|
|
|
|
|
|
[[websocket-server-deployment]]
|
|
=== Deployment
|
|
|
|
The Spring WebSocket API is easy to integrate into a Spring MVC application where
|
|
the `DispatcherServlet` serves both HTTP WebSocket handshake as well as other
|
|
HTTP requests. It is also easy to integrate into other HTTP processing scenarios
|
|
by invoking `WebSocketHttpRequestHandler`. This is convenient and easy to
|
|
understand. However, special considerations apply with regards to JSR-356 runtimes.
|
|
|
|
The Java WebSocket API (JSR-356) provides two deployment mechanisms. The first
|
|
involves a Servlet container classpath scan (Servlet 3 feature) at startup; and
|
|
the other is a registration API to use at Servlet container initialization.
|
|
Neither of these mechanism makes it possible to use a single "front controller"
|
|
for all HTTP processing -- including WebSocket handshake and all other HTTP
|
|
requests -- such as Spring MVC's `DispatcherServlet`.
|
|
|
|
This is a significant limitation of JSR-356 that Spring's WebSocket support addresses
|
|
server-specific ``RequestUpgradeStrategy``'s even when running in a JSR-356 runtime.
|
|
Such strategies currently exist for Tomcat, Jetty, GlassFish, WebLogic, WebSphere, and
|
|
Undertow (and WildFly).
|
|
|
|
|
|
|
|
[NOTE]
|
|
====
|
|
A request to overcome the above limitation in the Java WebSocket API has been
|
|
created and can be followed at
|
|
https://github.com/eclipse-ee4j/websocket-api/issues/211[WEBSOCKET_SPEC-211].
|
|
Tomcat, Undertow and WebSphere provide their own API alternatives that
|
|
makes it possible to this, and it's also possible with Jetty. We are hopeful
|
|
that more servers will follow do the same.
|
|
====
|
|
|
|
A secondary consideration is that Servlet containers with JSR-356 support are expected
|
|
to perform a `ServletContainerInitializer` (SCI) scan that can slow down application
|
|
startup, in some cases dramatically. If a significant impact is observed after an
|
|
upgrade to a Servlet container version with JSR-356 support, it should
|
|
be possible to selectively enable or disable web fragments (and SCI scanning)
|
|
through the use of the `<absolute-ordering />` element in `web.xml`:
|
|
|
|
[source,xml,indent=0]
|
|
[subs="verbatim,quotes,attributes"]
|
|
----
|
|
<web-app xmlns="http://java.sun.com/xml/ns/javaee"
|
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
|
xsi:schemaLocation="
|
|
http://java.sun.com/xml/ns/javaee
|
|
https://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
|
|
version="3.0">
|
|
|
|
<absolute-ordering/>
|
|
|
|
</web-app>
|
|
----
|
|
|
|
You can then selectively enable web fragments by name, such as Spring's own
|
|
`SpringServletContainerInitializer` that provides support for the Servlet 3
|
|
Java initialization API, if required:
|
|
|
|
[source,xml,indent=0]
|
|
[subs="verbatim,quotes,attributes"]
|
|
----
|
|
<web-app xmlns="http://java.sun.com/xml/ns/javaee"
|
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
|
xsi:schemaLocation="
|
|
http://java.sun.com/xml/ns/javaee
|
|
https://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
|
|
version="3.0">
|
|
|
|
<absolute-ordering>
|
|
<name>spring_web</name>
|
|
</absolute-ordering>
|
|
|
|
</web-app>
|
|
----
|
|
|
|
|
|
|
|
[[websocket-server-runtime-configuration]]
|
|
=== Server config
|
|
[.small]#<<web-reactive.adoc#webflux-websocket-server-config,Same in Spring WebFlux>>#
|
|
|
|
Each underlying WebSocket engine exposes configuration properties that control
|
|
runtime characteristics such as the size of message buffer sizes, idle timeout,
|
|
and others.
|
|
|
|
For Tomcat, WildFly, and GlassFish add a `ServletServerContainerFactoryBean` to your
|
|
WebSocket Java config:
|
|
|
|
[source,java,indent=0]
|
|
[subs="verbatim,quotes"]
|
|
----
|
|
@Configuration
|
|
@EnableWebSocket
|
|
public class WebSocketConfig implements WebSocketConfigurer {
|
|
|
|
@Bean
|
|
public ServletServerContainerFactoryBean createWebSocketContainer() {
|
|
ServletServerContainerFactoryBean container = new ServletServerContainerFactoryBean();
|
|
container.setMaxTextMessageBufferSize(8192);
|
|
container.setMaxBinaryMessageBufferSize(8192);
|
|
return container;
|
|
}
|
|
|
|
}
|
|
----
|
|
|
|
or WebSocket XML namespace:
|
|
|
|
[source,xml,indent=0]
|
|
[subs="verbatim,quotes,attributes"]
|
|
----
|
|
<beans xmlns="http://www.springframework.org/schema/beans"
|
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
|
xmlns:websocket="http://www.springframework.org/schema/websocket"
|
|
xsi:schemaLocation="
|
|
http://www.springframework.org/schema/beans
|
|
https://www.springframework.org/schema/beans/spring-beans.xsd
|
|
http://www.springframework.org/schema/websocket
|
|
https://www.springframework.org/schema/websocket/spring-websocket.xsd">
|
|
|
|
<bean class="org.springframework...ServletServerContainerFactoryBean">
|
|
<property name="maxTextMessageBufferSize" value="8192"/>
|
|
<property name="maxBinaryMessageBufferSize" value="8192"/>
|
|
</bean>
|
|
|
|
</beans>
|
|
----
|
|
|
|
[NOTE]
|
|
====
|
|
For client side WebSocket configuration, you should use `WebSocketContainerFactoryBean`
|
|
(XML) or `ContainerProvider.getWebSocketContainer()` (Java config).
|
|
====
|
|
|
|
For Jetty, you'll need to supply a pre-configured Jetty `WebSocketServerFactory` and plug
|
|
that into Spring's `DefaultHandshakeHandler` through your WebSocket Java config:
|
|
|
|
[source,java,indent=0]
|
|
[subs="verbatim,quotes"]
|
|
----
|
|
@Configuration
|
|
@EnableWebSocket
|
|
public class WebSocketConfig implements WebSocketConfigurer {
|
|
|
|
@Override
|
|
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
|
|
registry.addHandler(echoWebSocketHandler(),
|
|
"/echo").setHandshakeHandler(handshakeHandler());
|
|
}
|
|
|
|
@Bean
|
|
public DefaultHandshakeHandler handshakeHandler() {
|
|
|
|
WebSocketPolicy policy = new WebSocketPolicy(WebSocketBehavior.SERVER);
|
|
policy.setInputBufferSize(8192);
|
|
policy.setIdleTimeout(600000);
|
|
|
|
return new DefaultHandshakeHandler(
|
|
new JettyRequestUpgradeStrategy(new WebSocketServerFactory(policy)));
|
|
}
|
|
|
|
}
|
|
----
|
|
|
|
or WebSocket XML namespace:
|
|
|
|
[source,xml,indent=0]
|
|
[subs="verbatim,quotes,attributes"]
|
|
----
|
|
<beans xmlns="http://www.springframework.org/schema/beans"
|
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
|
xmlns:websocket="http://www.springframework.org/schema/websocket"
|
|
xsi:schemaLocation="
|
|
http://www.springframework.org/schema/beans
|
|
https://www.springframework.org/schema/beans/spring-beans.xsd
|
|
http://www.springframework.org/schema/websocket
|
|
https://www.springframework.org/schema/websocket/spring-websocket.xsd">
|
|
|
|
<websocket:handlers>
|
|
<websocket:mapping path="/echo" handler="echoHandler"/>
|
|
<websocket:handshake-handler ref="handshakeHandler"/>
|
|
</websocket:handlers>
|
|
|
|
<bean id="handshakeHandler" class="org.springframework...DefaultHandshakeHandler">
|
|
<constructor-arg ref="upgradeStrategy"/>
|
|
</bean>
|
|
|
|
<bean id="upgradeStrategy" class="org.springframework...JettyRequestUpgradeStrategy">
|
|
<constructor-arg ref="serverFactory"/>
|
|
</bean>
|
|
|
|
<bean id="serverFactory" class="org.eclipse.jetty...WebSocketServerFactory">
|
|
<constructor-arg>
|
|
<bean class="org.eclipse.jetty...WebSocketPolicy">
|
|
<constructor-arg value="SERVER"/>
|
|
<property name="inputBufferSize" value="8092"/>
|
|
<property name="idleTimeout" value="600000"/>
|
|
</bean>
|
|
</constructor-arg>
|
|
</bean>
|
|
|
|
</beans>
|
|
----
|
|
|
|
|
|
|
|
[[websocket-server-allowed-origins]]
|
|
=== Allowed origins
|
|
[.small]#<<web-reactive.adoc#webflux-websocket-server-cors,Same in Spring WebFlux>>#
|
|
|
|
As of Spring Framework 4.1.5, the default behavior for WebSocket and SockJS is to accept
|
|
only _same origin_ requests. It is also possible to allow _all_ or a specified list of origins.
|
|
This check is mostly designed for browser clients. There is nothing preventing other types
|
|
of clients from modifying the `Origin` header value (see
|
|
https://tools.ietf.org/html/rfc6454[RFC 6454: The Web Origin Concept] for more details).
|
|
|
|
The 3 possible behaviors are:
|
|
|
|
* Allow only same origin requests (default): in this mode, when SockJS is enabled, the
|
|
Iframe HTTP response header `X-Frame-Options` is set to `SAMEORIGIN`, and JSONP
|
|
transport is disabled since it does not allow to check the origin of a request.
|
|
As a consequence, IE6 and IE7 are not supported when this mode is enabled.
|
|
* Allow a specified list of origins: each provided _allowed origin_ must start with `http://`
|
|
or `https://`. In this mode, when SockJS is enabled, both IFrame and JSONP based
|
|
transports are disabled. As a consequence, IE6 through IE9 are not supported when this
|
|
mode is enabled.
|
|
* Allow all origins: to enable this mode, you should provide `{asterisk}` as the allowed origin
|
|
value. In this mode, all transports are available.
|
|
|
|
WebSocket and SockJS allowed origins can be configured as shown bellow:
|
|
|
|
[source,java,indent=0]
|
|
[subs="verbatim,quotes"]
|
|
----
|
|
import org.springframework.web.socket.config.annotation.EnableWebSocket;
|
|
import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
|
|
import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry;
|
|
|
|
@Configuration
|
|
@EnableWebSocket
|
|
public class WebSocketConfig implements WebSocketConfigurer {
|
|
|
|
@Override
|
|
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
|
|
registry.addHandler(myHandler(), "/myHandler").setAllowedOrigins("https://mydomain.com");
|
|
}
|
|
|
|
@Bean
|
|
public WebSocketHandler myHandler() {
|
|
return new MyHandler();
|
|
}
|
|
|
|
}
|
|
----
|
|
|
|
XML configuration equivalent:
|
|
|
|
[source,xml,indent=0]
|
|
[subs="verbatim,quotes,attributes"]
|
|
----
|
|
<beans xmlns="http://www.springframework.org/schema/beans"
|
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
|
xmlns:websocket="http://www.springframework.org/schema/websocket"
|
|
xsi:schemaLocation="
|
|
http://www.springframework.org/schema/beans
|
|
https://www.springframework.org/schema/beans/spring-beans.xsd
|
|
http://www.springframework.org/schema/websocket
|
|
https://www.springframework.org/schema/websocket/spring-websocket.xsd">
|
|
|
|
<websocket:handlers allowed-origins="https://mydomain.com">
|
|
<websocket:mapping path="/myHandler" handler="myHandler" />
|
|
</websocket:handlers>
|
|
|
|
<bean id="myHandler" class="org.springframework.samples.MyHandler"/>
|
|
|
|
</beans>
|
|
----
|
|
|
|
|
|
|
|
|
|
[[websocket-fallback]]
|
|
== SockJS Fallback
|
|
|
|
Over the public Internet, restrictive proxies outside your control may preclude WebSocket
|
|
interactions either because they are not configured to pass on the `Upgrade` header or
|
|
because they close long lived connections that appear idle.
|
|
|
|
The solution to this problem is WebSocket emulation, i.e. attempting to use WebSocket
|
|
first and then falling back on HTTP-based techniques that emulate a WebSocket
|
|
interaction and expose the same application-level API.
|
|
|
|
On the Servlet stack the Spring Framework provides both server (and also client) support
|
|
for the SockJS protocol.
|
|
|
|
|
|
[[websocket-fallback-sockjs-overview]]
|
|
=== Overview
|
|
|
|
The goal of SockJS is to let applications use a WebSocket API but fall back to
|
|
non-WebSocket alternatives when necessary at runtime, i.e. without the need to
|
|
change application code.
|
|
|
|
SockJS consists of:
|
|
|
|
* The https://github.com/sockjs/sockjs-protocol[SockJS protocol]
|
|
defined in the form of executable
|
|
https://sockjs.github.io/sockjs-protocol/sockjs-protocol-0.3.3.html[narrated tests].
|
|
* The https://github.com/sockjs/sockjs-client/[SockJS JavaScript client] - a client library for use in browsers.
|
|
* SockJS server implementations including one in the Spring Framework `spring-websocket` module.
|
|
* As of 4.1 `spring-websocket` also provides a SockJS Java client.
|
|
|
|
SockJS is designed for use in browsers. It goes to great lengths
|
|
to support a wide range of browser versions using a variety of techniques.
|
|
For the full list of SockJS transport types and browsers see the
|
|
https://github.com/sockjs/sockjs-client/[SockJS client] page. Transports
|
|
fall in 3 general categories: WebSocket, HTTP Streaming, and HTTP Long Polling.
|
|
For an overview of these categories see
|
|
https://spring.io/blog/2012/05/08/spring-mvc-3-2-preview-techniques-for-real-time-updates/[this blog post].
|
|
|
|
The SockJS client begins by sending `"GET /info"` to
|
|
obtain basic information from the server. After that it must decide what transport
|
|
to use. If possible WebSocket is used. If not, in most browsers
|
|
there is at least one HTTP streaming option and if not then HTTP (long)
|
|
polling is used.
|
|
|
|
All transport requests have the following URL structure:
|
|
----
|
|
http://host:port/myApp/myEndpoint/{server-id}/{session-id}/{transport}
|
|
----
|
|
|
|
* `{server-id}` - useful for routing requests in a cluster but not used otherwise.
|
|
* `{session-id}` - correlates HTTP requests belonging to a SockJS session.
|
|
* `{transport}` - indicates the transport type, e.g. "websocket", "xhr-streaming", etc.
|
|
|
|
The WebSocket transport needs only a single HTTP request to do the WebSocket handshake.
|
|
All messages thereafter are exchanged on that socket.
|
|
|
|
HTTP transports require more requests. Ajax/XHR streaming for example relies on
|
|
one long-running request for server-to-client messages and additional HTTP POST
|
|
requests for client-to-server messages. Long polling is similar except it
|
|
ends the current request after each server-to-client send.
|
|
|
|
SockJS adds minimal message framing. For example the server sends the letter +o+
|
|
("open" frame) initially, messages are sent as +a["message1","message2"]+
|
|
(JSON-encoded array), the letter +h+ ("heartbeat" frame) if no messages flow
|
|
for 25 seconds by default, and the letter +c+ ("close" frame) to close the session.
|
|
|
|
To learn more, run an example in a browser and watch the HTTP requests.
|
|
The SockJS client allows fixing the list of transports so it is possible to
|
|
see each transport one at a time. The SockJS client also provides a debug flag
|
|
which enables helpful messages in the browser console. On the server side enable
|
|
`TRACE` logging for `org.springframework.web.socket`.
|
|
For even more detail refer to the SockJS protocol
|
|
https://sockjs.github.io/sockjs-protocol/sockjs-protocol-0.3.3.html[narrated test].
|
|
|
|
|
|
|
|
[[websocket-fallback-sockjs-enable]]
|
|
=== Enable SockJS
|
|
|
|
SockJS is easy to enable through Java configuration:
|
|
|
|
[source,java,indent=0]
|
|
[subs="verbatim,quotes"]
|
|
----
|
|
@Configuration
|
|
@EnableWebSocket
|
|
public class WebSocketConfig implements WebSocketConfigurer {
|
|
|
|
@Override
|
|
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
|
|
registry.addHandler(myHandler(), "/myHandler").withSockJS();
|
|
}
|
|
|
|
@Bean
|
|
public WebSocketHandler myHandler() {
|
|
return new MyHandler();
|
|
}
|
|
|
|
}
|
|
----
|
|
|
|
and the XML configuration equivalent:
|
|
|
|
[source,xml,indent=0]
|
|
[subs="verbatim,quotes,attributes"]
|
|
----
|
|
<beans xmlns="http://www.springframework.org/schema/beans"
|
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
|
xmlns:websocket="http://www.springframework.org/schema/websocket"
|
|
xsi:schemaLocation="
|
|
http://www.springframework.org/schema/beans
|
|
https://www.springframework.org/schema/beans/spring-beans.xsd
|
|
http://www.springframework.org/schema/websocket
|
|
https://www.springframework.org/schema/websocket/spring-websocket.xsd">
|
|
|
|
<websocket:handlers>
|
|
<websocket:mapping path="/myHandler" handler="myHandler"/>
|
|
<websocket:sockjs/>
|
|
</websocket:handlers>
|
|
|
|
<bean id="myHandler" class="org.springframework.samples.MyHandler"/>
|
|
|
|
</beans>
|
|
----
|
|
|
|
The above is for use in Spring MVC applications and should be included in the
|
|
configuration of a <<mvc-servlet,DispatcherServlet>>. However, Spring's WebSocket
|
|
and SockJS support does not depend on Spring MVC. It is relatively simple to
|
|
integrate into other HTTP serving environments with the help of
|
|
{api-spring-framework}/web/socket/sockjs/support/SockJsHttpRequestHandler.html[SockJsHttpRequestHandler].
|
|
|
|
On the browser side, applications can use the
|
|
https://github.com/sockjs/sockjs-client/[sockjs-client] (version 1.0.x) that
|
|
emulates the W3C WebSocket API and communicates with the server to select the best
|
|
transport option depending on the browser it's running in. Review the
|
|
https://github.com/sockjs/sockjs-client/[sockjs-client] page and the list of
|
|
transport types supported by browser. The client also provides several
|
|
configuration options, for example, to specify which transports to include.
|
|
|
|
|
|
|
|
[[websocket-fallback-xhr-vs-iframe]]
|
|
=== IE 8, 9
|
|
|
|
Internet Explorer 8 and 9 are and will remain common for some time. They are
|
|
a key reason for having SockJS. This section covers important
|
|
considerations about running in those browsers.
|
|
|
|
The SockJS client supports Ajax/XHR streaming in IE 8 and 9 via Microsoft's
|
|
https://blogs.msdn.com/b/ieinternals/archive/2010/05/13/xdomainrequest-restrictions-limitations-and-workarounds.aspx[XDomainRequest].
|
|
That works across domains but does not support sending cookies.
|
|
Cookies are very often essential for Java applications.
|
|
However since the SockJS client can be used with many server
|
|
types (not just Java ones), it needs to know whether cookies matter.
|
|
If so the SockJS client prefers Ajax/XHR for streaming or otherwise it
|
|
relies on a iframe-based technique.
|
|
|
|
The very first `"/info"` request from the SockJS client is a request for
|
|
information that can influence the client's choice of transports.
|
|
One of those details is whether the server application relies on cookies,
|
|
e.g. for authentication purposes or clustering with sticky sessions.
|
|
Spring's SockJS support includes a property called `sessionCookieNeeded`.
|
|
It is enabled by default since most Java applications rely on the `JSESSIONID`
|
|
cookie. If your application does not need it, you can turn off this option
|
|
and the SockJS client should choose `xdr-streaming` in IE 8 and 9.
|
|
|
|
If you do use an iframe-based transport, and in any case, it is good to know
|
|
that browsers can be instructed to block the use of IFrames on a given page by
|
|
setting the HTTP response header `X-Frame-Options` to `DENY`,
|
|
`SAMEORIGIN`, or `ALLOW-FROM <origin>`. This is used to prevent
|
|
https://www.owasp.org/index.php/Clickjacking[clickjacking].
|
|
|
|
[NOTE]
|
|
====
|
|
Spring Security 3.2+ provides support for setting `X-Frame-Options` on every
|
|
response. By default the Spring Security Java config sets it to `DENY`.
|
|
In 3.2 the Spring Security XML namespace does not set that header by default
|
|
but may be configured to do so, and in the future it may set it by default.
|
|
|
|
See {doc-spring-security}/htmlsingle/#headers[Section 7.1. "Default Security Headers"]
|
|
of the Spring Security documentation for details on how to configure the
|
|
setting of the `X-Frame-Options` header. You may also check or watch
|
|
https://jira.spring.io/browse/SEC-2501[SEC-2501] for additional background.
|
|
====
|
|
|
|
If your application adds the `X-Frame-Options` response header (as it should!)
|
|
and relies on an iframe-based transport, you will need to set the header value to
|
|
`SAMEORIGIN` or `ALLOW-FROM <origin>`. Along with that the Spring SockJS
|
|
support also needs to know the location of the SockJS client because it is loaded
|
|
from the iframe. By default the iframe is set to download the SockJS client
|
|
from a CDN location. It is a good idea to configure this option to
|
|
a URL from the same origin as the application.
|
|
|
|
In Java config this can be done as shown below. The XML namespace provides a
|
|
similar option via the `<websocket:sockjs>` element:
|
|
|
|
[source,java,indent=0]
|
|
[subs="verbatim,quotes"]
|
|
----
|
|
@Configuration
|
|
@EnableWebSocket
|
|
public class WebSocketConfig implements WebSocketConfigurer {
|
|
|
|
@Override
|
|
public void registerStompEndpoints(StompEndpointRegistry registry) {
|
|
registry.addEndpoint("/portfolio").withSockJS()
|
|
.setClientLibraryUrl("http://localhost:8080/myapp/js/sockjs-client.js");
|
|
}
|
|
|
|
// ...
|
|
|
|
}
|
|
----
|
|
|
|
[NOTE]
|
|
====
|
|
During initial development, do enable the SockJS client `devel` mode that prevents
|
|
the browser from caching SockJS requests (like the iframe) that would otherwise
|
|
be cached. For details on how to enable it see the
|
|
https://github.com/sockjs/sockjs-client/[SockJS client] page.
|
|
====
|
|
|
|
|
|
|
|
[[websocket-fallback-sockjs-heartbeat]]
|
|
=== Heartbeats
|
|
|
|
The SockJS protocol requires servers to send heartbeat messages to preclude proxies
|
|
from concluding a connection is hung. The Spring SockJS configuration has a property
|
|
called `heartbeatTime` that can be used to customize the frequency. By default a
|
|
heartbeat is sent after 25 seconds assuming no other messages were sent on that
|
|
connection. This 25 seconds value is in line with the following
|
|
https://tools.ietf.org/html/rfc6202[IETF recommendation] for public Internet applications.
|
|
|
|
[NOTE]
|
|
====
|
|
When using STOMP over WebSocket/SockJS, if the STOMP client and server negotiate
|
|
heartbeats to be exchanged, the SockJS heartbeats are disabled.
|
|
====
|
|
|
|
The Spring SockJS support also allows configuring the `TaskScheduler` to use
|
|
for scheduling heartbeats tasks. The task scheduler is backed by a thread pool
|
|
with default settings based on the number of available processors. Applications
|
|
should consider customizing the settings according to their specific needs.
|
|
|
|
|
|
|
|
[[websocket-fallback-sockjs-servlet3-async]]
|
|
=== Client disconnects
|
|
|
|
HTTP streaming and HTTP long polling SockJS transports require a connection to remain
|
|
open longer than usual. For an overview of these techniques see
|
|
https://spring.io/blog/2012/05/08/spring-mvc-3-2-preview-techniques-for-real-time-updates/[this blog post].
|
|
|
|
In Servlet containers this is done through Servlet 3 async support that
|
|
allows exiting the Servlet container thread processing a request and continuing
|
|
to write to the response from another thread.
|
|
|
|
A specific issue is that the Servlet API does not provide notifications for a client
|
|
that has gone away, see https://java.net/jira/browse/SERVLET_SPEC-44[SERVLET_SPEC-44].
|
|
However, Servlet containers raise an exception on subsequent attempts to write
|
|
to the response. Since Spring's SockJS Service supports sever-sent heartbeats (every
|
|
25 seconds by default), that means a client disconnect is usually detected within that
|
|
time period or earlier if messages are sent more frequently.
|
|
|
|
[NOTE]
|
|
====
|
|
As a result network IO failures may occur simply because a client has disconnected, which
|
|
can fill the log with unnecessary stack traces. Spring makes a best effort to identify
|
|
such network failures that represent client disconnects (specific to each server) and log
|
|
a minimal message using the dedicated log category `DISCONNECTED_CLIENT_LOG_CATEGORY`
|
|
defined in `AbstractSockJsSession`. If you need to see the stack traces, set that
|
|
log category to TRACE.
|
|
====
|
|
|
|
|
|
|
|
[[websocket-fallback-cors]]
|
|
=== SockJS and CORS
|
|
|
|
If you allow cross-origin requests (see <<websocket-server-allowed-origins>>), the SockJS protocol
|
|
uses CORS for cross-domain support in the XHR streaming and polling transports. Therefore
|
|
CORS headers are added automatically unless the presence of CORS headers in the response
|
|
is detected. So if an application is already configured to provide CORS support, e.g.
|
|
through a Servlet Filter, Spring's SockJsService will skip this part.
|
|
|
|
It is also possible to disable the addition of these CORS headers via the
|
|
`suppressCors` property in Spring's SockJsService.
|
|
|
|
The following is the list of headers and values expected by SockJS:
|
|
|
|
* `"Access-Control-Allow-Origin"` - initialized from the value of the "Origin" request header.
|
|
* `"Access-Control-Allow-Credentials"` - always set to `true`.
|
|
* `"Access-Control-Request-Headers"` - initialized from values from the equivalent request header.
|
|
* `"Access-Control-Allow-Methods"` - the HTTP methods a transport supports (see `TransportType` enum).
|
|
* `"Access-Control-Max-Age"` - set to 31536000 (1 year).
|
|
|
|
For the exact implementation see `addCorsHeaders` in `AbstractSockJsService` as well
|
|
as the `TransportType` enum in the source code.
|
|
|
|
Alternatively if the CORS configuration allows it consider excluding URLs with the
|
|
SockJS endpoint prefix thus letting Spring's `SockJsService` handle it.
|
|
|
|
|
|
|
|
[[websocket-fallback-sockjs-client]]
|
|
=== SockJsClient
|
|
|
|
A SockJS Java client is provided in order to connect to remote SockJS endpoints without
|
|
using a browser. This can be especially useful when there is a need for bidirectional
|
|
communication between 2 servers over a public network, i.e. where network proxies may
|
|
preclude the use of the WebSocket protocol. A SockJS Java client is also very useful
|
|
for testing purposes, for example to simulate a large number of concurrent users.
|
|
|
|
The SockJS Java client supports the "websocket", "xhr-streaming", and "xhr-polling"
|
|
transports. The remaining ones only make sense for use in a browser.
|
|
|
|
The `WebSocketTransport` can be configured with:
|
|
|
|
* `StandardWebSocketClient` in a JSR-356 runtime
|
|
* `JettyWebSocketClient` using the Jetty 9+ native WebSocket API
|
|
* Any implementation of Spring's `WebSocketClient`
|
|
|
|
An `XhrTransport` by definition supports both "xhr-streaming" and "xhr-polling" since
|
|
from a client perspective there is no difference other than in the URL used to connect
|
|
to the server. At present there are two implementations:
|
|
|
|
* `RestTemplateXhrTransport` uses Spring's `RestTemplate` for HTTP requests.
|
|
* `JettyXhrTransport` uses Jetty's `HttpClient` for HTTP requests.
|
|
|
|
The example below shows how to create a SockJS client and connect to a SockJS endpoint:
|
|
|
|
[source,java,indent=0]
|
|
[subs="verbatim,quotes"]
|
|
----
|
|
List<Transport> transports = new ArrayList<>(2);
|
|
transports.add(new WebSocketTransport(new StandardWebSocketClient()));
|
|
transports.add(new RestTemplateXhrTransport());
|
|
|
|
SockJsClient sockJsClient = new SockJsClient(transports);
|
|
sockJsClient.doHandshake(new MyWebSocketHandler(), "ws://example.com:8080/sockjs");
|
|
----
|
|
|
|
[NOTE]
|
|
====
|
|
SockJS uses JSON formatted arrays for messages. By default Jackson 2 is used and needs
|
|
to be on the classpath. Alternatively you can configure a custom implementation of
|
|
`SockJsMessageCodec` and configure it on the `SockJsClient`.
|
|
====
|
|
|
|
To use the SockJsClient for simulating a large number of concurrent users you will
|
|
need to configure the underlying HTTP client (for XHR transports) to allow a sufficient
|
|
number of connections and threads. For example with Jetty:
|
|
|
|
[source,java,indent=0]
|
|
[subs="verbatim,quotes"]
|
|
----
|
|
HttpClient jettyHttpClient = new HttpClient();
|
|
jettyHttpClient.setMaxConnectionsPerDestination(1000);
|
|
jettyHttpClient.setExecutor(new QueuedThreadPool(1000));
|
|
----
|
|
|
|
Consider also customizing these server-side SockJS related properties (see Javadoc for details):
|
|
|
|
[source,java,indent=0]
|
|
[subs="verbatim,quotes"]
|
|
----
|
|
@Configuration
|
|
public class WebSocketConfig extends WebSocketMessageBrokerConfigurationSupport {
|
|
|
|
@Override
|
|
public void registerStompEndpoints(StompEndpointRegistry registry) {
|
|
registry.addEndpoint("/sockjs").withSockJS()
|
|
.setStreamBytesLimit(512 * 1024)
|
|
.setHttpMessageCacheSize(1000)
|
|
.setDisconnectDelay(30 * 1000);
|
|
}
|
|
|
|
// ...
|
|
}
|
|
----
|
|
|
|
|
|
|
|
|
|
[[websocket-stomp]]
|
|
== STOMP
|
|
|
|
The WebSocket protocol defines two types of messages, text and binary, but their
|
|
content is undefined. The defines a mechanism for client and server to negotiate a
|
|
sub-protocol -- i.e. a higher level messaging protocol, to use on top of WebSocket to
|
|
define what kind of messages each can send, what is the format and content for each
|
|
message, and so on. The use of a sub-protocol is optional but either way client and
|
|
server will need to agree on some protocol that defines message content.
|
|
|
|
|
|
|
|
[[websocket-stomp-overview]]
|
|
=== Overview
|
|
|
|
https://stomp.github.io/stomp-specification-1.2.html#Abstract[STOMP] is a simple,
|
|
text-oriented messaging protocol that was originally created for scripting languages
|
|
such as Ruby, Python, and Perl to connect to enterprise message brokers. It is
|
|
designed to address a minimal subset of commonly used messaging patterns. STOMP can be
|
|
used over any reliable, 2-way streaming network protocol such as TCP and WebSocket.
|
|
Although STOMP is a text-oriented protocol, message payloads can be
|
|
either text or binary.
|
|
|
|
STOMP is a frame based protocol whose frames are modeled on HTTP. The structure
|
|
of a STOMP frame:
|
|
|
|
----
|
|
COMMAND
|
|
header1:value1
|
|
header2:value2
|
|
|
|
Body^@
|
|
----
|
|
|
|
Clients can use the +SEND+ or +SUBSCRIBE+ commands to send or subscribe for
|
|
messages along with a +"destination"+ header that describes what the
|
|
message is about and who should receive it. This enables a simple
|
|
publish-subscribe mechanism that can be used to send messages through the broker
|
|
to other connected clients or to send messages to the server to request that
|
|
some work be performed.
|
|
|
|
When using Spring's STOMP support, the Spring WebSocket application acts
|
|
as the STOMP broker to clients. Messages are routed to `@Controller` message-handling
|
|
methods or to a simple, in-memory broker that keeps track of subscriptions and
|
|
broadcasts messages to subscribed users. You can also configure Spring to work
|
|
with a dedicated STOMP broker (e.g. RabbitMQ, ActiveMQ, etc) for the actual
|
|
broadcasting of messages. In that case Spring maintains
|
|
TCP connections to the broker, relays messages to it, and also passes messages
|
|
from it down to connected WebSocket clients. Thus Spring web applications can
|
|
rely on unified HTTP-based security, common validation, and a familiar programming
|
|
model message-handling work.
|
|
|
|
Here is an example of a client subscribing to receive stock quotes which
|
|
the server may emit periodically e.g. via a scheduled task sending messages
|
|
through a `SimpMessagingTemplate` to the broker:
|
|
|
|
----
|
|
SUBSCRIBE
|
|
id:sub-1
|
|
destination:/topic/price.stock.*
|
|
|
|
^@
|
|
----
|
|
|
|
Here is an example of a client sending a trade request, which the server
|
|
may handle through an `@MessageMapping` method and later on, after the execution,
|
|
broadcast a trade confirmation message and details down to the client:
|
|
|
|
----
|
|
SEND
|
|
destination:/queue/trade
|
|
content-type:application/json
|
|
content-length:44
|
|
|
|
{"action":"BUY","ticker":"MMM","shares",44}^@
|
|
----
|
|
|
|
The meaning of a destination is intentionally left opaque in the STOMP spec. It can
|
|
be any string, and it's entirely up to STOMP servers to define the semantics and
|
|
the syntax of the destinations that they support. It is very common, however, for
|
|
destinations to be path-like strings where `"/topic/.."` implies publish-subscribe
|
|
(__one-to-many__) and `"/queue/"` implies point-to-point (__one-to-one__) message
|
|
exchanges.
|
|
|
|
STOMP servers can use the +MESSAGE+ command to broadcast messages to all subscribers.
|
|
Here is an example of a server sending a stock quote to a subscribed client:
|
|
|
|
----
|
|
MESSAGE
|
|
message-id:nxahklf6-1
|
|
subscription:sub-1
|
|
destination:/topic/price.stock.MMM
|
|
|
|
{"ticker":"MMM","price":129.45}^@
|
|
----
|
|
|
|
It is important to know that a server cannot send unsolicited messages. All messages
|
|
from a server must be in response to a specific client subscription, and the
|
|
+"subscription-id"+ header of the server message must match the +"id"+ header of the
|
|
client subscription.
|
|
|
|
The above overview is intended to provide the most basic understanding of the
|
|
STOMP protocol. It is recommended to review the protocol
|
|
https://stomp.github.io/stomp-specification-1.2.html[specification] in full.
|
|
|
|
|
|
|
|
[[websocket-stomp-benefits]]
|
|
=== Benefits
|
|
|
|
Use of STOMP as a sub-protocol enables the Spring Framework and Spring Security to
|
|
provide a richer programming model vs using raw WebSockets. The same point can be
|
|
made about how HTTP vs raw TCP and how it enables Spring MVC and other web frameworks
|
|
to provide rich functionality. The following is a list of benefits:
|
|
|
|
* No need to invent a custom messaging protocol and message format.
|
|
* STOMP clients are available including a <<websocket-stomp-client,Java client>>
|
|
in the Spring Framework.
|
|
* Message brokers such as RabbitMQ, ActiveMQ, and others can be used (optionally) to
|
|
manage subscriptions and broadcast messages.
|
|
* Application logic can be organized in any number of ``@Controller``'s and messages
|
|
routed to them based on the STOMP destination header vs handling raw WebSocket messages
|
|
with a single `WebSocketHandler` for a given connection.
|
|
* Use Spring Security to secure messages based on STOMP destinations and message types.
|
|
|
|
|
|
|
|
[[websocket-stomp-enable]]
|
|
=== Enable STOMP
|
|
|
|
STOMP over WebSocket support is available in the `spring-messaging` and the
|
|
`spring-websocket` modules. Once you have those dependencies, you can expose a STOMP
|
|
endpoints, over WebSocket with <<websocket-fallback>>, as shown below:
|
|
|
|
[source,java,indent=0]
|
|
[subs="verbatim,quotes"]
|
|
----
|
|
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
|
|
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
|
|
|
|
@Configuration
|
|
@EnableWebSocketMessageBroker
|
|
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
|
|
|
|
@Override
|
|
public void registerStompEndpoints(StompEndpointRegistry registry) {
|
|
registry.addEndpoint("/portfolio").withSockJS(); // <1>
|
|
}
|
|
|
|
@Override
|
|
public void configureMessageBroker(MessageBrokerRegistry config) {
|
|
config.setApplicationDestinationPrefixes("/app"); // <2>
|
|
config.enableSimpleBroker("/topic", "/queue"); // <3>
|
|
}
|
|
}
|
|
----
|
|
|
|
<1> `"/portfolio"` is the HTTP URL for the endpoint to which a WebSocket (or SockJS)
|
|
client will need to connect to for the WebSocket handshake.
|
|
<2> STOMP messages whose destination header begins with `"/app"` are routed to
|
|
`@MessageMapping` methods in `@Controller` classes.
|
|
<3> Use the built-in, message broker for subscriptions and broadcasting;
|
|
Route messages whose destination header begins with "/topic" or "/queue" to the broker.
|
|
|
|
The same configuration in XML:
|
|
|
|
[source,xml,indent=0]
|
|
[subs="verbatim,quotes,attributes"]
|
|
----
|
|
<beans xmlns="http://www.springframework.org/schema/beans"
|
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
|
xmlns:websocket="http://www.springframework.org/schema/websocket"
|
|
xsi:schemaLocation="
|
|
http://www.springframework.org/schema/beans
|
|
https://www.springframework.org/schema/beans/spring-beans.xsd
|
|
http://www.springframework.org/schema/websocket
|
|
https://www.springframework.org/schema/websocket/spring-websocket.xsd">
|
|
|
|
<websocket:message-broker application-destination-prefix="/app">
|
|
<websocket:stomp-endpoint path="/portfolio">
|
|
<websocket:sockjs/>
|
|
</websocket:stomp-endpoint>
|
|
<websocket:simple-broker prefix="/topic, /queue"/>
|
|
</websocket:message-broker>
|
|
|
|
</beans>
|
|
----
|
|
|
|
[NOTE]
|
|
====
|
|
For the built-in, simple broker the "/topic" and "/queue" prefixes do not have any special
|
|
meaning. They're merely a convention to differentiate between pub-sub vs point-to-point
|
|
messaging (i.e. many subscribers vs one consumer). When using an external broker, please
|
|
check the STOMP page of the broker to understand what kind of STOMP destinations and
|
|
prefixes it supports.
|
|
====
|
|
|
|
To connect from a browser, for SockJS you can use the
|
|
https://github.com/sockjs/sockjs-client[sockjs-client]. For STOMP many applications have
|
|
used the https://github.com/jmesnil/stomp-websocket[jmesnil/stomp-websocket] library
|
|
(also known as stomp.js) which is feature complete and has been used in production for
|
|
years but is no longer maintained. At present the
|
|
https://github.com/JSteunou/webstomp-client[JSteunou/webstomp-client] is the most
|
|
actively maintained and evolving successor of that library and the example code below
|
|
is based on it:
|
|
|
|
[source,javascript,indent=0]
|
|
[subs="verbatim,quotes"]
|
|
----
|
|
var socket = new SockJS("/spring-websocket-portfolio/portfolio");
|
|
var stompClient = webstomp.over(socket);
|
|
|
|
stompClient.connect({}, function(frame) {
|
|
}
|
|
----
|
|
|
|
Or if connecting via WebSocket (without SockJS):
|
|
|
|
[source,javascript,indent=0]
|
|
[subs="verbatim,quotes"]
|
|
----
|
|
var socket = new WebSocket("/spring-websocket-portfolio/portfolio");
|
|
var stompClient = Stomp.over(socket);
|
|
|
|
stompClient.connect({}, function(frame) {
|
|
}
|
|
----
|
|
|
|
Note that the `stompClient` above does not need to specify `login` and `passcode` headers.
|
|
Even if it did, they would be ignored, or rather overridden, on the server side. See the
|
|
sections <<websocket-stomp-handle-broker-relay-configure>> and
|
|
<<websocket-stomp-authentication>> for more information on authentication.
|
|
|
|
For a more example code see:
|
|
|
|
* https://spring.io/guides/gs/messaging-stomp-websocket/[Using WebSocket to build an
|
|
interactive web application] getting started guide.
|
|
* https://github.com/rstoyanchev/spring-websocket-portfolio[Stock Portfolio] sample
|
|
application.
|
|
|
|
|
|
|
|
[[websocket-stomp-message-flow]]
|
|
=== Flow of Messages
|
|
|
|
Once a STOMP endpoint is exposed, the Spring application becomes a STOMP broker for
|
|
connected clients. This section describes the flow of messages on the server side.
|
|
|
|
The `spring-messaging` module contains foundational support for messaging applications
|
|
that originated in https://spring.io/spring-integration[Spring Integration] and was
|
|
later extracted and incorporated into the Spring Framework for broader use across many
|
|
https://spring.io/projects[Spring projects] and application scenarios.
|
|
Below is a list of a few of the available messaging abstractions:
|
|
|
|
* {api-spring-framework}/messaging/Message.html[Message] --
|
|
simple representation for a message including headers and payload.
|
|
* {api-spring-framework}/messaging/MessageHandler.html[MessageHandler] --
|
|
contract for handling a message.
|
|
* {api-spring-framework}/messaging/MessageChannel.html[MessageChannel] --
|
|
contract for sending a message that enables loose coupling between producers and consumers.
|
|
* {api-spring-framework}/messaging/SubscribableChannel.html[SubscribableChannel] --
|
|
`MessageChannel` with `MessageHandler` subscribers.
|
|
* {api-spring-framework}/messaging/support/ExecutorSubscribableChannel.html[ExecutorSubscribableChannel] --
|
|
`SubscribableChannel` that uses an `Executor` for delivering messages.
|
|
|
|
Both the Java config (i.e. `@EnableWebSocketMessageBroker`) and the XML namespace config
|
|
(i.e. `<websocket:message-broker>`) use the above components to assemble a message
|
|
workflow. The diagram below shows the components used when the simple, built-in message
|
|
broker is enabled:
|
|
|
|
image::images/message-flow-simple-broker.png[]
|
|
|
|
There are 3 message channels in the above diagram:
|
|
|
|
* `"clientInboundChannel"` -- for passing messages received from WebSocket clients.
|
|
* `"clientOutboundChannel"` -- for sending server messages to WebSocket clients.
|
|
* `"brokerChannel"` -- for sending messages to the message broker from within
|
|
server-side, application code.
|
|
|
|
The next diagram shows the components used when an external broker (e.g. RabbitMQ)
|
|
is configured for managing subscriptions and broadcasting messages:
|
|
|
|
image::images/message-flow-broker-relay.png[]
|
|
|
|
The main difference in the above diagram is the use of the "broker relay" for passing
|
|
messages up to the external STOMP broker over TCP, and for passing messages down from the
|
|
broker to subscribed clients.
|
|
|
|
When messages are received from a WebSocket connectin, they're decoded to STOMP frames,
|
|
then turned into a Spring `Message` representation, and sent to the
|
|
`"clientInboundChannel"` for further processing. For example STOMP messages whose
|
|
destination header starts with `"/app"` may be routed to `@MessageMapping` methods in
|
|
annotated controllers, while `"/topic"` and `"/queue"` messages may be routed directly
|
|
to the message broker.
|
|
|
|
An annotated `@Controller` handling a STOMP message from a client may send a message to
|
|
the message broker through the `"brokerChannel"`, and the broker will broadcast the
|
|
message to matching subscribers through the `"clientOutboundChannel"`. The same
|
|
controller can also do the same in response to HTTP requests, so a client may perform an
|
|
HTTP POST and then an `@PostMapping` method can send a message to the message broker
|
|
to broadcast to subscribed clients.
|
|
|
|
Let's trace the flow through a simple example. Given the following server setup:
|
|
|
|
[source,java,indent=0]
|
|
[subs="verbatim,quotes"]
|
|
----
|
|
@Configuration
|
|
@EnableWebSocketMessageBroker
|
|
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
|
|
|
|
@Override
|
|
public void registerStompEndpoints(StompEndpointRegistry registry) {
|
|
registry.addEndpoint("/portfolio");
|
|
}
|
|
|
|
@Override
|
|
public void configureMessageBroker(MessageBrokerRegistry registry) {
|
|
registry.setApplicationDestinationPrefixes("/app");
|
|
registry.enableSimpleBroker("/topic");
|
|
}
|
|
|
|
}
|
|
|
|
@Controller
|
|
public class GreetingController {
|
|
|
|
@MessageMapping("/greeting") {
|
|
public String handle(String greeting) {
|
|
return "[" + getTimestamp() + ": " + greeting;
|
|
}
|
|
|
|
}
|
|
|
|
----
|
|
|
|
. Client connects to `"http://localhost:8080/portfolio"` and once a WebSocket connection
|
|
is established, STOMP frames begin to flow on it.
|
|
. Client sends SUBSCRIBE frame with destination header `"/topic/greeting"`. Once received
|
|
and decoded, the message is sent to the `"clientInboundChannel"`, then routed to the
|
|
message broker which stores the client subscription.
|
|
. Client sends SEND frame to `"/app/greeting"`. The `"/app"` prefix helps to route it to
|
|
annotated controllers. After the `"/app"` prefix is stripped, the remaining `"/greeting"`
|
|
part of the destination is mapped to the `@MessageMapping` method in `GreetingController`.
|
|
. The value returned from `GreetingController` is turned into a Spring `Message` with
|
|
a payload based on the return value and a default destination header of
|
|
`"/topic/greeting"` (derived from the input destination with `"/app"` replaced by
|
|
`"/topic"`). The resulting message is sent to the "brokerChannel" and handled
|
|
by the message broker.
|
|
. The message broker finds all matching subscribers, and sends a MESSAGE frame to each
|
|
through the `"clientOutboundChannel"` from where messages are encoded as STOMP frames
|
|
and sent on the WebSocket connection.
|
|
|
|
The next section provides more details on annotated methods including the
|
|
kinds of arguments and return values supported.
|
|
|
|
|
|
|
|
[[websocket-stomp-handle-annotations]]
|
|
=== Annotated Controllers
|
|
|
|
Applications can use annotated `@Controller` classes to handle messages from clients.
|
|
Such classes can declare `@MessageMapping`, `@SubscribeMapping`, and `@ExceptionHandler`
|
|
methods as described next.
|
|
|
|
|
|
[[websocket-stomp-message-mapping]]
|
|
==== `@MessageMapping`
|
|
|
|
The `@MessageMapping` annotation can be used on methods to route messages based on their
|
|
destination. It is supported at the method level as well as at the type level. At type
|
|
level `@MessageMapping` is used to express shared mappings across all methods in a
|
|
controller.
|
|
|
|
By default destination mappings are expected to be Ant-style, path patterns, e.g. "/foo*",
|
|
"/foo/**". The patterns include support for template variables, e.g. "/foo/{id}", that can
|
|
be referenced with `@DestinationVariable` method arguments.
|
|
|
|
[TIP]
|
|
====
|
|
Applications can choose to switch to a dot-separated destination convention.
|
|
See <<websocket-stomp-destination-separator>>.
|
|
====
|
|
|
|
`@MessageMapping` methods can have flexible signatures with the following arguments:
|
|
|
|
[cols="1,2", options="header"]
|
|
|===
|
|
| Method argument | Description
|
|
|
|
| `Message`
|
|
| For access to the complete message.
|
|
|
|
| `MessageHeaders`
|
|
| For access to the headers within the `Message`.
|
|
|
|
| `MessageHeaderAccessor`, `SimpMessageHeaderAccessor`, `StompHeaderAccessor`
|
|
| For access to the headers via typed accessor methods.
|
|
|
|
| `@Payload`
|
|
| For access to the payload of the message, converted (e.g. from JSON) via a configured
|
|
`MessageConverter`.
|
|
|
|
The presence of this annotation is not required since it is assumed by default if no
|
|
other argument is matched.
|
|
|
|
Payload arguments may be annotated with `@javax.validation.Valid` or Spring's `@Validated`
|
|
in order to be automatically validated.
|
|
|
|
| `@Header`
|
|
| For access to a specific header value along with type conversion using an
|
|
`org.springframework.core.convert.converter.Converter` if necessary.
|
|
|
|
| `@Headers`
|
|
| For access to all headers in the message. This argument must be assignable to
|
|
`java.util.Map`.
|
|
|
|
| `@DestinationVariable`
|
|
| For access to template variables extracted from the message destination.
|
|
Values will be converted to the declared method argument type as necessary.
|
|
|
|
| `java.security.Principal`
|
|
| Reflects the user logged in at the time of the WebSocket HTTP handshake.
|
|
|
|
|===
|
|
|
|
When an `@MessageMapping` method returns a value, by default the value is serialized to
|
|
a payload through a configured `MessageConverter`, and then sent as a `Message` to the
|
|
`"brokerChannel"` from where it is broadcast to subscribers. The destination of the
|
|
outbound message is the same as that of the inbound message but prefixed with `"/topic"`.
|
|
|
|
You can use the `@SendTo` method annotation to customize the destination to send
|
|
the payload to. `@SendTo` can also be used at the class level to share a default target
|
|
destination to send messages to. `@SendToUser` is an variant for sending messages only to
|
|
the user associated with a message. See <<websocket-stomp-user-destination>> for details.
|
|
|
|
The return value from an `@MessageMapping` method may be wrapped with `ListenableFuture`,
|
|
`CompletableFuture`, or `CompletionStage` in order to produce the payload asynchronously.
|
|
|
|
As an alternative to returning a payload from an `@MessageMapping` method you can also
|
|
send messages using the `SimpMessagingTemplate`, which is also how return values are
|
|
handled under the covers. See <<websocket-stomp-handle-send>>.
|
|
|
|
|
|
[[websocket-stomp-subscribe-mapping]]
|
|
==== `@SubscribeMapping`
|
|
|
|
`@SubscribeMapping` is similar to `@MessageMapping` but narrows the mapping to
|
|
subscription messages only. It supports the same
|
|
<<websocket-stomp-message-mapping,method arguments>> as `@MessageMapping` does. However
|
|
for the return value, by default a message is sent directly to the client via
|
|
"clientOutboundChannel" in response to the subscription, and not to the broker via
|
|
"brokerChannel" as a broadcast to matching subscriptions. Adding `@SendTo` or
|
|
`@SendToUser` overrides this behavior and sends to the broker instead.
|
|
|
|
When is this useful? Let's assume the broker is mapped to "/topic" and "/queue" while
|
|
application controllers are mapped to "/app". In this setup, the broker *stores* all
|
|
subscriptions to "/topic" and "/queue" that are intended for *repeated* broadcasts, and
|
|
there is no need for the application to get involved. A client could also also subscribe to
|
|
some "/app" destination and a controller could return a value in response to that
|
|
subscription without involving the broker, effectively a *one-off*, *request-reply* exchange,
|
|
without storing or using the subscription again. One case for this is populating a UI
|
|
with initial data on startup.
|
|
|
|
When is this not useful? Do not try to map broker and controllers to the same destination
|
|
prefix unless you want both to process messages, including subscriptions, independently
|
|
for some reason. Inbound messages are handled in parallel. There are no guarantees whether
|
|
broker or controller will process a given message first. If the goal is to be notified
|
|
when a subscription is stored and ready for broadcasts, then a client should ask for a
|
|
receipt if the server supports it (simple broker does not). For example with the Java
|
|
<<websocket-stomp-client>>:
|
|
|
|
[source,java,indent=0]
|
|
[subs="verbatim,quotes"]
|
|
----
|
|
@Autowired
|
|
private TaskScheduler messageBrokerTaskScheduler;
|
|
|
|
// During initialization..
|
|
stompClient.setTaskScheduler(this.messageBrokerTaskScheduler);
|
|
|
|
// When subscribing..
|
|
StompHeaders headers = new StompHeaders();
|
|
headers.setDestination("/topic/...");
|
|
headers.setReceipt("r1");
|
|
FrameHandler handler = ...;
|
|
stompSession.subscribe(headers, handler).addReceiptTask(() -> {
|
|
// Subscription ready...
|
|
});
|
|
----
|
|
|
|
A server side option is <<websocket-stomp-interceptors,to register>> an
|
|
`ExecutorChannelInterceptor` on the `brokerChannel` and implement the `afterMessageHandled`
|
|
method that is invoked after messages, including subscriptions, have been handled.
|
|
|
|
|
|
[[websocket-stomp-exception-handler]]
|
|
==== `@MessageExceptionHandler`
|
|
|
|
An application can use `@MessageExceptionHandler` methods to handle exceptions from
|
|
`@MessageMapping` methods. Exceptions of interest can be declared in the annotation
|
|
itself, or through a method argument if you want to get access to the exception instance:
|
|
|
|
[source,java,indent=0]
|
|
[subs="verbatim,quotes"]
|
|
----
|
|
@Controller
|
|
public class MyController {
|
|
|
|
// ...
|
|
|
|
@MessageExceptionHandler
|
|
public ApplicationError handleException(MyException exception) {
|
|
// ...
|
|
return appError;
|
|
}
|
|
}
|
|
----
|
|
|
|
`@MessageExceptionHandler` methods support flexible method signatures and support the same
|
|
method argument types and return values as <<websocket-stomp-message-mapping>> methods.
|
|
|
|
Typically `@MessageExceptionHandler` methods apply within the `@Controller` class (or
|
|
class hierarchy) they are declared in. If you want such methods to apply more globally,
|
|
across controllers, you can declare them in a class marked with `@ControllerAdvice`.
|
|
This is comparable to <<web.adoc#mvc-ann-controller-advice,similar support>> in Spring MVC.
|
|
|
|
|
|
|
|
[[websocket-stomp-handle-send]]
|
|
=== Send Messages
|
|
|
|
What if you want to send messages to connected clients from any part of the
|
|
application? Any application component can send messages to the `"brokerChannel"`.
|
|
The easiest way to do that is to have a `SimpMessagingTemplate` injected, and
|
|
use it to send messages. Typically it should be easy to have it injected by
|
|
type, for example:
|
|
|
|
[source,java,indent=0]
|
|
[subs="verbatim,quotes"]
|
|
----
|
|
@Controller
|
|
public class GreetingController {
|
|
|
|
private SimpMessagingTemplate template;
|
|
|
|
@Autowired
|
|
public GreetingController(SimpMessagingTemplate template) {
|
|
this.template = template;
|
|
}
|
|
|
|
@RequestMapping(path="/greetings", method=POST)
|
|
public void greet(String greeting) {
|
|
String text = "[" + getTimestamp() + "]:" + greeting;
|
|
this.template.convertAndSend("/topic/greetings", text);
|
|
}
|
|
|
|
}
|
|
----
|
|
|
|
But it can also be qualified by its name "brokerMessagingTemplate" if another
|
|
bean of the same type exists.
|
|
|
|
|
|
|
|
[[websocket-stomp-handle-simple-broker]]
|
|
=== Simple Broker
|
|
|
|
The built-in, simple message broker handles subscription requests from clients,
|
|
stores them in memory, and broadcasts messages to connected clients with matching
|
|
destinations. The broker supports path-like destinations, including subscriptions
|
|
to Ant-style destination patterns.
|
|
|
|
[NOTE]
|
|
====
|
|
Applications can also use dot-separated destinations (vs slash).
|
|
See <<websocket-stomp-destination-separator>>.
|
|
====
|
|
|
|
|
|
|
|
[[websocket-stomp-handle-broker-relay]]
|
|
=== External Broker
|
|
|
|
The simple broker is great for getting started but supports only a subset of
|
|
STOMP commands (e.g. no acks, receipts, etc.), relies on a simple message
|
|
sending loop, and is not suitable for clustering. As an alternative, applications
|
|
can upgrade to using a full-featured message broker.
|
|
|
|
Check the STOMP documentation for your message broker of choice (e.g.
|
|
https://www.rabbitmq.com/stomp.html[RabbitMQ],
|
|
https://activemq.apache.org/stomp.html[ActiveMQ], etc.), install the broker,
|
|
and run it with STOMP support enabled. Then enable the STOMP broker relay in the
|
|
Spring configuration instead of the simple broker.
|
|
|
|
Below is example configuration that enables a full-featured broker:
|
|
|
|
[source,java,indent=0]
|
|
[subs="verbatim,quotes"]
|
|
----
|
|
@Configuration
|
|
@EnableWebSocketMessageBroker
|
|
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
|
|
|
|
@Override
|
|
public void registerStompEndpoints(StompEndpointRegistry registry) {
|
|
registry.addEndpoint("/portfolio").withSockJS();
|
|
}
|
|
|
|
@Override
|
|
public void configureMessageBroker(MessageBrokerRegistry registry) {
|
|
registry.enableStompBrokerRelay("/topic", "/queue");
|
|
registry.setApplicationDestinationPrefixes("/app");
|
|
}
|
|
|
|
}
|
|
----
|
|
|
|
XML configuration equivalent:
|
|
|
|
[source,xml,indent=0]
|
|
[subs="verbatim,quotes,attributes"]
|
|
----
|
|
<beans xmlns="http://www.springframework.org/schema/beans"
|
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
|
xmlns:websocket="http://www.springframework.org/schema/websocket"
|
|
xsi:schemaLocation="
|
|
http://www.springframework.org/schema/beans
|
|
https://www.springframework.org/schema/beans/spring-beans.xsd
|
|
http://www.springframework.org/schema/websocket
|
|
https://www.springframework.org/schema/websocket/spring-websocket.xsd">
|
|
|
|
<websocket:message-broker application-destination-prefix="/app">
|
|
<websocket:stomp-endpoint path="/portfolio" />
|
|
<websocket:sockjs/>
|
|
</websocket:stomp-endpoint>
|
|
<websocket:stomp-broker-relay prefix="/topic,/queue" />
|
|
</websocket:message-broker>
|
|
|
|
</beans>
|
|
----
|
|
|
|
The "STOMP broker relay" in the above configuration is a Spring
|
|
{api-spring-framework}/messaging/MessageHandler.html[MessageHandler]
|
|
that handles messages by forwarding them to an external message broker.
|
|
To do so it establishes TCP connections to the broker, forwards all
|
|
messages to it, and then forwards all messages received
|
|
from the broker to clients through their WebSocket sessions. Essentially
|
|
it acts as a "relay" that forwards messages in both directions.
|
|
|
|
[NOTE]
|
|
====
|
|
Please add `io.projectreactor.ipc:reactor-netty` and `io.netty:netty-all`
|
|
dependencies to your project for TCP connection management.
|
|
====
|
|
|
|
Furthermore, application components (e.g. HTTP request handling methods,
|
|
business services, etc.) can also send messages to the broker relay, as described
|
|
in <<websocket-stomp-handle-send>>, in order to broadcast messages to
|
|
subscribed WebSocket clients.
|
|
|
|
In effect, the broker relay enables robust and scalable message broadcasting.
|
|
|
|
|
|
|
|
[[websocket-stomp-handle-broker-relay-configure]]
|
|
=== Connect to Broker
|
|
|
|
A STOMP broker relay maintains a single "system" TCP connection to the broker.
|
|
This connection is used for messages originating from the server-side application
|
|
only, not for receiving messages. You can configure the STOMP credentials
|
|
for this connection, i.e. the STOMP frame `login` and `passcode` headers. This
|
|
is exposed in both the XML namespace and the Java config as the
|
|
``systemLogin``/``systemPasscode`` properties with default values ``guest``/``guest``.
|
|
|
|
The STOMP broker relay also creates a separate TCP connection for every connected
|
|
WebSocket client. You can configure the STOMP credentials to use for all TCP
|
|
connections created on behalf of clients. This is exposed in both the XML namespace
|
|
and the Java config as the ``clientLogin``/``clientPasscode`` properties with default
|
|
values ``guest``/``guest``.
|
|
|
|
[NOTE]
|
|
====
|
|
The STOMP broker relay always sets the `login` and `passcode` headers on every `CONNECT`
|
|
frame that it forwards to the broker on behalf of clients. Therefore WebSocket clients
|
|
need not set those headers; they will be ignored. As the <<websocket-stomp-authentication>>
|
|
section explains, instead WebSocket clients should rely on HTTP authentication to protect
|
|
the WebSocket endpoint and establish the client identity.
|
|
====
|
|
|
|
The STOMP broker relay also sends and receives heartbeats to and from the message
|
|
broker over the "system" TCP connection. You can configure the intervals for sending
|
|
and receiving heartbeats (10 seconds each by default). If connectivity to the broker
|
|
is lost, the broker relay will continue to try to reconnect, every 5 seconds,
|
|
until it succeeds.
|
|
|
|
Any Spring bean can implement `ApplicationListener<BrokerAvailabilityEvent>` in order
|
|
to receive notifications when the "system" connection to the broker is lost and
|
|
re-established. For example a Stock Quote service broadcasting stock quotes can
|
|
stop trying to send messages when there is no active "system" connection.
|
|
|
|
By default, the STOMP broker relay always connects, and reconnects as needed if
|
|
connectivity is lost, to the same host and port. If you wish to supply multiple addresses,
|
|
on each attempt to connect, you can configure a supplier of addresses, instead of a
|
|
fixed host and port. For example:
|
|
|
|
[source,java,indent=0]
|
|
[subs="verbatim,quotes"]
|
|
----
|
|
@Configuration
|
|
@EnableWebSocketMessageBroker
|
|
public class WebSocketConfig extends AbstractWebSocketMessageBrokerConfigurer {
|
|
|
|
// ...
|
|
|
|
@Override
|
|
public void configureMessageBroker(MessageBrokerRegistry registry) {
|
|
registry.enableStompBrokerRelay("/queue/", "/topic/").setTcpClient(createTcpClient());
|
|
registry.setApplicationDestinationPrefixes("/app");
|
|
}
|
|
|
|
private ReactorNettyTcpClient<byte[]> createTcpClient() {
|
|
|
|
Consumer<ClientOptions.Builder<?>> builderConsumer = builder -> {
|
|
builder.connectAddress(()-> {
|
|
// Select address to connect to ...
|
|
});
|
|
};
|
|
|
|
return new ReactorNettyTcpClient<>(builderConsumer, new StompReactorNettyCodec());
|
|
}
|
|
}
|
|
----
|
|
|
|
The STOMP broker relay can also be configured with a `virtualHost` property.
|
|
The value of this property will be set as the `host` header of every `CONNECT` frame
|
|
and may be useful for example in a cloud environment where the actual host to which
|
|
the TCP connection is established is different from the host providing the
|
|
cloud-based STOMP service.
|
|
|
|
|
|
|
|
[[websocket-stomp-destination-separator]]
|
|
=== Dot as Separator
|
|
|
|
When messages are routed to `@MessageMapping` methods, they're matched with
|
|
`AntPathMatcher` and by default patterns are expected to use slash "/" as separator.
|
|
This is a good convention in a web applications and similar to HTTP URLs. However if
|
|
you are more used to messaging conventions, you can switch to using dot "." as separator.
|
|
|
|
In Java config:
|
|
|
|
[source,java,indent=0]
|
|
[subs="verbatim,quotes"]
|
|
----
|
|
@Configuration
|
|
@EnableWebSocketMessageBroker
|
|
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
|
|
|
|
// ...
|
|
|
|
@Override
|
|
public void configureMessageBroker(MessageBrokerRegistry registry) {
|
|
registry.setPathMatcher(**new AntPathMatcher("."));**
|
|
registry.enableStompBrokerRelay("/queue", "/topic");
|
|
registry.setApplicationDestinationPrefixes("/app");
|
|
}
|
|
}
|
|
----
|
|
|
|
In XML:
|
|
|
|
[source,xml,indent=0]
|
|
[subs="verbatim,quotes,attributes"]
|
|
----
|
|
<beans xmlns="http://www.springframework.org/schema/beans"
|
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
|
xmlns:websocket="http://www.springframework.org/schema/websocket"
|
|
xsi:schemaLocation="
|
|
http://www.springframework.org/schema/beans
|
|
https://www.springframework.org/schema/beans/spring-beans.xsd
|
|
http://www.springframework.org/schema/websocket
|
|
https://www.springframework.org/schema/websocket/spring-websocket.xsd">
|
|
|
|
<websocket:message-broker application-destination-prefix="/app" path-matcher="**pathMatcher**">
|
|
<websocket:stomp-endpoint path="/stomp"/>
|
|
<websocket:stomp-broker-relay prefix="/topic,/queue" />
|
|
</websocket:message-broker>
|
|
|
|
**
|
|
<bean id="pathMatcher" class="org.springframework.util.AntPathMatcher">
|
|
<constructor-arg index="0" value="."/>
|
|
</bean>
|
|
**
|
|
|
|
</beans>
|
|
----
|
|
|
|
After that a controller may use dot "." as separator in `@MessageMapping` methods:
|
|
|
|
[source,java,indent=0]
|
|
[subs="verbatim,quotes"]
|
|
----
|
|
@Controller
|
|
@MessageMapping("foo")
|
|
public class FooController {
|
|
|
|
@MessageMapping("bar.{baz}")
|
|
public void handleBaz(@DestinationVariable String baz) {
|
|
// ...
|
|
}
|
|
}
|
|
----
|
|
|
|
The client can now send a message to `"/app/foo.bar.baz123"`.
|
|
|
|
In the example above we did not change the prefixes on the "broker relay" because those
|
|
depend entirely on the external message broker. Check the STOMP documentation pages of
|
|
the broker you're using to see what conventions it supports for the destination header.
|
|
|
|
The "simple broker" on the other hand does rely on the configured `PathMatcher` so if
|
|
you switch the separator that will also apply to the broker and the way matches
|
|
destinations from a message to patterns in subscriptions.
|
|
|
|
|
|
|
|
[[websocket-stomp-authentication]]
|
|
=== Authentication
|
|
|
|
Every STOMP over WebSocket messaging session begins with an HTTP request --
|
|
that can be a request to upgrade to WebSockets (i.e. a WebSocket handshake)
|
|
or in the case of SockJS fallbacks a series of SockJS HTTP transport requests.
|
|
|
|
Web applications already have authentication and authorization in place to
|
|
secure HTTP requests. Typically a user is authenticated via Spring Security
|
|
using some mechanism such as a login page, HTTP basic authentication, or other.
|
|
The security context for the authenticated user is saved in the HTTP session
|
|
and is associated with subsequent requests in the same cookie-based session.
|
|
|
|
Therefore for a WebSocket handshake, or for SockJS HTTP transport requests,
|
|
typically there will already be an authenticated user accessible via
|
|
`HttpServletRequest#getUserPrincipal()`. Spring automatically associates that user
|
|
with a WebSocket or SockJS session created for them and subsequently with all
|
|
STOMP messages transported over that session through a user header.
|
|
|
|
In short there is nothing special a typical web application needs to do above
|
|
and beyond what it already does for security. The user is authenticated at
|
|
the HTTP request level with a security context maintained through a cookie-based
|
|
HTTP session which is then associated with WebSocket or SockJS sessions created
|
|
for that user and results in a user header stamped on every `Message` flowing
|
|
through the application.
|
|
|
|
Note that the STOMP protocol does have a "login" and "passcode" headers
|
|
on the `CONNECT` frame. Those were originally designed for and are still needed
|
|
for example for STOMP over TCP. However for STOMP over WebSocket by default
|
|
Spring ignores authorization headers at the STOMP protocol level and assumes
|
|
the user is already authenticated at the HTTP transport level and expects that
|
|
the WebSocket or SockJS session contain the authenticated user.
|
|
|
|
[NOTE]
|
|
====
|
|
Spring Security provides
|
|
https://docs.spring.io/spring-security/site/docs/current/reference/htmlsingle/#websocket[WebSocket sub-protocol authorization]
|
|
that uses a `ChannelInterceptor` to authorize messages based on the user header in them.
|
|
Also Spring Session provides a
|
|
https://docs.spring.io/spring-session/docs/current/reference/html5/#websocket[WebSocket integration]
|
|
that ensures the user HTTP session does not expire when the WebSocket session is still active.
|
|
====
|
|
|
|
|
|
|
|
[[websocket-stomp-authentication-token-based]]
|
|
=== Token Authentication
|
|
|
|
https://github.com/spring-projects/spring-security-oauth[Spring Security OAuth]
|
|
provides support for token based security including JSON Web Token (JWT).
|
|
This can be used as the authentication mechanism in Web applications
|
|
including STOMP over WebSocket interactions just as described in the previous
|
|
section, i.e. maintaining identity through a cookie-based session.
|
|
|
|
At the same time cookie-based sessions are not always the best fit for example
|
|
in applications that don't wish to maintain a server-side session at all or in
|
|
mobile applications where it's common to use headers for authentication.
|
|
|
|
The https://tools.ietf.org/html/rfc6455#section-10.5[WebSocket protocol RFC 6455]
|
|
"doesn't prescribe any particular way that servers can authenticate clients during
|
|
the WebSocket handshake." In practice however browser clients can only use standard
|
|
authentication headers (i.e. basic HTTP authentication) or cookies and cannot for example
|
|
provide custom headers. Likewise the SockJS JavaScript client does not provide
|
|
a way to send HTTP headers with SockJS transport requests, see
|
|
https://github.com/sockjs/sockjs-client/issues/196[sockjs-client issue 196].
|
|
Instead it does allow sending query parameters that can be used to send a token
|
|
but that has its own drawbacks, for example as the token may be inadvertently
|
|
logged with the URL in server logs.
|
|
|
|
[NOTE]
|
|
====
|
|
The above limitations are for browser-based clients and do not apply to the
|
|
Spring Java-based STOMP client which does support sending headers with both
|
|
WebSocket and SockJS requests.
|
|
====
|
|
|
|
Therefore applications that wish to avoid the use of cookies may not have any good
|
|
alternatives for authentication at the HTTP protocol level. Instead of using cookies
|
|
they may prefer to authenticate with headers at the STOMP messaging protocol level
|
|
There are 2 simple steps to doing that:
|
|
|
|
1. Use the STOMP client to pass authentication header(s) at connect time.
|
|
2. Process the authentication header(s) with a `ChannelInterceptor`.
|
|
|
|
Below is the example server-side configuration to register a custom authentication
|
|
interceptor. Note that an interceptor only needs to authenticate and set
|
|
the user header on the CONNECT `Message`. Spring will note and save the authenticated
|
|
user and associate it with subsequent STOMP messages on the same session:
|
|
|
|
[source,java,indent=0]
|
|
[subs="verbatim,quotes"]
|
|
----
|
|
@Configuration
|
|
@EnableWebSocketMessageBroker
|
|
public class MyConfig implements WebSocketMessageBrokerConfigurer {
|
|
|
|
@Override
|
|
public void configureClientInboundChannel(ChannelRegistration registration) {
|
|
registration.interceptors(new ChannelInterceptor() {
|
|
@Override
|
|
public Message<?> preSend(Message<?> message, MessageChannel channel) {
|
|
StompHeaderAccessor accessor =
|
|
MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor.class);
|
|
if (StompCommand.CONNECT.equals(accessor.getCommand())) {
|
|
Authentication user = ... ; // access authentication header(s)
|
|
accessor.setUser(user);
|
|
}
|
|
return message;
|
|
}
|
|
});
|
|
}
|
|
}
|
|
----
|
|
|
|
Also note that when using Spring Security's authorization for messages, at present
|
|
you will need to ensure that the authentication `ChannelInterceptor` config is ordered
|
|
ahead of Spring Security's. This is best done by declaring the custom interceptor in
|
|
its own implementation of `WebSocketMessageBrokerConfigurer` marked with
|
|
`@Order(Ordered.HIGHEST_PRECEDENCE + 99)`.
|
|
|
|
|
|
|
|
[[websocket-stomp-user-destination]]
|
|
=== User Destinations
|
|
|
|
An application can send messages targeting a specific user, and Spring's STOMP support
|
|
recognizes destinations prefixed with `"/user/"` for this purpose.
|
|
For example, a client might subscribe to the destination `"/user/queue/position-updates"`.
|
|
This destination will be handled by the `UserDestinationMessageHandler` and
|
|
transformed into a destination unique to the user session,
|
|
e.g. `"/queue/position-updates-user123"`. This provides the convenience of subscribing
|
|
to a generically named destination while at the same time ensuring no collisions
|
|
with other users subscribing to the same destination so that each user can receive
|
|
unique stock position updates.
|
|
|
|
On the sending side messages can be sent to a destination such as
|
|
`"/user/{username}/queue/position-updates"`, which in turn will be translated
|
|
by the `UserDestinationMessageHandler` into one or more destinations, one for each
|
|
session associated with the user. This allows any component within the application to
|
|
send messages targeting a specific user without necessarily knowing anything more
|
|
than their name and the generic destination. This is also supported through an
|
|
annotation as well as a messaging template.
|
|
|
|
For example, a message-handling method can send messages to the user associated with
|
|
the message being handled through the `@SendToUser` annotation (also supported on
|
|
the class-level to share a common destination):
|
|
|
|
[source,java,indent=0]
|
|
[subs="verbatim,quotes"]
|
|
----
|
|
@Controller
|
|
public class PortfolioController {
|
|
|
|
@MessageMapping("/trade")
|
|
@SendToUser("/queue/position-updates")
|
|
public TradeResult executeTrade(Trade trade, Principal principal) {
|
|
// ...
|
|
return tradeResult;
|
|
}
|
|
}
|
|
----
|
|
|
|
If the user has more than one session, by default all of the sessions subscribed
|
|
to the given destination are targeted. However sometimes, it may be necessary to
|
|
target only the session that sent the message being handled. This can be done by
|
|
setting the `broadcast` attribute to false, for example:
|
|
|
|
[source,java,indent=0]
|
|
[subs="verbatim,quotes"]
|
|
----
|
|
@Controller
|
|
public class MyController {
|
|
|
|
@MessageMapping("/action")
|
|
public void handleAction() throws Exception{
|
|
// raise MyBusinessException here
|
|
}
|
|
|
|
@MessageExceptionHandler
|
|
@SendToUser(destinations="/queue/errors", broadcast=false)
|
|
public ApplicationError handleException(MyBusinessException exception) {
|
|
// ...
|
|
return appError;
|
|
}
|
|
}
|
|
----
|
|
|
|
[NOTE]
|
|
====
|
|
While user destinations generally imply an authenticated user, it isn't required
|
|
strictly. A WebSocket session that is not associated with an authenticated user
|
|
can subscribe to a user destination. In such cases the `@SendToUser` annotation
|
|
will behave exactly the same as with `broadcast=false`, i.e. targeting only the
|
|
session that sent the message being handled.
|
|
====
|
|
|
|
It is also possible to send a message to user destinations from any application
|
|
component by injecting the `SimpMessagingTemplate` created by the Java config or
|
|
XML namespace, for example (the bean name is `"brokerMessagingTemplate"` if required
|
|
for qualification with `@Qualifier`):
|
|
|
|
[source,java,indent=0]
|
|
[subs="verbatim,quotes"]
|
|
----
|
|
@Service
|
|
public class TradeServiceImpl implements TradeService {
|
|
|
|
private final SimpMessagingTemplate messagingTemplate;
|
|
|
|
@Autowired
|
|
public TradeServiceImpl(SimpMessagingTemplate messagingTemplate) {
|
|
this.messagingTemplate = messagingTemplate;
|
|
}
|
|
|
|
// ...
|
|
|
|
public void afterTradeExecuted(Trade trade) {
|
|
this.messagingTemplate.convertAndSendToUser(
|
|
trade.getUserName(), "/queue/position-updates", trade.getResult());
|
|
}
|
|
}
|
|
----
|
|
|
|
[NOTE]
|
|
====
|
|
When using user destinations with an external message broker, check the broker
|
|
documentation on how to manage inactive queues, so that when the user session is
|
|
over, all unique user queues are removed. For example, RabbitMQ creates auto-delete
|
|
queues when destinations like `/exchange/amq.direct/position-updates` are used.
|
|
So in that case the client could subscribe to `/user/exchange/amq.direct/position-updates`.
|
|
Similarly, ActiveMQ has
|
|
https://activemq.apache.org/delete-inactive-destinations.html[configuration options]
|
|
for purging inactive destinations.
|
|
====
|
|
|
|
In a multi-application server scenario a user destination may remain unresolved because
|
|
the user is connected to a different server. In such cases you can configure a
|
|
destination to broadcast unresolved messages to so that other servers have a chance to try.
|
|
This can be done through the `userDestinationBroadcast` property of the
|
|
`MessageBrokerRegistry` in Java config and the `user-destination-broadcast` attribute
|
|
of the `message-broker` element in XML.
|
|
|
|
|
|
|
|
[[websocket-stomp-appplication-context-events]]
|
|
=== Events
|
|
|
|
Several `ApplicationContext` events (listed below) are published and can be
|
|
received by implementing Spring's `ApplicationListener` interface.
|
|
|
|
* `BrokerAvailabilityEvent` -- indicates when the broker becomes available/unavailable.
|
|
While the "simple" broker becomes available immediately on startup and remains so while
|
|
the application is running, the STOMP "broker relay" may lose its connection
|
|
to the full featured broker, for example if the broker is restarted. The broker relay
|
|
has reconnect logic and will re-establish the "system" connection to the broker
|
|
when it comes back, hence this event is published whenever the state changes from connected
|
|
to disconnected and vice versa. Components using the `SimpMessagingTemplate` should
|
|
subscribe to this event and avoid sending messages at times when the broker is not
|
|
available. In any case they should be prepared to handle `MessageDeliveryException`
|
|
when sending a message.
|
|
* `SessionConnectEvent` -- published when a new STOMP CONNECT is received
|
|
indicating the start of a new client session. The event contains the message representing the
|
|
connect including the session id, user information (if any), and any custom headers the client
|
|
may have sent. This is useful for tracking client sessions. Components subscribed
|
|
to this event can wrap the contained message using `SimpMessageHeaderAccessor` or
|
|
`StompMessageHeaderAccessor`.
|
|
* `SessionConnectedEvent` -- published shortly after a `SessionConnectEvent` when the
|
|
broker has sent a STOMP CONNECTED frame in response to the CONNECT. At this point the
|
|
STOMP session can be considered fully established.
|
|
* `SessionSubscribeEvent` -- published when a new STOMP SUBSCRIBE is received.
|
|
* `SessionUnsubscribeEvent` -- published when a new STOMP UNSUBSCRIBE is received.
|
|
* `SessionDisconnectEvent` -- published when a STOMP session ends. The DISCONNECT may
|
|
have been sent from the client, or it may also be automatically generated when the
|
|
WebSocket session is closed. In some cases this event may be published more than once
|
|
per session. Components should be idempotent with regard to multiple disconnect events.
|
|
|
|
[NOTE]
|
|
====
|
|
When using a full-featured broker, the STOMP "broker relay" automatically reconnects the
|
|
"system" connection in case the broker becomes temporarily unavailable. Client connections
|
|
however are not automatically reconnected. Assuming heartbeats are enabled, the client
|
|
will typically notice the broker is not responding within 10 seconds. Clients need to
|
|
implement their own reconnect logic.
|
|
====
|
|
|
|
|
|
|
|
[[websocket-stomp-interceptors]]
|
|
=== Interception
|
|
|
|
<<websocket-stomp-appplication-context-events>> provide notifications for the lifecycle
|
|
of a STOMP connection and not for every client message. Applications can also register a
|
|
`ChannelInterceptor` to intercept any message, and in any part of the processing chain.
|
|
For example to intercept inbound messages from clients:
|
|
|
|
[source,java,indent=0]
|
|
[subs="verbatim,quotes"]
|
|
----
|
|
@Configuration
|
|
@EnableWebSocketMessageBroker
|
|
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
|
|
|
|
@Override
|
|
public void configureClientInboundChannel(ChannelRegistration registration) {
|
|
registration.interceptors(new MyChannelInterceptor());
|
|
}
|
|
}
|
|
----
|
|
|
|
A custom `ChannelInterceptor` can use `StompHeaderAccessor` or `SimpMessageHeaderAccessor`
|
|
to access information about the message.
|
|
|
|
[source,java,indent=0]
|
|
[subs="verbatim,quotes"]
|
|
----
|
|
public class MyChannelInterceptor implements ChannelInterceptor {
|
|
|
|
@Override
|
|
public Message<?> preSend(Message<?> message, MessageChannel channel) {
|
|
StompHeaderAccessor accessor = StompHeaderAccessor.wrap(message);
|
|
StompCommand command = accessor.getStompCommand();
|
|
// ...
|
|
return message;
|
|
}
|
|
}
|
|
----
|
|
|
|
Applications may also implement `ExecutorChannelInterceptor` which is a sub-interface
|
|
of `ChannelInterceptor` with callbacks in the thread in which the messages are handled.
|
|
While a `ChannelInterceptor` is invoked once for per message sent to a channel, the
|
|
`ExecutorChannelInterceptor` provides hooks in the thread of each `MessageHandler`
|
|
subscribed to messages from the channel.
|
|
|
|
Note that just like with the `SesionDisconnectEvent` above, a DISCONNECT message
|
|
may have been sent from the client, or it may also be automatically generated when
|
|
the WebSocket session is closed. In some cases an interceptor may intercept this
|
|
message more than once per session. Components should be idempotent with regard to
|
|
multiple disconnect events.
|
|
|
|
|
|
|
|
[[websocket-stomp-client]]
|
|
=== STOMP Client
|
|
|
|
Spring provides a STOMP over WebSocket client and a STOMP over TCP client.
|
|
|
|
To begin create and configure `WebSocketStompClient`:
|
|
|
|
[source,java,indent=0]
|
|
[subs="verbatim,quotes"]
|
|
----
|
|
WebSocketClient webSocketClient = new StandardWebSocketClient();
|
|
WebSocketStompClient stompClient = new WebSocketStompClient(webSocketClient);
|
|
stompClient.setMessageConverter(new StringMessageConverter());
|
|
stompClient.setTaskScheduler(taskScheduler); // for heartbeats
|
|
----
|
|
|
|
In the above example `StandardWebSocketClient` could be replaced with `SockJsClient`
|
|
since that is also an implementation of `WebSocketClient`. The `SockJsClient` can
|
|
use WebSocket or HTTP-based transport as a fallback. For more details see
|
|
<<websocket-fallback-sockjs-client>>.
|
|
|
|
Next establish a connection and provide a handler for the STOMP session:
|
|
|
|
[source,java,indent=0]
|
|
[subs="verbatim,quotes"]
|
|
----
|
|
String url = "ws://127.0.0.1:8080/endpoint";
|
|
StompSessionHandler sessionHandler = new MyStompSessionHandler();
|
|
stompClient.connect(url, sessionHandler);
|
|
----
|
|
|
|
When the session is ready for use the handler is notified:
|
|
|
|
[source,java,indent=0]
|
|
[subs="verbatim,quotes"]
|
|
----
|
|
public class MyStompSessionHandler extends StompSessionHandlerAdapter {
|
|
|
|
@Override
|
|
public void afterConnected(StompSession session, StompHeaders connectedHeaders) {
|
|
// ...
|
|
}
|
|
}
|
|
----
|
|
|
|
Once the session is established any payload can be sent and that will be
|
|
serialized with the configured `MessageConverter`:
|
|
|
|
[source,java,indent=0]
|
|
[subs="verbatim,quotes"]
|
|
----
|
|
session.send("/topic/foo", "payload");
|
|
----
|
|
|
|
You can also subscribe to destinations. The `subscribe` methods require a handler
|
|
for messages on the subscription and return a `Subscription` handle that can be
|
|
used to unsubscribe. For each received message the handler can specify the target
|
|
Object type the payload should be deserialized to:
|
|
|
|
[source,java,indent=0]
|
|
[subs="verbatim,quotes"]
|
|
----
|
|
session.subscribe("/topic/foo", new StompFrameHandler() {
|
|
|
|
@Override
|
|
public Type getPayloadType(StompHeaders headers) {
|
|
return String.class;
|
|
}
|
|
|
|
@Override
|
|
public void handleFrame(StompHeaders headers, Object payload) {
|
|
// ...
|
|
}
|
|
|
|
});
|
|
----
|
|
|
|
To enable STOMP heartbeat configure `WebSocketStompClient` with a `TaskScheduler`
|
|
and optionally customize the heartbeat intervals, 10 seconds for write inactivity
|
|
which causes a heartbeat to be sent and 10 seconds for read inactivity which
|
|
closes the connection.
|
|
|
|
[NOTE]
|
|
====
|
|
When using `WebSocketStompClient` for performance tests to simulate thousands
|
|
of clients from the same machine consider turning off heartbeats since each
|
|
connection schedules its own heartbeat tasks and that's not optimized for a
|
|
a large number of clients running on the same machine.
|
|
====
|
|
|
|
|
|
The STOMP protocol also supports receipts where the client must add a "receipt"
|
|
header to which the server responds with a RECEIPT frame after the send or
|
|
subscribe are processed. To support this the `StompSession` offers
|
|
`setAutoReceipt(boolean)` that causes a "receipt" header to be
|
|
added on every subsequent send or subscribe.
|
|
Alternatively you can also manually add a "receipt" header to the `StompHeaders`.
|
|
Both send and subscribe return an instance of `Receiptable`
|
|
that can be used to register for receipt success and failure callbacks.
|
|
For this feature the client must be configured with a `TaskScheduler`
|
|
and the amount of time before a receipt expires (15 seconds by default).
|
|
|
|
Note that `StompSessionHandler` itself is a `StompFrameHandler` which allows
|
|
it to handle ERROR frames in addition to the `handleException` callback for
|
|
exceptions from the handling of messages, and `handleTransportError` for
|
|
transport-level errors including `ConnectionLostException`.
|
|
|
|
|
|
|
|
[[websocket-stomp-websocket-scope]]
|
|
=== WebSocket Scope
|
|
|
|
Each WebSocket session has a map of attributes. The map is attached as a header to
|
|
inbound client messages and may be accessed from a controller method, for example:
|
|
|
|
[source,java,indent=0]
|
|
[subs="verbatim,quotes"]
|
|
----
|
|
@Controller
|
|
public class MyController {
|
|
|
|
@MessageMapping("/action")
|
|
public void handle(SimpMessageHeaderAccessor headerAccessor) {
|
|
Map<String, Object> attrs = headerAccessor.getSessionAttributes();
|
|
// ...
|
|
}
|
|
}
|
|
----
|
|
|
|
It is also possible to declare a Spring-managed bean in the `websocket` scope.
|
|
WebSocket-scoped beans can be injected into controllers and any channel interceptors
|
|
registered on the "clientInboundChannel". Those are typically singletons and live
|
|
longer than any individual WebSocket session. Therefore you will need to use a
|
|
scope proxy mode for WebSocket-scoped beans:
|
|
|
|
[source,java,indent=0]
|
|
[subs="verbatim,quotes"]
|
|
----
|
|
@Component
|
|
@Scope(scopeName = "websocket", proxyMode = ScopedProxyMode.TARGET_CLASS)
|
|
public class MyBean {
|
|
|
|
@PostConstruct
|
|
public void init() {
|
|
// Invoked after dependencies injected
|
|
}
|
|
|
|
// ...
|
|
|
|
@PreDestroy
|
|
public void destroy() {
|
|
// Invoked when the WebSocket session ends
|
|
}
|
|
}
|
|
|
|
@Controller
|
|
public class MyController {
|
|
|
|
private final MyBean myBean;
|
|
|
|
@Autowired
|
|
public MyController(MyBean myBean) {
|
|
this.myBean = myBean;
|
|
}
|
|
|
|
@MessageMapping("/action")
|
|
public void handle() {
|
|
// this.myBean from the current WebSocket session
|
|
}
|
|
}
|
|
----
|
|
|
|
As with any custom scope, Spring initializes a new `MyBean` instance the first
|
|
time it is accessed from the controller and stores the instance in the WebSocket
|
|
session attributes. The same instance is returned subsequently until the session
|
|
ends. WebSocket-scoped beans will have all Spring lifecycle methods invoked as
|
|
shown in the examples above.
|
|
|
|
|
|
|
|
[[websocket-stomp-configuration-performance]]
|
|
=== Performance
|
|
|
|
There is no silver bullet when it comes to performance. Many factors may
|
|
affect it including the size of messages, the volume, whether application
|
|
methods perform work that requires blocking, as well as external factors
|
|
such as network speed and others. The goal of this section is to provide
|
|
an overview of the available configuration options along with some thoughts
|
|
on how to reason about scaling.
|
|
|
|
In a messaging application messages are passed through channels for asynchronous
|
|
executions backed by thread pools. Configuring such an application requires
|
|
good knowledge of the channels and the flow of messages. Therefore it is
|
|
recommended to review <<websocket-stomp-message-flow>>.
|
|
|
|
The obvious place to start is to configure the thread pools backing the
|
|
`"clientInboundChannel"` and the `"clientOutboundChannel"`. By default both
|
|
are configured at twice the number of available processors.
|
|
|
|
If the handling of messages in annotated methods is mainly CPU bound then the
|
|
number of threads for the `"clientInboundChannel"` should remain close to the
|
|
number of processors. If the work they do is more IO bound and requires blocking
|
|
or waiting on a database or other external system then the thread pool size
|
|
will need to be increased.
|
|
|
|
[NOTE]
|
|
====
|
|
`ThreadPoolExecutor` has 3 important properties. Those are the core and
|
|
the max thread pool size as well as the capacity for the queue to store
|
|
tasks for which there are no available threads.
|
|
|
|
A common point of confusion is that configuring the core pool size (e.g. 10)
|
|
and max pool size (e.g. 20) results in a thread pool with 10 to 20 threads.
|
|
In fact if the capacity is left at its default value of Integer.MAX_VALUE
|
|
then the thread pool will never increase beyond the core pool size since
|
|
all additional tasks will be queued.
|
|
|
|
Please review the Javadoc of `ThreadPoolExecutor` to learn how these
|
|
properties work and understand the various queuing strategies.
|
|
====
|
|
|
|
On the `"clientOutboundChannel"` side it is all about sending messages to WebSocket
|
|
clients. If clients are on a fast network then the number of threads should
|
|
remain close to the number of available processors. If they are slow or on
|
|
low bandwidth they will take longer to consume messages and put a burden on the
|
|
thread pool. Therefore increasing the thread pool size will be necessary.
|
|
|
|
While the workload for the "clientInboundChannel" is possible to predict --
|
|
after all it is based on what the application does -- how to configure the
|
|
"clientOutboundChannel" is harder as it is based on factors beyond
|
|
the control of the application. For this reason there are two additional
|
|
properties related to the sending of messages. Those are the `"sendTimeLimit"`
|
|
and the `"sendBufferSizeLimit"`. Those are used to configure how long a
|
|
send is allowed to take and how much data can be buffered when sending
|
|
messages to a client.
|
|
|
|
The general idea is that at any given time only a single thread may be used
|
|
to send to a client. All additional messages meanwhile get buffered and you
|
|
can use these properties to decide how long sending a message is allowed to
|
|
take and how much data can be buffered in the mean time. Please review the
|
|
Javadoc and documentation of the XML schema for this configuration for
|
|
important additional details.
|
|
|
|
Here is example configuration:
|
|
|
|
[source,java,indent=0]
|
|
[subs="verbatim,quotes"]
|
|
----
|
|
@Configuration
|
|
@EnableWebSocketMessageBroker
|
|
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
|
|
|
|
@Override
|
|
public void configureWebSocketTransport(WebSocketTransportRegistration registration) {
|
|
registration.setSendTimeLimit(15 * 1000).setSendBufferSizeLimit(512 * 1024);
|
|
}
|
|
|
|
// ...
|
|
|
|
}
|
|
----
|
|
|
|
[source,xml,indent=0]
|
|
[subs="verbatim,quotes,attributes"]
|
|
----
|
|
<beans xmlns="http://www.springframework.org/schema/beans"
|
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
|
xmlns:websocket="http://www.springframework.org/schema/websocket"
|
|
xsi:schemaLocation="
|
|
http://www.springframework.org/schema/beans
|
|
https://www.springframework.org/schema/beans/spring-beans.xsd
|
|
http://www.springframework.org/schema/websocket
|
|
https://www.springframework.org/schema/websocket/spring-websocket.xsd">
|
|
|
|
<websocket:message-broker>
|
|
<websocket:transport send-timeout="15000" send-buffer-size="524288" />
|
|
<!-- ... -->
|
|
</websocket:message-broker>
|
|
|
|
</beans>
|
|
----
|
|
|
|
The WebSocket transport configuration shown above can also be used to configure the
|
|
maximum allowed size for incoming STOMP messages. Although in theory a WebSocket
|
|
message can be almost unlimited in size, in practice WebSocket servers impose
|
|
limits -- for example, 8K on Tomcat and 64K on Jetty. For this reason STOMP clients
|
|
such as the JavaScript https://github.com/JSteunou/webstomp-client[webstomp-client]
|
|
and others split larger STOMP messages at 16K boundaries and send them as multiple
|
|
WebSocket messages thus requiring the server to buffer and re-assemble.
|
|
|
|
Spring's STOMP over WebSocket support does this so applications can configure the
|
|
maximum size for STOMP messages irrespective of WebSocket server specific message
|
|
sizes. Do keep in mind that the WebSocket message size will be automatically
|
|
adjusted if necessary to ensure they can carry 16K WebSocket messages at a
|
|
minimum.
|
|
|
|
Here is example configuration:
|
|
|
|
[source,java,indent=0]
|
|
[subs="verbatim,quotes"]
|
|
----
|
|
@Configuration
|
|
@EnableWebSocketMessageBroker
|
|
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
|
|
|
|
@Override
|
|
public void configureWebSocketTransport(WebSocketTransportRegistration registration) {
|
|
registration.setMessageSizeLimit(128 * 1024);
|
|
}
|
|
|
|
// ...
|
|
|
|
}
|
|
----
|
|
|
|
[source,xml,indent=0]
|
|
[subs="verbatim,quotes,attributes"]
|
|
----
|
|
<beans xmlns="http://www.springframework.org/schema/beans"
|
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
|
xmlns:websocket="http://www.springframework.org/schema/websocket"
|
|
xsi:schemaLocation="
|
|
http://www.springframework.org/schema/beans
|
|
https://www.springframework.org/schema/beans/spring-beans.xsd
|
|
http://www.springframework.org/schema/websocket
|
|
https://www.springframework.org/schema/websocket/spring-websocket.xsd">
|
|
|
|
<websocket:message-broker>
|
|
<websocket:transport message-size="131072" />
|
|
<!-- ... -->
|
|
</websocket:message-broker>
|
|
|
|
</beans>
|
|
----
|
|
|
|
An important point about scaling is using multiple application instances.
|
|
Currently it is not possible to do that with the simple broker.
|
|
However when using a full-featured broker such as RabbitMQ, each application
|
|
instance connects to the broker and messages broadcast from one application
|
|
instance can be broadcast through the broker to WebSocket clients connected
|
|
through any other application instances.
|
|
|
|
|
|
|
|
[[websocket-stomp-stats]]
|
|
=== Monitoring
|
|
|
|
When using `@EnableWebSocketMessageBroker` or `<websocket:message-broker>` key
|
|
infrastructure components automatically gather stats and counters that provide
|
|
important insight into the internal state of the application. The configuration
|
|
also declares a bean of type `WebSocketMessageBrokerStats` that gathers all
|
|
available information in one place and by default logs it at `INFO` level once
|
|
every 30 minutes. This bean can be exported to JMX through Spring's
|
|
`MBeanExporter` for viewing at runtime, for example through JDK's `jconsole`.
|
|
Below is a summary of the available information.
|
|
|
|
Client WebSocket Sessions::
|
|
Current::: indicates how many client sessions there are
|
|
currently with the count further broken down by WebSocket vs HTTP
|
|
streaming and polling SockJS sessions.
|
|
Total::: indicates how many total sessions have been established.
|
|
Abnormally Closed:::
|
|
Connect Failures:::: these are sessions that got established but were
|
|
closed after not having received any messages within 60 seconds. This is
|
|
usually an indication of proxy or network issues.
|
|
Send Limit Exceeded:::: sessions closed after exceeding the configured send
|
|
timeout or the send buffer limits which can occur with slow clients
|
|
(see previous section).
|
|
Transport Errors:::: sessions closed after a transport error such as
|
|
failure to read or write to a WebSocket connection or
|
|
HTTP request/response.
|
|
STOMP Frames::: the total number of CONNECT, CONNECTED, and DISCONNECT frames
|
|
processed indicating how many clients connected on the STOMP level. Note that
|
|
the DISCONNECT count may be lower when sessions get closed abnormally or when
|
|
clients close without sending a DISCONNECT frame.
|
|
STOMP Broker Relay::
|
|
TCP Connections::: indicates how many TCP connections on behalf of client
|
|
WebSocket sessions are established to the broker. This should be equal to the
|
|
number of client WebSocket sessions + 1 additional shared "system" connection
|
|
for sending messages from within the application.
|
|
STOMP Frames::: the total number of CONNECT, CONNECTED, and DISCONNECT frames
|
|
forwarded to or received from the broker on behalf of clients. Note that a
|
|
DISCONNECT frame is sent to the broker regardless of how the client WebSocket
|
|
session was closed. Therefore a lower DISCONNECT frame count is an indication
|
|
that the broker is pro-actively closing connections, may be because of a
|
|
heartbeat that didn't arrive in time, an invalid input frame, or other.
|
|
Client Inbound Channel:: stats from thread pool backing the "clientInboundChannel"
|
|
providing insight into the health of incoming message processing. Tasks queueing
|
|
up here is an indication the application may be too slow to handle messages.
|
|
If there I/O bound tasks (e.g. slow database query, HTTP request to 3rd party
|
|
REST API, etc) consider increasing the thread pool size.
|
|
Client Outbound Channel:: stats from the thread pool backing the "clientOutboundChannel"
|
|
providing insight into the health of broadcasting messages to clients. Tasks
|
|
queueing up here is an indication clients are too slow to consume messages.
|
|
One way to address this is to increase the thread pool size to accommodate the
|
|
number of concurrent slow clients expected. Another option is to reduce the
|
|
send timeout and send buffer size limits (see the previous section).
|
|
SockJS Task Scheduler:: stats from thread pool of the SockJS task scheduler which
|
|
is used to send heartbeats. Note that when heartbeats are negotiated on the
|
|
STOMP level the SockJS heartbeats are disabled.
|
|
|
|
|
|
|
|
[[websocket-stomp-testing]]
|
|
=== Testing
|
|
|
|
There are two main approaches to testing applications using Spring's STOMP over
|
|
WebSocket support. The first is to write server-side tests verifying the functionality
|
|
of controllers and their annotated message handling methods. The second is to write
|
|
full end-to-end tests that involve running a client and a server.
|
|
|
|
The two approaches are not mutually exclusive. On the contrary each has a place
|
|
in an overall test strategy. Server-side tests are more focused and easier to write
|
|
and maintain. End-to-end integration tests on the other hand are more complete and
|
|
test much more, but they're also more involved to write and maintain.
|
|
|
|
The simplest form of server-side tests is to write controller unit tests. However
|
|
this is not useful enough since much of what a controller does depends on its
|
|
annotations. Pure unit tests simply can't test that.
|
|
|
|
Ideally controllers under test should be invoked as they are at runtime, much like
|
|
the approach to testing controllers handling HTTP requests using the Spring MVC Test
|
|
framework. i.e. without running a Servlet container but relying on the Spring Framework
|
|
to invoke the annotated controllers. Just like with Spring MVC Test here there are two
|
|
two possible alternatives, either using a "context-based" or "standalone" setup:
|
|
|
|
1. Load the actual Spring configuration with the help of the
|
|
Spring TestContext framework, inject "clientInboundChannel" as a test field, and
|
|
use it to send messages to be handled by controller methods.
|
|
|
|
2. Manually set up the minimum Spring framework infrastructure required to invoke
|
|
controllers (namely the `SimpAnnotationMethodMessageHandler`) and pass messages for
|
|
controllers directly to it.
|
|
|
|
Both of these setup scenarios are demonstrated in the
|
|
https://github.com/rstoyanchev/spring-websocket-portfolio/tree/master/src/test/java/org/springframework/samples/portfolio/web[tests for the stock portfolio]
|
|
sample application.
|
|
|
|
The second approach is to create end-to-end integration tests. For that you will need
|
|
to run a WebSocket server in embedded mode and connect to it as a WebSocket client
|
|
sending WebSocket messages containing STOMP frames.
|
|
The https://github.com/rstoyanchev/spring-websocket-portfolio/tree/master/src/test/java/org/springframework/samples/portfolio/web[tests for the stock portfolio]
|
|
sample application also demonstrates this approach using Tomcat as the embedded
|
|
WebSocket server and a simple STOMP client for test purposes.
|