知乎专栏 |
Restful 控制器
URL 参数,只能传递简单数据类型,参数可以在 web 服务器日志中看到,方便调试。可以在 Web 服务器中拦截,能用于 7层负载均衡转发策略中。还能做数据统计分析。
form data 数据提交的优势是简单,key-value 对应,一般情况下,web服务器不做特殊设置,是看不到数据的,数据类型支持数组等简单类型。
json 方案是应对复杂的数据结构,方便进行反序列化操作,除了常规数据类型 int/float/string/bool 还支持 map,list 以及用户定义的 class
@GetMapping("iterable") public Iterable<PicturePsychoanalysis> get() { Iterable<PicturePsychoanalysis> picturePsychoanalysis = picturePsychoanalysisService.findByAnalysisIsNull(); return picturePsychoanalysis; }
@PostMapping("/{device}/question/voice") public String questionVoice(@PathVariable String device, @RequestParam("file") MultipartFile file, @RequestParam("session") String session) { if (file.isEmpty()) { logger.error("上传失败,请选择文件"); } String fileName = file.getOriginalFilename(); String filePath = "/tmp/".concat(session.concat(".mp3")); File saveAs = new File(filePath); try { file.transferTo(saveAs); logger.info("上传成功 " + fileName); } catch (IOException e) { logger.error(e.toString(), e); return "上传失败"; } return "上传成功"; }
@RequestMapping("/get/{id}") public Member getStatistics(@PathVariable long id) { Member statistics = memberRepostitory.findOne(id); if (statistics == null) { statistics = new Member(); } return statistics; }
MediaType.APPLICATION_JSON_VALUE 执行结果反馈json数据
@RestController @RequestMapping("/api/persons") public class MainController { @RequestMapping( value = "/detail/{id}", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE ) public ResponseEntity<Persons> getUserDetail(@PathVariable Long id) { Persons user = personsRepository.findById(id); return new ResponseEntity<>(user, HttpStatus.OK); } }
@RequestMapping(value = "/create", method = RequestMethod.POST, produces = { "application/xml", "application/json" }) public ResponseEntity<Member> create(@RequestBody Member member) { memberRepository.save(member); return new ResponseEntity<Member>(member, HttpStatus.OK); }
package api.config; import java.io.IOException; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Primary; import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder; import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.JsonSerializer; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.SerializerProvider; @Configuration public class JacksonConfig { @Bean @Primary @ConditionalOnMissingBean(ObjectMapper.class) public ObjectMapper jacksonObjectMapper(Jackson2ObjectMapperBuilder builder) { ObjectMapper objectMapper = builder.createXmlMapper(false).build(); objectMapper.getSerializerProvider().setNullValueSerializer(new JsonSerializer<Object>() { @Override public void serialize(Object o, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException, JsonProcessingException { jsonGenerator.writeString(""); } }); return objectMapper; } }
restful 将同时支持 json 和 xml 数据传递
package com.example.api.restful; import java.util.List; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.domain.PageRequest; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RestController; import com.example.api.domain.RecentRead; import com.example.api.repository.RecentReadRepostitory; @RestController @RequestMapping("/restful/article") public class ArticleRestController { @Autowired private RecentReadRepostitory recentReadRepostitory; @RequestMapping(value = "/recent/read/add/{memberId}/{articleId}", method = RequestMethod.GET, produces = { "application/xml", "application/json" }) public ResponseEntity<RecentRead> recentAdd(@PathVariable long memberId, @PathVariable long articleId) { RecentRead recentRead = new RecentRead(); recentRead.setMemberId(memberId); recentRead.setArticleId(articleId); recentReadRepostitory.save(recentRead); return new ResponseEntity<RecentRead>(recentRead, HttpStatus.OK); } @RequestMapping(value="/recent/read/list/{id}", produces = { "application/xml", "application/json" }) public List<RecentRead> recentList(@PathVariable long id) { int page = 0; int limit = 20; List<RecentRead> recentRead = recentReadRepostitory.findByMemberId(id, new PageRequest(page, limit)); return recentRead; } }
开发中发现很多人不适应新的接口方式,有时候只能妥协,这些顽固不化的人需要这样的数据库格式
{ "status":true, "reason":"登录成功", "code":1, "data":{ "id":2, "name":null, "sex":null, "age":0, "wechat":null, "mobile":"13113668890", "picture":null, "ipAddress":"0:0:0:0:0:0:0:1" } }
返回数据必须放在 data 字典中, 而我通常是采用 http status code 来返回状态,返回结果是对象。实现上面的需求我们需要加入一个data成员变量,因为我们不清楚最终要返回什么对象。所以声明为 java.lang.Object
package com.example.api.pojo; import java.io.Serializable; public class RestfulResponse implements Serializable { /** * */ private static final long serialVersionUID = -4045645995352698349L; private boolean status; private String reason; private int code; private Object data; public RestfulResponse(boolean status, int code, String reason, Object data) { this.status = status; this.code = code; this.reason = reason; this.data = data; } public boolean isStatus() { return status; } public void setStatus(boolean status) { this.status = status; } public String getReason() { return reason; } public void setReason(String reason) { this.reason = reason; } public int getCode() { return code; } public void setCode(int code) { this.code = code; } public Object getData() { return data; } public void setData(Object data) { this.data = data; } @Override public String toString() { return "RestfulResponse [status=" + status + ", reason=" + reason + ", code=" + code + ", data=" + data + "]"; } }
Service
public RestfulResponse bindWechat(String mobile, String wechat) { Member member = memberRepository.findByMobile(mobile); member.setWechat(wechat); memberRepository.save(member); return new RestfulResponse(true, 1, "微信绑定成功", member); }
Controller
@RequestMapping("/login/sms/{mobile}/{code}") public RestfulResponse sms(@PathVariable String mobile, @PathVariable String wechat) { return memberService.bindWechat(mobile, wechat); }
spring.servlet.multipart.max-file-size=128KB spring.servlet.multipart.max-request-size=128KB spring.http.multipart.enabled=false
RestController
package api.restful; import java.io.File; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.util.ArrayList; import java.util.List; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.multipart.MultipartFile; import org.springframework.web.servlet.mvc.support.RedirectAttributes; import api.pojo.RestfulResponse; @RestController @RequestMapping("/upload") public class UploadRestController { private final static String FOLDER = "/tmp"; public UploadRestController() { } @PostMapping("/single") public RestfulResponse upload(@RequestParam("file") MultipartFile file, RedirectAttributes redirectAttributes) { if (file.isEmpty()) { return new RestfulResponse(false, 0, "Please select a file to upload", ""); } try { byte[] bytes = file.getBytes(); Path path = Paths.get(FOLDER + "/" + file.getOriginalFilename()); Files.write(path, bytes); return new RestfulResponse(true, 0, "", path.toString()); } catch (Exception e) { return new RestfulResponse(false, 0, e.getMessage(), null); } } @PostMapping(value = "/group") public RestfulResponse group(@RequestParam("files") MultipartFile[] files) { List<String> filelist = new ArrayList<String>(); try { for (MultipartFile file : files) { File tmpfile = new File(FOLDER + "/" + file.getOriginalFilename()); file.transferTo(tmpfile); filelist.add(tmpfile.getPath()); } return new RestfulResponse(true, 0, null, filelist); } catch (Exception e) { return new RestfulResponse(false, 0, e.getMessage(), null); } } }
由于上传文件名可能存在空格等特殊字符,这里使用UUID替代文件名
@PostMapping(value = "/file") public RestfulResponse file(@RequestParam("file") MultipartFile[] files) { List<Object> filelist = new ArrayList<Object>(); try { for (MultipartFile file : files) { UUID uuid = UUID.randomUUID(); String filename = String.format("%s/%s.%s", folder, uuid.toString(), this.getExtensionName(filename)); File tmpfile = new File(filename); String filepath = tmpfile.getPath(); System.out.println(filepath); file.transferTo(tmpfile); filelist.add(tmpfile.toString()); } return new RestfulResponse(true, 0, null, filelist); } catch (Exception e) { return new RestfulResponse(false, 0, e.getMessage(), null); } } private String getExtensionName(String filename) { if ((filename != null) && (filename.length() > 0)) { int dot = filename.lastIndexOf('.'); if ((dot > -1) && (dot < (filename.length() - 1))) { return filename.substring(dot + 1); } } return filename; }
获取文件名及后缀信息 MultipartFile file = new MultipartFile(); String file = file.getOriginalFilename() 获取文件名 MultipartFile file = new MultipartFile(); String fileName = file.getOriginalFilename().substring(0,file.getOriginalFilename().lastIndexOf(".")) 获取文件后缀 MultipartFile file = new MultipartFile(); String fileSuffix = file.getOriginalFilename().substring(file.getOriginalFilename().lastIndexOf(".")) 获取文件类型 MultipartFile file = new MultipartFile(); String fileType = file.getContentType() 获取文件大小 MultipartFile file = new MultipartFile(); String fileSize = file.getSize()
下面是一个导出 CSV 文件的例子
@GetMapping("/export") public void export(HttpServletResponse response) throws IOException { response.setContentType("application/csv"); // response.setContentType("application/csv;charset=gb18030"); response.setHeader("Content-Disposition", "attachment; filename=\"file.csv\""); BufferedWriter writer = new BufferedWriter(response.getWriter()); // 需要写入 utf8bom 头否则会出现中文乱码 // byte[] uft8bom = { (byte) 0xef, (byte) 0xbb, (byte) 0xbf }; String bom = new String(new byte[] { (byte) 0xEF, (byte) 0xBB, (byte) 0xBF }); writer.write(bom); writer.write("A,B,C"); writer.newLine(); tableRepository.findAll().forEach(table -> { try { String tmp = String.format("%s,%s,%s", table.getId(), table.getMethod(), table.getMoney()); writer.write(tmp); writer.newLine(); } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } }); writer.flush(); writer.close(); }
HTTP RFC 7807 规范:https://www.rfc-editor.org/rfc/rfc7807。这个规范里定义了HTTP API的“问题细节”(Problem Details)内容。用它来携带HTTP错误返回信息,避免自定义新的错误返回格式。
HTTP/1.1 403 Forbidden Content-Type: application/problem+json Content-Language: en { "status": 403, "type": "https://bankname.com/common-problems/low-balance", "title": "You not have enough balance", "detail": "Your current balance is 30 and you are transterring 50", "instance": "/account-transfer-service" }
type: 问题的类型; title: 问题类型描述; status: HTTP状态码; detail: 问题实例描述; instance: URI的内容应该用来描述问题实例,但不是必须的。
@GetMapping("/ProblemDetail/v1/{id}") public ResponseEntity config(@PathVariable("id") Long id) { if (id < 100) { return ResponseEntity.ok(new Member(id, "netkiller")); } else { ProblemDetail pd = ProblemDetail.forStatusAndDetail(HttpStatus.NOT_FOUND, "Member id '" + id + "' does no exist"); pd.setType(URI.create("https://www.netkiller.cn/errors/not-found")); pd.setTitle("Record Not Found"); pd.setProperty("hostname", "www.netkiller.cn"); return ResponseEntity.status(404).body(pd); } }
@GetMapping(path = "/ProblemDetail/v2/{id}") public ResponseEntity getEmployeeById_V3(@PathVariable("id") Long id) { try { //somthing threw this exception throw new NullPointerException("Something was expected but it was null"); } catch (NullPointerException npe) { ProblemDetail pd = ProblemDetail .forStatusAndDetail(HttpStatus.INTERNAL_SERVER_ERROR, "Null Pointer Exception"); pd.setType(URI.create("https://www.netkiller.cn/errors/npe")); pd.setTitle("Null Pointer Exception"); pd.setProperty("hostname", "www.netkiller.cn"); throw new ErrorResponseException(HttpStatus.NOT_FOUND, pd, npe); } }
@PostMapping(path = "/foo", params = {"id", "name=John"}) public ResponseEntity<String> handlePostRequest() { // 处理请求 return ResponseEntity.ok("Success"); }
package netkiller.json; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.OutputStream; import javax.json.*; public final class Writer { public static void main(String[] args) { // TODO Auto-generated method stub JsonObjectBuilder jsonBuilder = Json.createObjectBuilder(); JsonObjectBuilder addressBuilder = Json.createObjectBuilder(); JsonArrayBuilder phoneNumBuilder = Json.createArrayBuilder(); phoneNumBuilder.add("12355566688").add("0755-2222-3333"); addressBuilder.add("street", "Longhua").add("city", "Shenzhen").add("zipcode", 518000); jsonBuilder.add("nickname", "netkiller").add("name", "Neo").add("department", "IT").add("role", "Admin"); jsonBuilder.add("phone", phoneNumBuilder); jsonBuilder.add("address", addressBuilder); JsonObject jsonObject = jsonBuilder.build(); System.out.println(jsonObject); try { // write to file File file = new File("json.txt"); if (!file.exists()) { file.createNewFile(); } OutputStream os = null; os = new FileOutputStream(file); JsonWriter jsonWriter = Json.createWriter(os); jsonWriter.writeObject(jsonObject); jsonWriter.close(); } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } } }
运行后输出
{"nickname":"netkiller","name":"Neo","department":"IT","role":"Admin","phone":["12355566688","0755-2222-3333"],"address":{"street":"Longhua","city":"Shenzhen","zipcode":"518000"}}
package netkiller.json; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import javax.json.Json; import javax.json.JsonArray; import javax.json.JsonObject; import javax.json.JsonReader; import javax.json.JsonValue; public final class Reader { public static final String JSON_FILE="json.txt"; public static void main(String[] args) throws IOException { InputStream fis = new FileInputStream(JSON_FILE); //create JsonReader object JsonReader jsonReader = Json.createReader(fis); //get JsonObject from JsonReader JsonObject jsonObject = jsonReader.readObject(); //we can close IO resource and JsonReader now jsonReader.close(); fis.close(); System.out.printf("nickname: %s \n", jsonObject.getString("nickname")); System.out.printf("name: %s \n", jsonObject.getString("name")); System.out.printf("department: %s \n", jsonObject.getString("department")); System.out.printf("role: %s \n", jsonObject.getString("role")); JsonArray jsonArray = jsonObject.getJsonArray("phone"); //long[] numbers = new long[jsonArray.size()]; int index = 0; for(JsonValue value : jsonArray){ //numbers[index++] = Long.parseLong(value.toString()); System.out.printf("phone[%d]: %s \n", index++, value.toString()); } //reading inner object from json object JsonObject innerJsonObject = jsonObject.getJsonObject("address"); System.out.printf("address: %s, %s, %d \n", innerJsonObject.getString("street"), innerJsonObject.getString("city"), innerJsonObject.getInt("zipcode")); } }
运行结果
nickname: netkiller name: Neo department: IT role: Admin phone[0]: +8612355566688 phone[1]: 0755-2222-3333 address: Longhua, Shenzhen, 518000
package netkiller.json; import java.io.BufferedReader; import java.io.InputStreamReader; import java.io.Reader; import java.io.StringReader; import java.net.URL; import java.net.URLConnection; import java.nio.charset.Charset; import javax.json.*; public class HttpUrl { public static void main(String[] args) { // TODO Auto-generated method stub String URL = "http://www.example.com/json/2/20/0.html"; // system.out.println("Requeted URL:" + URL); StringBuilder sb = new StringBuilder(); URLConnection urlConn = null; InputStreamReader in = null; try { URL url = new URL(URL); urlConn = url.openConnection(); if (urlConn != null) urlConn.setReadTimeout(60 * 1000); if (urlConn != null && urlConn.getInputStream() != null) { in = new InputStreamReader(urlConn.getInputStream(), Charset.defaultCharset()); BufferedReader bufferedReader = new BufferedReader(in); if (bufferedReader != null) { int cp; while ((cp = bufferedReader.read()) != -1) { sb.append((char) cp); } bufferedReader.close(); } } in.close(); String jsonString = sb.toString(); //System.out.println(jsonString); JsonReader reader = Json.createReader(new StringReader(jsonString)); JsonObject jsonObject = reader.readObject(); reader.close(); // System.out.println(jsonObject.size()); for (int i = 0; i < jsonObject.size() - 2; i++) { JsonObject rowObject = jsonObject.getJsonObject(Integer.toString(i)); // System.out.println(rowObject.toString()); System.out.printf("%s\t%s\t%s\n", rowObject.getJsonString("id"), rowObject.getJsonString("title"), rowObject.getJsonString("ctime")); } } catch (Exception e) { throw new RuntimeException("Exception while calling URL:" + URL, e); } } }
#序列化时间格式 spring.jackson.date-format=yyyy-MM-dd HH:mm:ss spring.mvc.date-format=yyyy-MM-dd HH:mm:ss #mvc序列化时候时区选择 spring.jackson.time-zone=GMT+8
默认 json 中的时间格式是这样的
"createDate":"2018-09-11T07:34:20.106+0000","updateDate":"2018-09-11T07:34:20.106+0000"
@JsonFormat 可以格式化 json 返回的时间格式。
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
格式化后
"createDate":"2018-09-11 07:42:44","updateDate":"2018-09-11 07:42:44"
解决时区问题,MongoDb 默认使用UTC,显示时间相差8小时
@JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss") private Date createdDate = new Date();
public class Test { @JsonFormat(shape=JsonFormat.Shape.STRING, pattern="yyyy-MMM-dd HH:mm:ss z", timezone="EST") @JsonProperty("pubDate") private Date recentBookPubDate; }
public class Test { @JsonFormat(shape=JsonFormat.Shape.NUMBER) @JsonProperty("birthDate") private Date birthDate; }
{ "birthDate" : 1528702883858 }
package cn.netkiller.json; public class Member { private String name; public Member() { // TODO Auto-generated constructor stub } public Member(String name) { // TODO Auto-generated constructor stub this.name = name; } public String getName() { return name; } public void setName(String name) { this.name = name; } @Override public String toString() { return "Member [name=" + name + "]"; } }
package cn.netkiller.json; import java.io.IOException; import org.springframework.boot.jackson.JsonComponent; import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.core.JsonParser; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.core.TreeNode; import com.fasterxml.jackson.databind.DeserializationContext; import com.fasterxml.jackson.databind.JsonDeserializer; import com.fasterxml.jackson.databind.JsonSerializer; import com.fasterxml.jackson.databind.SerializerProvider; import com.fasterxml.jackson.databind.node.TextNode; @JsonComponent public class Json { public Json() { // TODO Auto-generated constructor stub } public static class MemberJsonSerializer extends JsonSerializer<Member> { @Override public void serialize(Member value, JsonGenerator gen, SerializerProvider serializers) throws IOException { // TODO Auto-generated method stub gen.writeStartObject(); gen.writeStringField("member", value.toString()); gen.writeEndObject(); } } public static class MemberJsonDeserializer extends JsonDeserializer<Member> { @Override public Member deserialize(JsonParser p, DeserializationContext ctxt) throws IOException, JsonProcessingException { // TODO Auto-generated method stub TreeNode treeNode = p.getCodec().readTree(p); TextNode member = (TextNode) treeNode.get("member"); return new Member(member.asText()); } } }
package cn.netkiller.json.controller; import cn.netkiller.json.Member; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.JsonMappingException; import com.fasterxml.jackson.databind.ObjectMapper; /** * * @author neo */ @RestController public class SimpleController { @Autowired public ObjectMapper objectMapper; @GetMapping("/") public String home() throws JsonMappingException, JsonProcessingException { String json = "{\"name\":\"netkiller\"}"; Member member = objectMapper.readValue(json, Member.class); System.out.println(member.getName()); return member.getName(); } }
ObjectMapper mapper = new ObjectMapper(); User user = new User(); //Object to JSON in file mapper.writeValue(new File("c:\\user.json"), user); //Object to JSON in String String jsonInString = mapper.writeValueAsString(user); //Convert object to JSON string and pretty print String jsonInString = mapper.writerWithDefaultPrettyPrinter().writeValueAsString(user);
Notification notification = new Notification(status, time, summary + info); ObjectMapper objectMapper = new ObjectMapper(); String json = objectMapper.writeValueAsString(notification);
ObjectMapper mapper = new ObjectMapper(); String jsonInString = "{'name' : 'mkyong'}"; //JSON from file to Object User user = mapper.readValue(new File("c:\\user.json"), User.class); //JSON from String to Object User user = mapper.readValue(jsonInString, User.class);
package cn.netkiller; import java.util.Date; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; public class ObjectToJSON { public static void main(String[] args) throws JsonProcessingException { Test test = new Test("Neo", 123, new Date()); ObjectMapper mapper = new ObjectMapper(); String jsonData = mapper.writerWithDefaultPrettyPrinter() .writeValueAsString(test); System.out.println(jsonData); } }
package cn.netkiller; import java.io.IOException; import com.fasterxml.jackson.core.JsonParseException; import com.fasterxml.jackson.databind.JsonMappingException; import com.fasterxml.jackson.databind.ObjectMapper; public class JSONToObject { public static void main(String[] args) throws JsonParseException, JsonMappingException, IOException { String jsonData = "{" +"\"name\" : \"Neo\"," +"\"age\" : 12," +"\"birthDate\" : \"2019-Jun-11 07:00:46 CST\"" +"}"; ObjectMapper mapper = new ObjectMapper(); Test test = mapper.readValue(jsonData, Test.class); System.out.println(test.getName()+" | "+test.getBirthDate()+" | "+ test.getAge()); } }
HashMap<String, HashMap<String, String>> data = new HashMap<String, HashMap<String, String>>(); Gson gson = new Gson(); data = gson.fromJson(jsonString, data.getClass());
Gson gson = new Gson(); Map<String, String> data = new LinkedHashMap<String, String>() {{ put("text", text); put("scene", "talk"); }}; String json = gson.toJson(data, LinkedHashMap.class);
注意:String json = gson.toJson(data); 这样转换不成功,返回 null,必须指定 class 才能成功 String json = gson.toJson(data, LinkedHashMap.class);
避免接口无序执行,被同时多次执行,同一时间只能有一个请求,请求完毕之后才能进行下一次请求。
@GetMapping("/lock/{id}") public String lock1(@PathVariable("id") String id) throws InterruptedException { synchronized (id.intern()) { log.info(Thread.currentThread().getName() + " 上锁"); Thread.sleep(10000); log.info(Thread.currentThread().getName() + " 解锁"); } return Thread.currentThread().getName(); }
使用 ConcurrentHashMap 数据共享
private final Map<String, Object> share = new ConcurrentHashMap<>(); @GetMapping("/share/{id}") public Map<String, Object> shareTest(@PathVariable("id") String id) throws InterruptedException { // share.computeIfAbsent(id, k -> new Object()); share.computeIfAbsent(id, key -> { return new Date(); }); synchronized (share) { log.info(Thread.currentThread().getName() + " 上锁"); Thread.sleep(1000); log.info(Thread.currentThread().getName() + " 解锁"); } return share; }
SseEmitter 实现,可以对比一下两者的区别
@GetMapping(path="/events", produces=MediaType.TEXT_EVENT_STREAM_VALUE) public SseEmitter handle() { SseEmitter emitter = new SseEmitter(); // Save the emitter somewhere.. return emitter; } // In some other thread emitter.send("Hello once"); // and again later on emitter.send("Hello again"); // and done at some point emitter.complete();
@GetMapping(value = "emitter", produces = MediaType.TEXT_EVENT_STREAM_VALUE) public SseEmitter emitter() { SseEmitter sseEmitter = new SseEmitter(300_000L); sseEmitter.onTimeout(() -> System.out.println("超时")); sseEmitter.onCompletion(() -> System.out.println("完成")); try { sseEmitter.send(SseEmitter.event().id("1").name("test").data("Test")); } catch (IOException e) { throw new RuntimeException(e); } sseEmitter.complete(); return sseEmitter; }
@GetMapping("/mvc/sse") public SseEmitter streamSseMvc() { SseEmitter emitter = new SseEmitter(); ExecutorService sseMvcExecutor = Executors.newSingleThreadExecutor(); sseMvcExecutor.execute(() -> { try { for (int i = 0; true; i++) { SseEventBuilder event = SseEmitter.event() .data("SSE MVC - " + LocalTime.now().toString()) .id(String.valueOf(i)) .name("sse event - mvc"); emitter.send(event); Thread.sleep(1000); } } catch (Exception ex) { emitter.completeWithError(ex); } }); return emitter; }
@RequestMapping("/sse") public SseEmitter sse() { SseEmitter emitter = new SseEmitter(); new Thread(() -> { try { for (int i = 0; i < 10; i++) { emitter.send(SseEmitter.event() .id(String.valueOf(i)) .name("message") .data("Message " + i)); Thread.sleep(1000); } emitter.complete(); } catch (Exception e) { emitter.completeWithError(e); } }).start(); return emitter; }
@RequestMapping("/async") public ResponseBodyEmitter async() { ResponseBodyEmitter emitter = new ResponseBodyEmitter(); new Thread(() -> { try { for (int i = 0; i < 10; i++) { emitter.send("Message " + i); Thread.sleep(1000); } emitter.complete(); } catch (Exception e) { emitter.completeWithError(e); } }).start(); return emitter; }
@GetMapping("/events") public ResponseBodyEmitter handle() { ResponseBodyEmitter emitter = new ResponseBodyEmitter(); // Save the emitter somewhere.. return emitter; } // In some other thread emitter.send("Hello once"); // and again later on emitter.send("Hello again"); // and done at some point emitter.complete();
@GetMapping(value = "sse", produces = MediaType.TEXT_EVENT_STREAM_VALUE) public ResponseBodyEmitter sse() { ResponseBodyEmitter responseBodyEmitter = new ResponseBodyEmitter(); CompletableFuture.runAsync(() -> { List.of("text", "audio", "video", "image").forEach((value) -> { try { responseBodyEmitter.send(SseEmitter.event().id(usingRandom(5)).name(value).data(usingRandom(128).concat("\n").concat(usingRandom(128))).comment(usingRandom(128)).build()); } catch (IOException e) { log.error(e.toString()); responseBodyEmitter.completeWithError(e); } }); responseBodyEmitter.complete(); }); return responseBodyEmitter; }
@RequestMapping("/stream") public StreamingResponseBody stream(HttpServletResponse response) { response.setContentType("text/plain"); return outputStream -> { for (int i = 0; i < 10; i++) { outputStream.write(("Message " + i + "\n").getBytes()); outputStream.flush(); try { Thread.sleep(1000); } catch (InterruptedException e) { throw new RuntimeException(e); } } }; }
StreamingResponseBody 输出 Raw Data
@GetMapping("/download") public StreamingResponseBody handle() { return new StreamingResponseBody() { @Override public void writeTo(OutputStream outputStream) throws IOException { // write... } }; }
@GetMapping(value = "sse", produces = MediaType.TEXT_EVENT_STREAM_VALUE) public ResponseBodyEmitter sse() { ResponseBodyEmitter responseBodyEmitter = new ResponseBodyEmitter(); CompletableFuture.runAsync(() -> { List.of("text", "audio", "video", "image").forEach((value) -> { try { responseBodyEmitter.send(SseEmitter.event().id(usingRandom(5)).name(value).data(usingRandom(128).concat("\n").concat(usingRandom(128))).comment(usingRandom(128)).build()); } catch (IOException e) { log.error(e.toString()); responseBodyEmitter.completeWithError(e); } }); responseBodyEmitter.complete(); }); return responseBodyEmitter; }
neo@MacBook-Pro-M2 ~/w/watch (main)> curl -H "Accept: text/event-stream" http://dev.netkiller.cn:8081/test/sse id:GZ1O2 event:text data:amftmGZcWyhniUbDganQLbttIXe0qAoWbEaMwtzqmW7S0a3odWpkGD4hgBC9sOmljSWEArmwpTv1ZY637tz2m8IdnSHzy5OjWKWwdXdG4GfvqTfpSavk3RWt09aS68zM data:DojMvAJhZk7hgoyrQFcx1oXRN78xpxJ6I4k9r7kMfJUBkozpSOsSLmxD2z39ISZkY8rur2mEzDNhzVZAunYC37D5bRW4PNZ0hD7PsAsYXQVxQmOQSODLVUJbJQ5YlmRI :tZ5Q0DsH5hFQDHOoviwpWtX96pGyaPREqtlHdr9HuX8FMTJCBkcEK6yJZyjEIjlhiZ7kZKTWaGslvwOda4pIrGFlA1hWtMj5Bntik8CMSuhGkB3nAXbTJ8eJMTe9CHde id:kpKRH event:audio data:fRNjEj0daux6WaISv3ZrtnWeGFtlG0Q5bvKTRyjmkSBAEbm2enA9Jsxx4H4DzcdlJDQKsG9Md9f1Rh5YlpikbNWRXAiT1Kw9fMgFdWZvRIDuaRBIxwIZ6T3a5NhWwV9c data:OzNJYLBpvRAYNALKKxOmkGuvTLYVscvZS6UYcBX1hKOv0Zx7yKOXgq1vt8LKcluyNNYIoeQsOCbERcEyOLdL8UYqQujeMCXRspa5Gl5SskarbKMlfhobRuydrmaoA3jW :Hye2fybk1dHvLXo5nJBdHXJBRfno6C4N6YFvRo0ch2FilKCuAV7VgoOrQj1Hx0TETFbxRziCNsfHU6GVJ9oy5wdAERVIrGnlEh3K2L2jcG5oU8HbADcHWyZ4mPjja0VA id:I3Hq6 event:video data:IIRcsgPMaxVIkF69JtKz2bHRYfZWF9ydc1kBdpEt4w8J4xezdsDixTb2Jv9ue6F3l8wSGubdw8A77szA65M7VrzKr9NsuRlG5YXzyhioCHDP21vWUttByXAfavRFUaQf data:hOZB3iFwK5159tHAh5Ul2plVreIjHm7WlJGZ0dHoYWLsg6Ft8To3Qtv1Gt1p0TCFLyrASr9COyEup5q9tjVsLUSr75ZWeuDoPNjMfNOH4jBrf1PVtKfT7HNgo8fxt64c :AhbH0VI78vukAUNieNMRoET7lQiY94UQZbSNaM2UqP9R0RYcxPoTE9g0wpyhz3qJbuxoYkWL5sw2bm7jv3nITL3tqSo64IZZRpkpbcD5OBvtqoFe08euNEB34aEFbOXt id:pqmcf event:image data:HKPvvsa2Gut3Pji3DbPXnmWcVGkBHkrIRjaNeaBq0kTl2o0Xo2H7mwYAmZ40lJxlb3cEdvcu2ddYe8mZR9pBOXyl16Sk48UFfRLvirXESw4REb3VWdZVaLCgTeRSgpOC data:Ef2EmTusV8xNTdLrcb3ndKOD23Sz1v8iw98YMqd9TGIQppNvXHmrTv4JYzVOIr7NwOqpID81OyjmGhNkqpG4Ho1FMigw939xHuaY9d6tZHSFqVY3wHjMCFqsNuao4gOW :HhyY8h0Yez0pIKMYNavtTBuAFX8iOuRlGpAntNak4H09mQbHZ5O7Sc5huOjcgL43BQs0V57UTKI2cpSptroxJTRQPQmJwrHGbWzHHqjjB85p4AsSBzF5mWit2eEmDKWu