还是老样子,废话不多说,这里直接讲代码,后面讲故事。
第一章 代码实现
1、在Controller类中添加【@EnableAsync】注解,注意不是方法上哦。
package cn.renkai721.controller;
import cn.renkai721.bean.*;
import cn.renkai721.common.BaseController;
import cn.renkai721.service.*;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiResponse;
import io.swagger.annotations.ApiResponses;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.web.bind.annotation.*;
import javax.servlet.http.HttpServletRequest;
@EnableAsync
@RestController
@RequestMapping("/async")
@Slf4j
@Api(value = "AsyncTestController",tags = {"测试异步"})
public class AsyncTestController extends BaseController {
@Autowired
private AsyncTestService asyncTestService;
@GetMapping("/testAsync")
@ResponseBody
@ApiResponses(value = {
@ApiResponse(code=200,message="返回对象",response=LoginRespBean.class)
})
public String testAsync(HttpServletRequest request) throws Exception {
asyncTestService.getLoginIpAndAddress(request);
return "success";
}
@GetMapping("/sendEmailToUser")
@ResponseBody
@ApiResponses(value = {
@ApiResponse(code=200,message="返回对象",response=LoginRespBean.class)
})
public String sendEmailToUser(UserReqBean vo) throws Exception {
asyncTestService.sendEmailToUser(vo);
return "success";
}
}
2、在你的Service的方法中【@Async】注解。这两步就可以实现异步操作了。
package cn.renkai721.service;
import cn.renkai721.bean.*;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import javax.servlet.AsyncContext;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.net.InetAddress;
import java.net.UnknownHostException;
@Service
@Slf4j
@Configuration
public class AsyncTestService {
@Async
public void getLoginIpAndAddress(HttpServletRequest request){
AsyncContext asyncContext = request.startAsync();
// 这个方法必须写
asyncContext.setTimeout(0);
this.getLoginIpAndAddress(asyncContext);
}
public String getLoginIpAndAddress(AsyncContext asyncContext){
StringBuffer sb = new StringBuffer();
try {
HttpServletRequest request = (HttpServletRequest)asyncContext.getRequest();
String userAgent = request.getHeader("User-Agent");
String device = this.getLoginDevice(userAgent);
String ip = this.getIpAddress(request);
sb.append("device="+device+",");
sb.append("ip="+ip+",");
log.info("device={},ip={}",device,ip);
} catch (Exception e) {
log.error("getLoginIpAndAddress e={}",e);
e.printStackTrace();
}
return sb.toString();
}
@Async
public String getIpAddress(HttpServletRequest request) throws IOException {
// 获取请求主机IP地址,如果通过代理进来,则透过防火墙获取真实IP地址
String ip = request.getHeader("X-Forwarded-For");
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("Proxy-Client-IP");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("WL-Proxy-Client-IP");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("HTTP_CLIENT_IP");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("HTTP_X_FORWARDED_FOR");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getRemoteAddr();
// 以下是后期添加的 要是不想在数据库看到 0:0:0.....或者 127.0.0.1的 数字串可用下边方法 亲测
if(ip.equals("127.0.0.1") || ip.equals("0:0:0:0:0:0:0:1")){
//根据网卡取本机配置的IP
InetAddress inet=null;
try {
inet = InetAddress.getLocalHost();
} catch (UnknownHostException e) {
e.printStackTrace();
}
ip= inet.getHostAddress();
}
}
} else if (ip.length() > 15) {
String[] ips = ip.split(",");
for (int index = 0; index < ips.length; index++) {
String strIp = (String) ips[index];
if (!("unknown".equalsIgnoreCase(strIp))) {
ip = strIp;
break;
}
}
}
return ip;
}
public String getLoginDevice(String userAgent){
log.info("userAgent={}",userAgent);
String device = "未知型号";
if(userAgent.indexOf("Windows") !=-1){
device = "Windows PC";
}else if(userAgent.indexOf("Mac") !=-1 && userAgent.indexOf("CPU") == -1){
device = "MAC PC";
}else if(userAgent.indexOf("iPhone") !=-1){
device = "iPhone";
}else if(userAgent.indexOf("Android") !=-1){
device = "Android";
}
return device;
}
@Async
public void sendEmailToUser(UserReqBean vo) {
log.info("sendEmailToUser vo={}",vo);
this.sendMessageToUserByPhone(vo.getPhone());
this.sendEmailToUserByEmail(vo);
}
public void sendMessageToUserByPhone(String phone) {
log.info("sendMessageToUserByPhone phone={}",phone);
}
public void sendEmailToUserByEmail(UserReqBean vo) {
log.info("sendEmailToUserByEmail email={}",vo.getEmail());
}
}
3、经过上面两步就可以实现了,下面给出DEMO中的【UserReqBean】类。
package cn.renkai721.bean;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.Accessors;
import lombok.extern.slf4j.Slf4j;
import java.io.Serializable;
@Data
@Slf4j
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Accessors(chain = true)
@ApiModel(
description = "测试异步用户对象"
)
public class UserReqBean implements Serializable {
@ApiModelProperty("用户ID主键")
private String userId;
@ApiModelProperty("手机号码")
private String phone;
@ApiModelProperty("邮箱")
private String email;
}
第二章 唠嗑
1、仔细看DEMO的人会发现,在传递request的时候,居然和普通的方法不一样,不能直接使用。而是使用了AsyncContext。
2、这里还是有一个注意的点,就是asyncContext.setTimeout(0);这个方法不能少,否则会报错,提示NullPointerException。
java.lang.NullPointerException
at org.apache.coyote.http11.InternalOutputBuffer.realWriteBytes(InternalOutputBuffer.java:215)
at org.apache.tomcat.util.buf.ByteChunk.flushBuffer(ByteChunk.java:480)
at org.apache.coyote.http11.InternalOutputBuffer.flush(InternalOutputBuffer.java:119)
at org.apache.coyote.http11.AbstractHttp11Processor.action(AbstractHttp11Processor.java:799)
at org.apache.coyote.Response.action(Response.java:174)
at org.apache.catalina.connector.OutputBuffer.doFlush(OutputBuffer.java:366)
at org.apache.catalina.connector.OutputBuffer.flush(OutputBuffer.java:333)
at org.apache.catalina.connector.CoyoteWriter.flush(CoyoteWriter.java:98)
at cn.cloud.action.CounterThread.printPage(LoginServlet.java:69)
at cn.cloud.action.CounterThread.run(LoginServlet.java:53)
九月 06, 2022 13:03:16 上午 org.apache.catalina.core.StandardContext reload
信息: Reloading Context with name [/myshop] has started
九月 06, 2022 13:03:16 上午 org.apache.catalina.core.StandardContext reload
信息: Reloading Context with name [/myshop] is completed
第三章 课外知识
1、我们在实际的工作中,有时候会遇到一些非核心的附加功能,比如短信或微信模板消息通知,或者一些耗时比较久,但主流程不需要立即获得其结果反馈的操作,比如保存图片、同步数据到其它合作方等等。如果将这些操作都置于主流程中同步处理,势必会对核心流程的性能造成影响,甚至由于第三方服务的问题导致自身服务不可用。这时候就应该将这些操作异步化,以提高主流程的性能,并与第三方解耦,提高主流程的可用性。在Spring Boot中,或者说在Spring中,我们实现异步处理一般有以下几种方式:
通过 @EnableAsync 与 @Asyc 注解结合实现 | 参照本文DEMO |
通过异步事件实现 | |
通过消息队列实现 |
2、一般都是同步调用的,但经常有特殊业务需要做异步来处理。比如:注册用户、需要送积分、发短信和邮件、或者下单成功、发送消息等等。
1 | 容错问题,如果送积分出现异常,不能因为送积分而导致用户注册失败。 |
2 | 提升性能,比如注册用户花了30毫秒,送积分划分50毫秒,如果同步的话一共耗时:70毫秒,用异步的话,无需等待积分,故耗时是:30毫秒就完成了业务。 |
3、异步,同步的区别
同步:按顺序执行,前面结束了,后面才开始,是一个串行调用。
异步:按顺序执行,是否现在执行异步中的方法不一定,但是一定会先顺序执行异步方法外的代码。