001/* 002 * Copyright 2002-2018 the original author or authors. 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * https://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016 017package org.springframework.web.servlet.mvc.method.annotation; 018 019import java.io.IOException; 020import java.util.LinkedHashSet; 021import java.util.Set; 022 023import org.springframework.http.MediaType; 024import org.springframework.http.server.ServerHttpResponse; 025import org.springframework.util.Assert; 026 027/** 028 * A controller method return value type for asynchronous request processing 029 * where one or more objects are written to the response. 030 * 031 * <p>While {@link org.springframework.web.context.request.async.DeferredResult} 032 * is used to produce a single result, a {@code ResponseBodyEmitter} can be used 033 * to send multiple objects where each object is written with a compatible 034 * {@link org.springframework.http.converter.HttpMessageConverter}. 035 * 036 * <p>Supported as a return type on its own as well as within a 037 * {@link org.springframework.http.ResponseEntity}. 038 * 039 * <pre> 040 * @RequestMapping(value="/stream", method=RequestMethod.GET) 041 * public ResponseBodyEmitter handle() { 042 * ResponseBodyEmitter emitter = new ResponseBodyEmitter(); 043 * // Pass the emitter to another component... 044 * return emitter; 045 * } 046 * 047 * // in another thread 048 * emitter.send(foo1); 049 * 050 * // and again 051 * emitter.send(foo2); 052 * 053 * // and done 054 * emitter.complete(); 055 * </pre> 056 * 057 * @author Rossen Stoyanchev 058 * @author Juergen Hoeller 059 * @since 4.2 060 */ 061public class ResponseBodyEmitter { 062 063 private final Long timeout; 064 065 private final Set<DataWithMediaType> earlySendAttempts = new LinkedHashSet<DataWithMediaType>(8); 066 067 private Handler handler; 068 069 private boolean complete; 070 071 private Throwable failure; 072 073 private final DefaultCallback timeoutCallback = new DefaultCallback(); 074 075 private final DefaultCallback completionCallback = new DefaultCallback(); 076 077 078 /** 079 * Create a new ResponseBodyEmitter instance. 080 */ 081 public ResponseBodyEmitter() { 082 this.timeout = null; 083 } 084 085 /** 086 * Create a ResponseBodyEmitter with a custom timeout value. 087 * <p>By default not set in which case the default configured in the MVC 088 * Java Config or the MVC namespace is used, or if that's not set, then the 089 * timeout depends on the default of the underlying server. 090 * @param timeout timeout value in milliseconds 091 */ 092 public ResponseBodyEmitter(Long timeout) { 093 this.timeout = timeout; 094 } 095 096 097 /** 098 * Return the configured timeout value, if any. 099 */ 100 public Long getTimeout() { 101 return this.timeout; 102 } 103 104 105 synchronized void initialize(Handler handler) throws IOException { 106 this.handler = handler; 107 108 for (DataWithMediaType sendAttempt : this.earlySendAttempts) { 109 sendInternal(sendAttempt.getData(), sendAttempt.getMediaType()); 110 } 111 this.earlySendAttempts.clear(); 112 113 if (this.complete) { 114 if (this.failure != null) { 115 this.handler.completeWithError(this.failure); 116 } 117 else { 118 this.handler.complete(); 119 } 120 } 121 else { 122 this.handler.onTimeout(this.timeoutCallback); 123 this.handler.onCompletion(this.completionCallback); 124 } 125 } 126 127 /** 128 * Invoked after the response is updated with the status code and headers, 129 * if the ResponseBodyEmitter is wrapped in a ResponseEntity, but before the 130 * response is committed, i.e. before the response body has been written to. 131 * <p>The default implementation is empty. 132 */ 133 protected void extendResponse(ServerHttpResponse outputMessage) { 134 } 135 136 /** 137 * Write the given object to the response. 138 * <p>If any exception occurs a dispatch is made back to the app server where 139 * Spring MVC will pass the exception through its exception handling mechanism. 140 * @param object the object to write 141 * @throws IOException raised when an I/O error occurs 142 * @throws java.lang.IllegalStateException wraps any other errors 143 */ 144 public void send(Object object) throws IOException { 145 send(object, null); 146 } 147 148 /** 149 * Write the given object to the response also using a MediaType hint. 150 * <p>If any exception occurs a dispatch is made back to the app server where 151 * Spring MVC will pass the exception through its exception handling mechanism. 152 * @param object the object to write 153 * @param mediaType a MediaType hint for selecting an HttpMessageConverter 154 * @throws IOException raised when an I/O error occurs 155 * @throws java.lang.IllegalStateException wraps any other errors 156 */ 157 public synchronized void send(Object object, MediaType mediaType) throws IOException { 158 Assert.state(!this.complete, "ResponseBodyEmitter is already set complete"); 159 sendInternal(object, mediaType); 160 } 161 162 private void sendInternal(Object object, MediaType mediaType) throws IOException { 163 if (object != null) { 164 if (this.handler != null) { 165 try { 166 this.handler.send(object, mediaType); 167 } 168 catch (IOException ex) { 169 throw ex; 170 } 171 catch (Throwable ex) { 172 throw new IllegalStateException("Failed to send " + object, ex); 173 } 174 } 175 else { 176 this.earlySendAttempts.add(new DataWithMediaType(object, mediaType)); 177 } 178 } 179 } 180 181 /** 182 * Complete request processing. 183 * <p>A dispatch is made into the app server where Spring MVC completes 184 * asynchronous request processing. 185 * <p><strong>Note:</strong> you do not need to call this method after an 186 * {@link IOException} from any of the {@code send} methods. The Servlet 187 * container will generate an error notification that Spring MVC will process 188 * and handle through the exception resolver mechanism and then complete. 189 */ 190 public synchronized void complete() { 191 this.complete = true; 192 if (this.handler != null) { 193 this.handler.complete(); 194 } 195 } 196 197 /** 198 * Complete request processing with an error. 199 * <p>A dispatch is made into the app server where Spring MVC will pass the 200 * exception through its exception handling mechanism. 201 */ 202 public synchronized void completeWithError(Throwable ex) { 203 this.complete = true; 204 this.failure = ex; 205 if (this.handler != null) { 206 this.handler.completeWithError(ex); 207 } 208 } 209 210 /** 211 * Register code to invoke when the async request times out. This method is 212 * called from a container thread when an async request times out. 213 */ 214 public synchronized void onTimeout(Runnable callback) { 215 this.timeoutCallback.setDelegate(callback); 216 } 217 218 /** 219 * Register code to invoke when the async request completes. This method is 220 * called from a container thread when an async request completed for any 221 * reason including timeout and network error. This method is useful for 222 * detecting that a {@code ResponseBodyEmitter} instance is no longer usable. 223 */ 224 public synchronized void onCompletion(Runnable callback) { 225 this.completionCallback.setDelegate(callback); 226 } 227 228 229 /** 230 * Handle sent objects and complete request processing. 231 */ 232 interface Handler { 233 234 void send(Object data, MediaType mediaType) throws IOException; 235 236 void complete(); 237 238 void completeWithError(Throwable failure); 239 240 void onTimeout(Runnable callback); 241 242 void onCompletion(Runnable callback); 243 } 244 245 246 /** 247 * A simple holder of data to be written along with a MediaType hint for 248 * selecting a message converter to write with. 249 */ 250 public static class DataWithMediaType { 251 252 private final Object data; 253 254 private final MediaType mediaType; 255 256 public DataWithMediaType(Object data, MediaType mediaType) { 257 this.data = data; 258 this.mediaType = mediaType; 259 } 260 261 public Object getData() { 262 return this.data; 263 } 264 265 public MediaType getMediaType() { 266 return this.mediaType; 267 } 268 } 269 270 271 private class DefaultCallback implements Runnable { 272 273 private Runnable delegate; 274 275 public void setDelegate(Runnable delegate) { 276 this.delegate = delegate; 277 } 278 279 @Override 280 public void run() { 281 ResponseBodyEmitter.this.complete = true; 282 if (this.delegate != null) { 283 this.delegate.run(); 284 } 285 } 286 } 287 288}