Spring MVC
1. 参数绑定
@RequestParam: 接收的参数是来自requestHeader中,即请求头。通常用于GET请求,可以获取URL上的数据.用来处理 Content-Type 为 application/x-www-form-urlencoded 编码的内容,Content-Type默认为该属性
- required 表示是否必须,默认为 true,必须。
- defaultValue 可设置请求参数的默认值。
- value 为接收url的参数名(相当于key值)。
@RequestBody: 接收的参数是来自requestBody中,即请求体.一般用于处理非 Content-Type: application/x-www-form-urlencoded编码格式的数据,比如:application/json、application/xml等类型的数据。 比如
@PostMapping("/map")
public @ResponseBody User getMap(@RequestBody Map<String, Object> map) {
}
//对应到Talent API Tester 请求应该是[在postman中将body选为raw->JSON(application/json)]
//POST:http://localhost/map
//HEADERS:Content-Type/application/json
//BODY:{"name":"啊"}
@PostMapping("/map1")
public @ResponseBody User getMap1(@RequestParam Map<String, Object> map) {
}
//对应的请求应该是(在Postman中将body选为x-www-form-urlencoded )
//POST:http://localhost/map1
//HEADERS:Content-Type/application/x-www-form-urlencoded
//BODY:{"name":"啊"}
1.包装数据类型
包装数据类型绑定的时候(一般不使用基础数据类型),前台请求key值可以不传,数据也可以为空,成功绑定需要key值和参数名一致,另外可以@RequestParam来建立对应关系.
2.数组元素
@RequestMapping("/array")
@ResponseBody
public String[] getUser(String[] name) {
return name;
}
// http://localhost/user?name=a&name=b
3.多层级对象
@RequestMapping("/user")
@ResponseBody
public User getUser(User user) {
return user;
}
// http://localhost/user?name=a&role.name=admin
// User对象中包含role对象
3.1 多对象含相同属性
user:name\type... role:name\type...
@InitBinder("user")
public void initUser(WebDataBinder binder) {
binder.setFieldDefaultPrefix("user.");
}
@InitBinder("role")
public void initAdmin(WebDataBinder binder) {
binder.setFieldMarkerPrefix("role.");
}
//同时在请求中要在各个属性前加上 对象前缀.
3.2 List类型
需要新建一个类用来保存list对象,
// UserList{ private List<User> users;//users与前台请求一致}
@GetMapping("/list")
@ResponseBody
public String getUser(UserList userList) {
return userList.toString();
}
// http://localhost/list?users[0].name=xxx&users[1].name=yyy
3.3 Map类型
和List类似,需要额外的类用来维护该对象
2. Restful API
@RestController
@RequestMapping("/users")
public class UserController {
@GetMapping("/")//get请求:用户列表
public List<User> listUsers() {
return new ArrayList<User>();
}
@PostMapping("/")// post请求:新增用户
public String postUser(@RequestBody(required = false) User user) {
return "success";
}
@GetMapping("/{id}")// get请求:根据id获取用户
public User getUser(@PathVariable Long id) {
return new User();
}
@PutMapping("/{id}")// put请求:根据id,user更新用户
public String putUser(@PathVariable Long id, @RequestBody User user) {
return "success";
}
@DeleteMapping("/{id}")
public String deleteUser(@PathVariable Long id) {
return "success";
}
}
测试:
1.GET http://localhost/users/
2.Post http://localhost/users/
HEADERS Content-Type:application/json
BODY {"name":"aaa"}
3.GET http://localhost/users/1
4.PUT http://localhost/users/1
HEADERS Content-Type:application/json
BODY {"name":"aaa"}
5.DELETE http://localhost/users/1
3. JSON解析器
SpringBoot默认使用Jackson,替换为:Fastjson,先引入com.alibaba.fastjson
public class MvcConfig implements WebMvcConfigurer {
@Override
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
/*
先把Jackson的消息转换器删除.
备注:
(1)源码分析可知,返回json的过程为: Controller调用结束后返回一个数据对象,for循环遍历conventers,找到支持application/json的HttpMessageConverter,然后将返回的数据序列化成json。
具体参考org.springframework.web.servlet.mvc.method.annotation.AbstractMessageConverterMethodProcessor的writeWithMessageConverters方法
(2)由于是list结构,我们添加的fastjson在最后。因此必须要将jackson的转换器删除,不然会先匹配上jackson,导致没使用fastjson
*/
for (int i = converters.size() - 1; i >= 0; i--) {
if (converters.get(i) instanceof MappingJackson2HttpMessageConverter) {
converters.remove(i);
}
}
FastJsonHttpMessageConverter fastJsonHttpMessageConverter = new FastJsonHttpMessageConverter();
//自定义fastjson配置
FastJsonConfig config = new FastJsonConfig();
config.setSerializerFeatures(
SerializerFeature.WriteMapNullValue, // 是否输出值为null的字段,默认为false,我们将它打开
SerializerFeature.WriteNullListAsEmpty, // 将Collection类型字段的字段空值输出为[]
SerializerFeature.WriteNullStringAsEmpty, // 将字符串类型字段的空值输出为空字符串
SerializerFeature.WriteNullNumberAsZero, // 将数值类型字段的空值输出为0
SerializerFeature.WriteDateUseDateFormat,
SerializerFeature.DisableCircularReferenceDetect // 禁用循环引用
);
fastJsonHttpMessageConverter.setFastJsonConfig(config);
// 添加支持的MediaTypes;不添加时默认为*/*,也就是默认支持全部
// 但是MappingJackson2HttpMessageConverter里面支持的MediaTypes为application/json
// 参考它的做法, fastjson也只添加application/json的MediaType
List<MediaType> fastMediaTypes = new ArrayList<>();
fastMediaTypes.add(MediaType.APPLICATION_JSON_UTF8);
fastJsonHttpMessageConverter.setSupportedMediaTypes(fastMediaTypes);
converters.add(fastJsonHttpMessageConverter);
}
}
测试如下
@JSONField(format = "yyyy-MM-dd")
LocalDateTime time;
@GetMapping("/")//get请求:用户列表
public List<User> listUsers() {
List<User> list = new ArrayList<User>();
User user = new User();
user.setName("1");
user.setTime(LocalDateTime.now());
list.add(user);
return list;
}
// [{"name":"1","time":"2020-02-14"}]
4. 表单验证
表单验证如果仅仅在前端处理,仍然有比如直接用postman请求来跳过其验证的方法,所以后台往往也需要添加数据验证,表单验证可以使用JSR 303的Bean Validation,在实体类上添加相应的注解,就可以完成后台表单验证
@Data
public class User {
@NotNull(message = "名字不能为空")
@Size(min = 2, max = 10, message = "长度在2到10之间")
private String name;
@JSONField(format = "yyyy-MM-dd")
private LocalDateTime time;
}
其中@NotNull/@Size均是javax.validation.constraints包下的内容 ,更多使用方法可以参考 JSR 303 控制器中相关地方只需要添加BindingResult和@Valid即可.
@PostMapping("/test")
public Object test(@Valid @RequestBody User user, BindingResult bindingResult) {
if (bindingResult.hasErrors()) {
return bindingResult.getAllErrors();
}
return "ok";
}
模拟请求和响应如下:
POST /valid/test HTTP/1.1
Content-Length: 12
Content-Type: application/json
Host: localhost:8080
{"name":"1"}
Content-Type: application/json;charset=UTF-8
Content-Length: 309
Date: Sun, 01 Mar 2020 08:07:25 GMT
Keep-Alive: timeout=60
Connection: keep-alive
[{"arguments":[{"arguments":[],"code":"name","codes":["user.name","name"],"defaultMessage":"name"},10,2],"bindingFailure":false,"code":"Size","codes":["Size.user.name","Size.name","Size.java.lang.String","Size"],"defaultMessage":"长度在2到10之间","field":"name","objectName":"user","rejectedValue":"1"}]
5. 请求Rest接口
@PostMapping("/test")
public String test() {
RestTemplate restTemplate = new RestTemplate();
return restTemplate.getForObject("http://localhost:8080/users/1", UserDO.class).toString();
}
6. 上传下载
//Servlet原生
response.setCharacterEncoding("utf-8");
response.setContentType("application/octet-stream");
response.setHeader("Content-disposition", "attachment;filename=\""
+ new String(fileName.getBytes("UTF-8"),"ISO8859_1") + "\"");
FileInputStream fis = new FileInputStream(path+fileName);
ServletOutputStream sos = response.getOutputStream();
int b;
byte[] buffer = new byte[1024*8];
while ((b = fis.read(buffer))!= -1) {
sos.write(buffer, 0, b);
}
fis.close();
sos.close();
//SpringMVC
@RequestMapping("/download")
public ResponseEntity<byte[]> download(@RequestParam String fileName) throws IOException {
File file = new File("C:\\"+fileName);
byte[] body = null;
InputStream inputStream = new FileInputStream(file);
body = new byte[inputStream.available()];
inputStream.read(body);
HttpHeaders httpHeaders = new HttpHeaders();
httpHeaders.add("Content-disposition","attachment;filename="+fileName);
HttpStatus httpStatus = HttpStatus.OK;
ResponseEntity<byte[]> entity = new ResponseEntity<>(body,httpHeaders,httpStatus);
return entity;
}
Spring Bean
Bean的生命周期
实例化 -> 属性赋值 -> 初始化 -> 销毁
Bean级生命周期接口
- BeanNameAware(对象实例化设置属性之后设置BeanName)
- BeanFactoryAware(设置BeanFactory)
- InitializingBean(实例化之后调用)
- DisposableBean (销毁)
package com.demo.bean;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.*;
@Slf4j
public class Test implements BeanNameAware, BeanFactoryAware, InitializingBean, DisposableBean {
private String name;
public String getName() {
return name;
}
public void setName(String name) {
System.out.println("setName"+name);
this.name = name;
}
@Override
public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
System.out.println(beanFactory.toString());
}
@Override
public void setBeanName(String s) {
System.out.println("beanName:"+s);
}
@Override
public void afterPropertiesSet() throws Exception {
System.out.println("afterPropertiesSet");
}
@Override
public void destroy() throws Exception {
System.out.println("destroy");
}
private void myInit() {
System.out.println("myInit");
}
private void myDestroy() {
System.out.println("myDes");
}
}
}
package com.demo.bean;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.BeanPostProcessor;
/**
* 实例化完成之后调用该接口
*/
public class MyBeanPostProcessor implements BeanPostProcessor {
//实例化完成,setBeanName/setBeanFactory完成之后调用该方法
public Object postProcessBeforeInitialization(Object o, String s) throws BeansException {
System.out.println("postProcessBeforeInitialization");
return o;
}
//全部是实例化完成以后调用该方法
public Object postProcessAfterInitialization(Object o, String s) throws BeansException {
System.out.println("postProcessAfterInitialization");
return o;
}
}
package com.demo.bean;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.InstantiationAwareBeanPostProcessorAdapter;
/**
* 实例化前/后,及框架设置Bean属性时调用该接口
*/
public class MyInstantiationAwareBeanPostProcessor extends InstantiationAwareBeanPostProcessorAdapter {
//在Bean对象实例化前调用
@Override
public Object postProcessBeforeInstantiation(Class<?> beanClass, String beanName) throws BeansException {
System.out.println("postProcessBeforeInstantiation");
return null;
}
//在Bean对象实例化后调用(如调用构造器之后调用)
@Override
public boolean postProcessAfterInstantiation(Object bean, String beanName) throws BeansException {
System.out.println("postProcessAfterInstantiation");
return true;
}
}
<?xml version="1.0" encoding="UTF-8"?>
<beans
xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:p="http://www.springframework.org/schema/p"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd">
<bean id="test" class="com.demo.bean.Test"
p:name = "Paul"
init-method="myInit"
destroy-method="myDestroy"
/>
</beans>
@Resource
private BeanFactory beanFactory;
@Test
void beanFactory(){
org.springframework.core.io.Resource resource = new ClassPathResource("Test.xml");
BeanFactory beanFactory = new DefaultListableBeanFactory();
XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader((DefaultListableBeanFactory) beanFactory);
reader.loadBeanDefinitions(resource);
//向容器中注册后处理器 MyBeanPostProcessor
((DefaultListableBeanFactory) beanFactory).addBeanPostProcessor(new MyBeanPostProcessor());
//向容器中注册后处理器 MyInstantiationAwareBeanPostProcessor
//注意:后处理器调用顺序和注册顺序无关。在处理多个后处理器的情况下,必需通过实现Ordered接口来确定顺序
((DefaultListableBeanFactory) beanFactory).addBeanPostProcessor(new MyInstantiationAwareBeanPostProcessor());
com.demo.bean.Test test = (com.demo.bean.Test) beanFactory.getBean("test");
((DefaultListableBeanFactory) beanFactory).destroySingletons();
}
}
输出如下:
postProcessBeforeInstantiation postProcessAfterInstantiation setNamePaul beanName:test org.springframework.beans.factory.support.DefaultListableBeanFactory@2f0e7fa8: defining beans [test]; root of factory hierarchy postProcessBeforeInitialization afterPropertiesSet myInit postProcessAfterInitialization Paul destroy myDes
容器级Bean生命周期接口
- InstantiationAwareBeanPostProcessorAdapter
//在Bean对象实例化前调用
@Override
public Object postProcessBeforeInstantiation(Class<?> beanClass, String beanName) throws BeansException;
//在Bean对象实例化后调用(如调用构造器之后调用)
@Override
public boolean postProcessAfterInstantiation(Object bean, String beanName) throws BeansException;
/**
* 在设置某个属性前调用,然后再调用设置属性的方法
* 注意:这里的设置属性是指通过配置设置属性,直接调用对象的setXX方法不会调用该方法,如bean配置中配置了属性address/age属性,将会调用该方法
* @param pvs 如 PropertyValues: length=2; bean property 'address'; bean property 'age'
*/
@Override
public PropertyValues postProcessPropertyValues(PropertyValues pvs, PropertyDescriptor[] pds, Object bean, String beanName) throws BeansException;
- BeanPostProcessor
//实例化完成,setBeanName/setBeanFactory完成之后调用该方法
public Object postProcessBeforeInitialization(Object o, String s) throws BeansException;
//全部是实例化完成以后调用该方法
public Object postProcessAfterInitialization(Object o, String s) throws BeansException;
BeanFactory和ApplicationContext
前者是Spring容器底层实现的基础,负责配置创建管理bean,它主要的方法由getBean,在beanfactory初始化的时候并不会创建bean,只有在读取配置从容器中拿bean的时候才会实例化,与后者相比,它只能提供基本的DI功能.
- ApplicationContext(称为Spring上下文)是BeanFactory的子接口,它们都可以当做Spring的容器,Spring容器是生成Bean实例的工厂,并管理容器中的Bean。在基于Spring的Java EE应用中,所有组件都被当成Bean处理,包括数据源,SessionFactory、事务管理器等。在启动的时候就把所有的Bean全部实例化了。也可以为Bean配置lazy-init=true来让Bean延迟实例化.
其常用的实现类包括:
- AnnotationConfigApplicationContext:从一个或多个基于java的配置类中加载上下文定义,适用于java注解的方式。
- ClassPathXmlApplicationContext:从类路径下的一个或多个xml配置文件中加载上下文定义,适用于xml配置的方式。
Bean的作用域
Spring默认的Bean是容器级单例的(即通过SpringIOC获取的对象都是唯一的),而Java中使用比如静态枚举类实现的单例是JVM级的单例. 通常使用@Scope来声明bean的作用域:
- singleton:IOC容器仅创建一个Bean实例,IOC容器每次返回的是同一个Bean实例。
- prototype:IOC容器可以创建多个Bean实例,每次返回的都是一个新的实例。
- request:该属性仅对HTTP请求产生作用,使用该属性定义Bean时,每次HTTP请求都会创建一个新的Bean,适用于WebApplicationContext环境。
- session:该属性仅用于HTTP Session,同一个Session共享一个Bean实例。不同Session使用不同的实例。(用于比如购物车等场景)
- global-session:该属性仅用于HTTP Session,同session作用域不同的是,所有的Session共享一个Bean实例。
Bean循环依赖问题
比如classA中有一个成员变量B,B中又有A作为成员变量.在Spring中是通过三级缓存解决循环依赖的问题的( 当bean是单例模式的时候才能被缓存). Spring单例对象的初始化主要是三步:
实例化(instance)-填充属性-初始化(调用init方法) 循环依赖主要是因为前两步:
在A实例化后,便将自己曝光在singletonFactories中,当注入B属性的时候发现B还没有被create,所以走create流程,B初始化第一步的时候发现依赖A,于是尝试getA,在一级缓存singletonObjects二级缓存earlySingletonObjects都没有找到A,尝试从三级缓存singletonFactories中,由于A的提前曝光,所以B能拿到A的引用,在B拿到之后便完成了初始化,并将自己放到一级缓存中,此时返回到A中,便能顺利完成初始化.
/** Cache of singleton objects: bean name --> bean instance */
private final Map<String, Object> singletonObjects = new ConcurrentHashMap<String, Object>(256);
/** Cache of singleton factories: bean name --> ObjectFactory */
private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<String, ObjectFactory<?>>(16);
/** Cache of early singleton objects: bean name --> bean instance */
private final Map<String, Object> earlySingletonObjects = new HashMap<String, Object>(16);
singletonFactories : 单例对象工厂的cache earlySingletonObjects :提前暴光的单例对象的Cache singletonObjects:单例对象的cache 由于加入singletonFactories三级缓存的前提是执行了构造器,所以构造器的循环依赖没法解决.
Bean的创建和使用
创建
- 注解 一般就是在类上添加@Component, @Service等注解(@Controller表示提供http接口的bean, @Configuration表示配置类Bean)( 注意bean的包路径扫描配置)
- @Bean 主要是结合Configuration来定义bean,首先是声明一个配置类,然后再配置类中,通过返回bean对象的方法形式来声明bean
@Configuration
public class BeanLoadConfig {
@Bean
public ConfigDemoBean configDemoBean() {
return new ConfigDemoBean();
}
}
- 工厂类方式 实现FactoryBean接口
@Component
public class MyBean implements FactoryBean<Object> {
@Override
public Object getObject() throws Exception {
return null;
}
@Override
public Class<?> getObjectType() {
return null;
}
}
再通过MyBean来获取所需要的bean对象
使用
- Autowired @Autowired或者@Resource添加到成员变量上,即表示这个成员变量会由Spring容器注入对应的Bean对象
- Setter
private MyBean demo;
@Autowired
private void setMyBean(Mybean demo) {
this.demo = demo;
}
- 构造方法
public class DemoController {
private MyBean demo;
public DemoController(MyBean demo) {
this.demo= demo;
}
}
多实例Bean的选择
比如一个接口两个实现类,
IPrint: ConsolePrint,LogPrint
@Autowired注解时,属性名即为默认的Bean名,如下面的logPrint就是获取beanName=logPrint的bean @Resource(name=xxx) 直接指定Bean的name,来唯一选择匹配的bean
@Resource(name = "consolePrint")
private IPrint consolePrint;
@Autowired
private IPrint logPrint;
也可以通过@Primary来解决:当有多个bean满足注入条件时,有这个注解的实例被选中
@Primary注解的使用有唯一性要求:一个接口的子类中,只能有一个实现上有这个注解
尽量使用构造注入
保证依赖不可变(final关键字) 保证依赖不为空(省去了我们对其检查)
保证返回客户端(调用)的代码的时候是完全初始化的状态
避免了循环依赖
提升了代码的可复用性
@RequiredArgsConstructor
@Service
public class AServiceImpl implements AService {
private final JdbcTemplate jdbcTemplate;
}
静态实例
@Component
@Slf4j
public class MinioUtil {
private static MinioClient minioClient;
private MinioConfig minioConfig;
//不需要显示的添加@Autowire(如果类只提供了一个带参数的构造方法)
public MinioUtil(MinioConfig minioConfig) {
this.minioConfig = minioConfig;
minioClient = minioConfig.getMinioClient();
}
}
Spring AOP
异常处理
@ControllerAdvice
@Slf4j
public class CommonExceptionAdvice {
@ExceptionHandler(Exception.class)
@ResponseBody
public CommonResponseBody handlerException(Exception e) {
log.error(" 公共异常: {} ", e);
return new CommonResponseBody(ERROR.getCode(), ERROR.getMsg(), e.getMessage());
}
@ExceptionHandler(IllegalArgumentException.class)
@ResponseBody
public CommonResponseBody handlerIllegalArgumentException(IllegalArgumentException e) {
log.error("非法参数异常: {} ", e);
return new CommonResponseBody(PARAMS_ERROR.getCode(), PARAMS_ERROR.getMsg(), e.getMessage());
}
@ExceptionHandler(MethodArgumentNotValidException.class)
@ResponseBody
public CommonResponseBody handlerMethodArgumentNotValidException(MethodArgumentNotValidException e) {
log.error("非法参数异常: {} ", e);
List<String> errors = e.getBindingResult().getAllErrors().stream().map(ObjectError::getDefaultMessage).collect(Collectors.toCollection(ArrayList::new));
return new CommonResponseBody(PARAMS_ERROR.getCode(), PARAMS_ERROR.getMsg(), errors);
}
@ExceptionHandler(NullPointerException.class)
@ResponseBody
public CommonResponseBody handlerNullPointerException(NullPointerException e) {
log.error("空值异常: {} ", e);
return new CommonResponseBody(PARAMS_NULL.getCode(), PARAMS_NULL.getMsg(), e.getMessage());
}
@ExceptionHandler(CustomException.class)
@ResponseBody
public CommonResponseBody handlerCustomException(CustomException e) {
log.error("自定义异常: {} ", e);
return new CommonResponseBody(e.getCode(), e.getMsg(), "");
}
}
@Data
@AllArgsConstructor
public class CommonResponseBody implements Serializable {
private Integer code;
private String msg;
private Object data;
public CommonResponseBody(Integer code, String msg) {
this.code = code;
this.msg = msg;
this.data = "";
}
}
@Data
@AllArgsConstructor
public class CustomException extends Exception {
private Integer code;
private String msg;
}
@RestControllerAdvice
@RequiredArgsConstructor
public class CommonResponseBodyAdvice implements ResponseBodyAdvice<Object> {
private final ObjectMapper objectMapper;
@Override
public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
return true;
}
@SneakyThrows
@Override
public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class<? extends HttpMessageConverter<?>> selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
if (MediaType.IMAGE_JPEG.getType().equalsIgnoreCase(selectedContentType.getType())) {
return body;
}
if (body instanceof CommonResponseBody) {
return body;
}
if (body instanceof String) {
return objectMapper.writeValueAsString(new CommonResponseBody(SUCCESS.getCode(), SUCCESS.getMsg(), body));
}
return new CommonResponseBody(SUCCESS.getCode(), SUCCESS.getMsg(), body);
}
}
@Getter
public enum CommonResponseEnum {
SUCCESS(20000, "操作成功!"),
ERROR(40000, "操作失败!"),
DATA_NOT_FOUND(40001, "查询失败!"),
PARAMS_NULL(40002, "参数不能为空!"),
NOT_LOGIN(40003, "当前账号未登录!"),
PARAMS_ERROR(40005, "参数不合法!"),
AUTH_DENY(4006, "没有权限"),
;
private Integer code;
private final String msg;
CommonResponseEnum(Integer code, String msg) {
this.code = code;
this.msg = msg;
}
}
简单AuthFilter
@Component
public class CommonAuthFilter implements Filter {
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) servletRequest;
try {
if (Objects.equals(request.getHeader("token"), "123")) {
filterChain.doFilter(servletRequest, servletResponse);
} else {
throw new CommonExceptionAdvice.CustomException(AUTH_DENY.getCode(), AUTH_DENY.getMsg());
}
} catch (CommonExceptionAdvice.CustomException e) {
servletResponse.setCharacterEncoding("utf-8");
servletResponse.setContentType(MediaType.APPLICATION_JSON_VALUE);
servletResponse.getWriter().print(new ObjectMapper().writeValueAsString(new CommonResponseBody(e.getCode(), e.getMsg())));
}
}
}
AOP 日志(非异步)
package com.yonyou.aspect;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.Data;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.AfterThrowing;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import org.springframework.web.multipart.MultipartFile;
import javax.servlet.http.HttpServletRequest;
import java.util.HashMap;
import java.util.Map;
/**
* @author zongkxc
* @Description
* @create 2021/6/8 10:14
*/
@Component
@Aspect
@Slf4j
public class RequestLogAspect {
@Pointcut("execution(* com.yonyou.web..*(..))")
public void requestServer() {
}
@Around("requestServer()")
public Object doAround(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
long start = System.currentTimeMillis();
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = attributes.getRequest();
Object result = proceedingJoinPoint.proceed();
RequestInfo requestInfo = new RequestInfo();
requestInfo.setIp(request.getRemoteAddr());
requestInfo.setUrl(request.getRequestURL().toString());
requestInfo.setHttpMethod(request.getMethod());
requestInfo.setClassMethod(String.format("%s.%s", proceedingJoinPoint.getSignature().getDeclaringTypeName(),
proceedingJoinPoint.getSignature().getName()));
requestInfo.setRequestParams(getRequestParamsByProceedingJoinPoint(proceedingJoinPoint));
requestInfo.setResult(result);
requestInfo.setTimeCost(System.currentTimeMillis() - start);
log.info("Request Info : {}", new ObjectMapper().writeValueAsString(requestInfo));
return result;
}
@SneakyThrows
@AfterThrowing(pointcut = "requestServer()", throwing = "e")
public void doAfterThrow(JoinPoint joinPoint, RuntimeException e) {
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = attributes.getRequest();
RequestErrorInfo requestErrorInfo = new RequestErrorInfo();
requestErrorInfo.setIp(request.getRemoteAddr());
requestErrorInfo.setUrl(request.getRequestURL().toString());
requestErrorInfo.setHttpMethod(request.getMethod());
requestErrorInfo.setClassMethod(String.format("%s.%s", joinPoint.getSignature().getDeclaringTypeName(),
joinPoint.getSignature().getName()));
requestErrorInfo.setRequestParams(getRequestParamsByJoinPoint(joinPoint));
requestErrorInfo.setException(e);
log.info("Error Request Info : {}", new ObjectMapper().writeValueAsString(requestErrorInfo));
}
private Map<String, Object> getRequestParamsByProceedingJoinPoint(ProceedingJoinPoint proceedingJoinPoint) {
String[] paramNames = ((MethodSignature) proceedingJoinPoint.getSignature()).getParameterNames();
Object[] paramValues = proceedingJoinPoint.getArgs();
return buildRequestParam(paramNames, paramValues);
}
private Map<String, Object> getRequestParamsByJoinPoint(JoinPoint joinPoint) {
String[] paramNames = ((MethodSignature) joinPoint.getSignature()).getParameterNames();
Object[] paramValues = joinPoint.getArgs();
return buildRequestParam(paramNames, paramValues);
}
private Map<String, Object> buildRequestParam(String[] paramNames, Object[] paramValues) {
Map<String, Object> requestParams = new HashMap<>();
for (int i = 0; i < paramNames.length; i++) {
Object value = paramValues[i];
if (value instanceof MultipartFile file) {
value = file.getOriginalFilename();
}
requestParams.put(paramNames[i], value);
}
return requestParams;
}
@Data
public class RequestInfo {
private String ip;
private String url;
private String httpMethod;
private String classMethod;
private Object requestParams;
private Object result;
private Long timeCost;
}
@Data
public class RequestErrorInfo {
private String ip;
private String url;
private String httpMethod;
private String classMethod;
private Object requestParams;
private RuntimeException exception;
}
}
AOP 日志 (异步)
Spring Transaction
Spring事务管理分为编程式和声明式,声明式基于AOP,将业务和事务解耦,声明式的事务不会污染业务代码,常用于Service层。
传播性(propagation)
- PROPAGATION_REQUIRED(默认) 在程序运行中,如果一个方法已经在一个事务中,那么就加入改事务,否则创建一个新的事务。
- PROPAGATION_REQUIRES_NEW 不管是否存在事务,该方法总会为自己发起一个新的事务。如果方法已经运行在一个事务中,则原有事务挂起,新的事务被创建。
- PROPAGATION_SUPPORTS 假设ServiceB.methodB() 的事务级别为 PROPAGATION_SUPPORTS,那么当执行到ServiceB.methodB() 时,如果发现ServiceA.methodA()已经开启了一个事务,则加入当前的事务,如果发现ServiceA.methodA() 没有开启事务,则自己也不开启事务。这种时候,内部方法的事务性完全依赖于最外层的事务。
事务不生效的原因
- spring默认是通过动态代理实现aop(jdk代理:基于接口,cglib基于继承),无法代理private方法。 可以手动织入,@spectj,编译的时候织入需要代理的方法;或者私有方法更改为public
- 需要通过代理类调用方法才能调用事务增强过的方法。可以在service中注入一个selfbean,且子调用必须用注入的对象进行调用,否则还是this。 事务生效
回滚不生效的原因
1,手动catch到异常 2,默认只对runtimeexception和error进行回滚,对于受检异常,并不会回滚,这种异常常与业务相关。可以在@transactional(rollbackFor=" Exception.class")
Spring Async
引入
今天线上的项目遇到一个问题,有个Ajax请求后台需要处理的时间太长,后台业务大致是主表和子表数据的迁移,子表数量过多且需要复制数据导致请求时间过长。 考虑Ajax轮询或者后台多线程处理,鉴于子表的数据迁移之间互不影响,所以使用@Async来异步处理子表的数据迁移工作。
开启
Spring原生配置application.xml
beans里面需要添加:
xmlns:task="http://www.springframework.org/schema/task"
xsi:schemaLocation中添加:
http://www.springframework.org/schema/task
http://www.springframework.org/schema/task/spring-task.xsd"
<task:annotation-driver>
SpringBoot只需要在启动类上添加注解@EnableAsync即可
带返回值的Future
测试例子如下:
@Component
public class AsyncTask {
@Async
public Future<String> doTask1(String xlhs) throws Exception {
System.out.println("开始做任务一");
// 子表1 拷贝入库操作
return new AsyncResult<String>("ok");
}
@Async
public Future<String> doTask2() throws Exception {
System.out.println("开始做任务二");
// 子表2 拷贝入库操作
return new AsyncResult<String>("ok");
}
}
控制器中代码:
//
Future<String> task1 = testTaskAsync.doTask1();
Future<String> task2 = testTaskAsync.doTask2();
while(true){
if(task1.isDone()&& task2.isDone()){
break;
}
}
//
控制台可见 任务一和任务二并不是顺序执行的,即完成@Async的使用,使用异步的核心就是程序在顺序执行时,不等待异步调用的语句返回结果就执行后面的程序。 每个异步方法都在在独立的线程内完成操作,这样可以有效减少响应时间。
Future接口
public interface Future<V> {
boolean cancel(boolean mayInterruptIfRunning);
boolean isCancelled();
boolean isDone();
V get() throws InterruptedException, ExecutionException;
V get(long timeout, TimeUnit unit)
throws InterruptedException, ExecutionException, TimeoutException;
}
cancel方法用来取消任务,如果取消任务成功则返回true,如果取消任务失败则返回false。参数mayInterruptIfRunning表示是否允许取消正在执行却没有执行完毕的任务,如果设置true,则表示可以取消正在执行过程中的任务。如果任务已经完成,则无论mayInterruptIfRunning为true还是false,此方法肯定返回false,即如果取消已经完成的任务会返回false;如果任务正在执行,若mayInterruptIfRunning设置为true,则返回true,若mayInterruptIfRunning设置为false,则返回false;如果任务还没有执行,则无论mayInterruptIfRunning为true还是false,肯定返回true。 - isCancelled方法表示任务是否被取消成功,如果在任务正常完成前被取消成功,则返回 true。 - isDone方法表示任务是否已经完成,若任务完成,则返回true; - get() 方法用来获取执行结果,这个方法会产生阻塞,会一直等到任务执行完毕才返回; - get(long timeout, TimeUnit unit) 用来获取执行结果,如果在指定时间内,还没获取到结果,就直接返回null。
Future提供了三种功能:判断任务是否完成;能够中断任务;能够获取任务执行结果。