SpringMVC & SpringBoot小记
SpringMVC
1、SpringMVC常用注解
https://blog.csdn.net/lipinganq/article/details/79155072
https://blog.csdn.net/javazejian/article/details/54561302
用于依赖注入:
Bean注入:@Autowired、@Resource。都可用于Bean注入,区别:(可参阅:https://blog.csdn.net/u012102104/article/details/79481007)
@Autowired:Spring的注解;按类型来查找相应的实例进行注入,若还用了@Qualifier则按指定的实例名查找实例;有name、type等属性
@Resource:JDK1.6开始提供的注解;优先按实例名字查找实例,若找不到再按类型查找;只有required属性
配置文件中的参数注入到java变量:@Value
用于Bean定义:(根据被Spring容器发现的方式可分为两类)
自动扫描:@Component(@Controller、@Service、@Repository,三者与@Component等效,只不过在语义上更明确):用在类上,声明此类是个Bean,会被Spring ComponentScan扫描并由Spring容器自动创建实例并注册
手动配置:@Configuration+@Bean:前者用在类上、后者用在@Configuration修饰的类内的方法上,效果相当于xml中的beans与bean的关系;实例需要用户在@Bean修饰的方法中手动创建返回,Spring容器通过@Configuration扫描并注册其所修饰的类内的Bean
其他:@Scope、@RequestMapping(@GetMapping、@PostMapping....)、@ResponseBody等
1、@Component
@Component
是所有受Spring 管理组件的通用形式,@Component注解可以放在类的头上,@Component不推荐使用。
2、@Controller
@Controller对应表现层的Bean,也就是Action,例如:
1 @Controller 2 @Scope("prototype") 3 public class UserAction extends BaseAction<User>{ 4 …… 5 }
使用@Controller注解标识UserAction之后,就表示要把UserAction交给Spring容器管理,在Spring容器中会存在一个名字为"userAction"的action,这个名字是根据UserAction类名来取的。注意:如果@Controller不指定其value【@Controller】,则默认的bean名字为这个类的类名首字母小写,如果指定value【@Controller(value="UserAction")】或者【@Controller("UserAction")】,则使用value作为bean的名字。
这里的UserAction还使用了@Scope注解,@Scope("prototype")表示将Action的范围声明为原型,可以利用容器的scope="prototype"来保证每一个请求有一个单独的Action来处理,避免struts中Action的线程安全问题。spring 默认scope是单例模式(scope="singleton"),这样只会创建一个Action对象,每次访问都是同一Action对象,数据不安全,struts2 是要求每次次访问都对应不同的Action,scope="prototype" 可以保证当有请求的时候都创建一个Action对象
3、@ Service
@Service对应的是业务层Bean,例如:
1 @Service("userService") 2 public class UserServiceImpl implements UserService { 3 ……… 4 }
@Service("userService")注解是告诉Spring,当Spring要创建UserServiceImpl的的实例时,bean的名字必须叫做"userService",这样当Action需要使用UserServiceImpl的的实例时,就可以由Spring创建好的"userService",然后注入给Action:在Action只需要声明一个名字叫“userService”的变量来接收由Spring注入的"userService"即可,具体代码如下:
1 // 注入userService 2 @Resource(name = "userService") 3 private UserService userService;
注意:在Action声明的“userService”变量的类型必须是“UserServiceImpl”或者是其父类“UserService”,否则由于类型不一致而无法注入,由于Action中的声明的“userService”变量使用了@Resource注解去标注,并且指明了其name = "userService",这就等于告诉Spring,说我Action要实例化一个“userService”,你Spring快点帮我实例化好,然后给我,当Spring看到userService变量上的@Resource的注解时,根据其指明的name属性可以知道,Action中需要用到一个UserServiceImpl的实例,此时Spring就会把自己创建好的名字叫做"userService"的UserServiceImpl的实例注入给Action中的“userService”变量,帮助Action完成userService的实例化,这样在Action中就不用通过“UserService userService = new UserServiceImpl();”这种最原始的方式去实例化userService了。如果没有Spring,那么当Action需要使用UserServiceImpl时,必须通过“UserService userService = new UserServiceImpl();”主动去创建实例对象,但使用了Spring之后,Action要使用UserServiceImpl时,就不用主动去创建UserServiceImpl的实例了,创建UserServiceImpl实例已经交给Spring来做了,Spring把创建好的UserServiceImpl实例给Action,Action拿到就可以直接用了。Action由原来的主动创建UserServiceImpl实例后就可以马上使用,变成了被动等待由Spring创建好UserServiceImpl实例之后再注入给Action,Action才能够使用。这说明Action对“UserServiceImpl”类的“控制权”已经被“反转”了,原来主动权在自己手上,自己要使用“UserServiceImpl”类的实例,自己主动去new一个出来马上就可以使用了,但现在自己不能主动去new“UserServiceImpl”类的实例,new“UserServiceImpl”类的实例的权力已经被Spring拿走了,只有Spring才能够new“UserServiceImpl”类的实例,而Action只能等Spring创建好“UserServiceImpl”类的实例后,再“恳求”Spring把创建好的“UserServiceImpl”类的实例给他,这样他才能够使用“UserServiceImpl”,这就是Spring核心思想“控制反转”,也叫“依赖注入”,“依赖注入”也很好理解,Action需要使用UserServiceImpl干活,那么就是对UserServiceImpl产生了依赖,Spring把Acion需要依赖的UserServiceImpl注入(也就是“给”)给Action,这就是所谓的“依赖注入”。对Action而言,Action依赖什么东西,就请求Spring注入给他,对Spring而言,Action需要什么,Spring就主动注入给他。
4、@ Repository
@Repository对应数据访问层Bean ,例如:
1 @Repository(value="userDao") 2 public class UserDaoImpl extends BaseDaoImpl<User> { 3 ……… 4 }
@Repository(value="userDao")注解是告诉Spring,让Spring创建一个名字叫“userDao”的UserDaoImpl实例。
当Service需要使用Spring创建的名字叫“userDao”的UserDaoImpl实例时,就可以使用@Resource(name = "userDao")注解告诉Spring,Spring把创建好的userDao注入给Service即可。
1 // 注入userDao,从数据库中根据用户Id取出指定用户时需要用到 2 @Resource(name = "userDao") 3 private BaseDao<User> userDao;
@Scope
标记在类和方法,标记上述Spring Bean的作用域:singleton、prototype、request、session、application、websocket
- singleton:单例,无论getBean多少次得到的都是同一个实例。默认值为此,如@Service、@Repository、@Component等的scope
- prototype:每次getBean都创建一个新的实例。从某方面来说,类似于new操作。
- request:每个http请求创建一个实例,该实例仅在该请求内有效且该请求过程中用同一个实例,不同request创建的实例互不干扰;实例随请求结束而销毁
- session:每当创建一个会话时创建一个实例,实例随着会话的结束而销毁
- application:在一个ServletContext lifecycle内公用一个实例。注:不是ApplicationContext lifecycle,一个ServletContext可能包含多个ApplicationContext
- websocket:在一个WebSocket lifecycle内公用一个实例
详情可参阅:https://docs.spring.io/spring/docs/current/spring-framework-reference/core.html#beans-factory-scopes
Java Singleton与Spring Singleton的区别:Java singleton class is per classloader and Spring’s singleton is per application context. 更多可参考https://javabeat.net/spring-singleton-java-singleton/
接收请求参数和页面传参
转自:http://blog.csdn.net/z69183787/article/details/41653875
2、接收请求参数
(1)使用HttpServletRequest获取,如request.getParameter("name")
(2)在方法声明参数,如@RequestParam("pass")String password,或@Param("pass")String password。表单参数也可以用这种方式获取,Spring会自动将表单参数注入到方法参数,和表单的name属性保持一致。
(3)对于URL中的参数,还可以使用Map来接收;对于非url中的参数,可以用Map、POJO接收
(4)自动注入Bean属性
3、向页面传参
(1)使用HttpServletRequest 和 Session 然后setAttribute(),就和Servlet中一样
(2)使用ModelAndView对象
(3)使用ModelMap对象
(4)使用@ModelAttribute注解
4、异常捕获处理
@ExceptionHandler:捕获特定异常进行处理,仅对当前Controller有效
@ControllerAdvice:修饰类,捕获全局各个Controller抛出的异常。ControllerAdvice注解只拦截Controller不会拦截Interceptor的异常。
示例:
@Controller @RequestMapping("/exception") @ControllerAdvice public class ExceptionController { @ExceptionHandler({ ArithmeticException.class }) // 单用@ExceptionHandler时只对当前controller有效即只捕获当前Controller抛出的异常,结合@ControllerAdvice可对全局有效。 // @ResponseStatus(value = HttpStatus.NOT_FOUND) @ResponseBody public String handleArithmeticException(Exception e) { e.printStackTrace(); return "some arighmetric error"; } @RequestMapping(value = "e/{id}", method = { RequestMethod.GET }) @ResponseBody public String testExceptionHandle(@PathVariable(value = "id") Integer id) { System.out.println(10 / id); return id.toString(); } } @Controller class TTT { @RequestMapping(value = "e/{id}", method = { RequestMethod.GET }) @ResponseBody public String testExceptionHandle(@PathVariable(value = "id") Integer id) { System.out.println(10 / id); return id.toString(); } }
该示例中,若没有@ControllerAdvice注解,则分别访问 http://localhost:8081/exception/e/0 、 http://localhost:8081/e/0 前者会被@ExceptionController指定的方法捕获处理、后者则不会被捕获。若加上@ControllerAdvice注解则访问两个路径都会被处理。
另外注意,类需要在能被spring扫描到的包下。
值得注意的是,此法只对Controller类中的handler抛出的异常有效,对其他的异常(如404、servlet的filter抛出的异常无效。后者例如SpringSecurity的failureHandler中抛出的异常)。关于SpringMVC中的异常处理原理及真正的全局异常拦截处理,可参阅 https://www.cnblogs.com/z-sm/p/12834647.html 中的异常处理部分。
5、request body传数组
public Integer deleteAdminAnStus(@RequestBody List<String> studentIdList, HttpServletRequest request) {} //前端请求时直接在body传类似如下格式的数据即可: ["a","b"]
6、页面重定向
假设当前在 /api/v1,则
response.sendRedirect("/test"); 将导向 /test
response.sendRedirect("test"); 将导向 /api/test
7、手动返回错误信息
response.sendError(400, "some error");
8、返回数据的统一包装
1 @ControllerAdvice // 拦截所有Controller的处理结果 2 public class ControllerResponseWrapper implements ResponseBodyAdvice<Object> { 3 private static final Logger logger = LoggerFactory.getLogger(ControllerResponseWrapper.class); 4 5 private final Set<MediaType> jsonMediaType = new HashSet<>( 6 Arrays.asList(MediaType.APPLICATION_JSON, MediaType.APPLICATION_JSON_UTF8)); 7 8 // 对于哪些请求要执行beforeBodyWrite,返回true执行,返回false不执行 9 @Override 10 public boolean supports(MethodParameter methodParameter, Class<? extends HttpMessageConverter<?>> converterType) { 11 return true; 12 } 13 14 @Override 15 public Object beforeBodyWrite(Object obj, MethodParameter returnType, MediaType mediaType, 16 Class<? extends HttpMessageConverter<?>> converterType, ServerHttpRequest request, 17 ServerHttpResponse response) { 18 19 // 不是需要验证的路径,不处理 20 // String reqURLPath = request.getURI().getPath(); 21 // if (!reqURLPath.startsWith(WebSecurityConfig.AUTH_PATH_PREFIX)) {//发送500时reqURL被重定向到/error了,所以此法不可行 22 // } 23 // 类型 不属于 需要处理的包头的时候,不处理 24 if (!jsonMediaType.contains(mediaType)) { 25 } 26 // 当类型 是属于需要处理的时候 并且 obj不是ReturnMsg的时候 进行格式化处理 27 else if (obj == null || !(obj instanceof ReturnMsg)) { 28 if (isExceptionOccur(obj)) {// 1.说明请求失败,返回了默认错误处理DefaultHandlerExceptionResolver的结果 29 Map<String, Object> res = (Map<String, Object>) obj; 30 int status = (int) res.get("status"); 31 String errorMsg = (String) res.get("error") + ": " + res.get("message"); 32 String path = (String) res.get("path"); 33 34 obj = ReturnMsg.fail(status, errorMsg, path, request.getMethodValue()); 35 36 } else {// 2.说明请求成功 37 ReturnMsg newRes = ReturnMsg.success(obj, request.getURI().getPath(), request.getMethodValue()); 38 if (obj instanceof String) {// 对于String类型默认会用org.springframework.http.converter.StringHttpMessageConverter处理,所以ReturnMsg类型obj会出错 39 try { 40 obj = new ObjectMapper().writeValueAsString(newRes); 41 } catch (JsonProcessingException e) { 42 // TODO Auto-generated catch block 43 e.printStackTrace(); 44 } 45 } else { 46 obj = newRes; 47 } 48 } 49 } 50 // 返回结果已经是包装格式 51 else { 52 } 53 54 logger.info("Req from {}: {}", request.getRemoteAddress(), obj); 55 return obj; 56 } 57 58 /** 根据返回结果判断是否是发ere生异常返回的,正常结果也可能是这个格式,所以检查得严格! */ 59 private static boolean isExceptionOccur(Object obj) { 60 /** 61 * 默认的Spring异常处理DefaultHandlerExceptionResolver返回格式为:<br> 62 * { <br> 63 * "timestamp":"2018-06-19T06:19:57.757+0000",<br> 64 * "status":400,<br> 65 * "error":"Bad Request",<br> 66 * "message":"Failed to convert value of type 'java.lang.String' to required type 'java.lang.Integer'",<br> 67 * "path":"/api/v1/student/recent_experiment"<br> 68 * } 69 */ 70 71 // 不是Map则肯定不是异常 72 if ((obj == null) || !(obj instanceof Map<?, ?>)) { 73 return false; 74 } 75 76 // 不符合异常的格式,则不是异常。由于正常返回数据也有可能这几个字段,所以检查要严格点 77 @SuppressWarnings("unchecked") 78 Map<String, Object> mapObj = (Map<String, Object>) obj; 79 if (mapObj.get("path") == null && mapObj.get("timestamp") == null || mapObj.get("status") == null) { 80 return false;// 发生异常时 error 和 message 有可能为null 81 } 82 // 发生异常时status肯定不是200 83 if ((Integer) (mapObj.get("status")) == 200) { 84 return false; 85 } 86 return true; 87 } 88 }
通过实现ResponseBodyAdvice接口的beforeBodyWrite方法来完成,在方法里包装Controller返回数据srcRes得到 自定义统一数据格式ApiResMsg的数据finalRes 后返回。(注:只能捕获到Controller类里方法返回的结果)
Controller方法返回的srcRes为String类型时是种特殊情况,需特殊处理:
beforeBodyWrite方法返回的数据将会被converterType所表示的MessageConverter处理后输出给前端,在SpringMVC中定义了9个MessageConverter来处理返回数据。其中:
org.springframework.http.converter.json.MappingJackson2HttpMessageConverter:可以处理任意类型数据:若是基本数据类型则原样输出、对于Map、Java Bean则会转成JSON输出(对于Map输出所有键值对、Java Bean输出所有getXxx方法的xxx即输出的参数名为xxx而不是field名故可以输出Bean里不存在的属性名)
class org.springframework.http.converter.StringHttpMessageConverter:只能处理String类型的数据,若输入非String类型显然会报错。
对于一个Controller中的方法来说,其返回值srcRes会被哪种MessageConverter处理取决于srcRes的类型:
对于@RestController(@Controller+@ResponseBody)下的方法来说,通常(除了String类型)其返回的content-type=application/json,beforeBodyWrite返回值finalRes将被converterType=MappingJackson2HttpMessageConverter处理
然而当srcRes为String类型时是特例,其content-type=text/plain, converterType=StringHttpMessageConverter(猜测不与上述一样的原因是SpringMVC中返回String类型的srcRes可以代表一个view?)
可见,srcRes是String时converterType为StringHttpMessageConverter,若直接在beforeBodyWrite里将其包装为ApiResMsg类型的finalRes,则会报错。解决:此时可以在beforeBodyWrite里进一步将finalRes转为String类型,然而仍存在问题,因为srcRes为null时无法知道srcRes的类型,终极解法:如示例代码所示,根据converterType确定是否将finalRes转为String类型。
相关见:https://my.oschina.net/u/1757225/blog/1543715
9、配置文件(yml或properties文件)中的配置参数注入到变量
有两种方法,都要求变量所在的类为Spring Bean。
法1:@Value,单个注入。通过@Value注入到变量,需要一个个指定,即每个变量都写上该注解来指定。有注入到实例变量、静态变量两种使用方式。
注入到实例变量
@Component public class GlobalValue{ @Value("${sensestudy.redis.host}") private String hostname }
注入静态变量
默认@Value是注入到实例域的,要注入到静态域,可以通过如下方法:
@Service class GlobalValue { public static String hostname; // 也可以是Bean,见下一个示例 @Value("${sensestudy.redis.host}") public void setMyHost(String h) { hostname = h; } } @Component public class I18nUtil { private static MessageSource messageSource; @Autowired public void setMessageSource(MessageSource msgSource){ messageSource = msgSource; } public static String getI18nMsg(String key){return messageSource.getMsg(key);} }
其本质其实就是“延迟为静态变量赋值”,所以基于该思想实际上有很多种实现方式,如上面的setter注入、还有@PostConstruct等initialization callback。
以上面的I18nUtil为例,此定义方式的:
优点:使用者使用时无需通过@Autowired定义变量(如@Autowired I18nUtil i18nUtil),而是可以直接以静态变量或静态方法的方式使用I18nUtil,如I18nUtil.getI18nMsg。
缺点:上述”优点“在如下场景下将成为缺点:若I18nUtil没有被Spring容器扫描注册到,则启动时不会报错,只有在运行时会因找不到messageSource而报NPE错;相反,如果I18nUtil方法不是静态的,则使用者使用时须通过@Autowired I18nUtil i18nUtil 及 i18nUtil.getI18nMsg来使用,此时若I18nUtil未被扫描到则启动时就会报错,显然这样更有利于尽早避免隐藏的错误。(PS 在本人实践过程中就趟过前者的坑)。
如何让I18nUtil不用被容器管理(不同@Component等修饰)且其内部静态变量能够自动注入呢?可借助 SmartInitializingSingleton 实现(参阅文章),示例:
@Component public class AutowireStaticSmartInitializingSingleton implements SmartInitializingSingleton { //法1 @Autowired private AutowireCapableBeanFactory beanFactory; /** * 当所有的单例Bena初始化完成后,对static静态成员进行赋值 */ @Override public void afterSingletonsInstantiated() { // 因为是给static静态属性赋值,因此这里new一个实例做注入是可行的 beanFactory.autowireBean(new I18nUtil()); } //法2 @Autowired private AutowiredAnnotationBeanPostProcessor autowiredAnnotationBeanPostProcessor; @Override public void afterSingletonsInstantiated() { autowiredAnnotationBeanPostProcessor.processInjection(new I18nUtil()); } }
注入其他类型的数据:
数组:
代码: @Value("${sensestudy.userInfo.includeUrls}") private String[] includeUrls;
配置:
sensestudy: skipUrls: /a, /b/c ,/c
逗号跟在值后面或前面都无关紧要、key所在行可以没有值而是所有值都单独在一行、也可所有值在同一行。
List或Map:(未验证)
代码:
list: topic1,topic2,topic3
maps: "{key1: 'value1', key2: 'value2'}"
配置:
@Value("#{'${list}'.split(',')}") // $为从配置文件读取值、#支持EL表达式 private List<String> list;
@Value("#{'${sensestudy.creator.project.no-version-types:SCRATCH,SCRATCH_TEACHING,DATA_LABEL}'.split(',')}")
private List<String> noVersionProjectTypes
@Value("#{${maps}}") private Map<String,String> maps;
@Value("#{systemProperties['pop3.port'] ?: 25}") //配置值存在则用配置值,否则用25
private String port;
另一种List接收参数的示例:
代码: @ConfigurationProperties(prefix = "ss") @Data public class AppListPros { private List<AppInfo> appList; @Data public static class AppInfo{ private String id; private String name; private String nameEn; private String url; private List<String> roles; private List<String> apps; } } 配置: ss: appList: - id: sense name: AI nameEn: Inno url: /lab/projects/list roles : - ROLE_ADMINSUPER - ROLE_ADMINORGANIZE - ROLE_ADMINSCHOOL
配默认值:默认情况下若@Value中指定的key在配置文件中不存在,则启动会报错并失败。可通过如下方式配置默认值,配置文件中若无sensestudy.redis.host配置项则会将指定的默认值注入。
@Value("${sensestudy.redis.host:}") :默认值为空串
@Value("${sensestudy.redis.host:local}") :默认值为local
参考资料:https://blog.csdn.net/J080624/article/details/80269616
法2:@ConfigurationProperties,批量注入。配置文件中的值批量注入到类中。
示例及说明:
@Validated @Data @Component @ConfigurationProperties(prefix = "sensestudy.security.jwt") public class JwtSettings { @NotBlank private String tokenIssuer; @NotBlank private String tokenSigningKey; @NotNull private Integer tokenExpirationTimeMinutes; @NotNull private Integer refreshTokenExpireTimeMinutes; /** 将token放入cookie的相关设置 */ @Valid private CookieLize cookieLize = new CookieLize(); /** * cookie设置,所具有的参数见 {@link javax.servlet.http.Cookie} <br> */ @Data public static class CookieLize { @NotNull private Boolean enabled = false;// 这里设置了默认值,若配置文件中未配置则默认采用此值 @NotBlank private String domain = "*"; @NotBlank private String path = "/"; @NotNull private Boolean secure = false; @NotNull private Boolean httpOnly = false; } }
1. 默认情况下,某变量在配置文件中找不到对应属性,不会报错。这点与@Value的不同。为了尽早发现配置错误,可通过validation来实现“未找到则报错”的效果。
2. 配置文件中属性名通常有多个层级,为了将类变量与之对应,可通过在类内部定义并实例化一个其他类对象。如上面示例中的cookieLize变量。
此注入方式下,JwtSettings成为了个Spring Bean,使用者通常通过Autowired该Bean来使用配置值。对于内嵌的对象cookieLize只能通过最外层一路调用get方法到里层获取,可将里层类CookieLize也声明为Bean来避免;另一方面,默认情况下会把属性值注入到同层级的同名变量中,这正是所期望的。不过,也仍可在变量上使用@Value注入,从而实现将属性值注入到不同名变量的效果,此时要求@Value对应的变量所在类应是个Spring Bean。在CookieLize为Spring Bean的情况下,cookieLize就不能再new了,而是得Autowired,否则cookieLize对象各属性会为null。修改后结果如下(推荐用此种方式):
@Validated @Data @Component @ConfigurationProperties(prefix = "sensestudy.security.jwt") public class JwtSettings { @NotBlank private String tokenIssuer; @NotBlank private String tokenSigningKey; @NotNull private Integer tokenExpirationTimeMinutes; @NotNull private Integer refreshTokenExpireTimeMinutes; /** 将token放入cookie的相关设置 */ @Autowired @Valid private CookieLize cookieLize; /** * cookie设置,所具有的参数见 {@link javax.servlet.http.Cookie} <br> */ @Component @Data public static class CookieLize { @NotNull private Boolean enabled = false;// 这里设置了默认值,若配置文件中未配置则默认采用此值 @NotBlank private String domain = "*"; @NotBlank private String path = "/"; @Value("${my.isSecure}") // 指定对应到my.isSecure属性而非默认的sensestudy.security.jwt.secure @NotNull private Boolean secure = false; @NotNull private Boolean httpOnly = false; } }
10、上传文件并附带额外参数
其实就是借助form表单来实现。
后端代码:
@PostMapping(value = "/api/v1/admin/students") @PreAuthorize(("hasRole('ROLE_ADMIN')")) public List<String> addAdminStudents(@RequestParam UserEntity userEntity, @RequestParam("stuFile") List<MultipartFile> files, @RequestParam("isOverride") boolean isOverride, HttpServletRequest request) throws Exception { // List<MultipartFile> files = ((MultipartHttpServletRequest) request).getFiles("stuFile"); String adminId = controllerUtils.getUserIdFromJwt(request); List<StudentBeanForAddAccount> stuBeanList = resolveStudentAccountFile(files); return studentService.addStudentAccounts(adminId, stuBeanList, isOverride); }
前端逻辑:构造个form表单放所需参数(包括stuFile、isOverride)提交即可;若有很多个非文件域参数,则后端可以声明个@RequestParam UserEntity userEntity来接收这些非文件域参数(实践发现需要去掉@RequestParameter才生效)。
进阶:有时候额外参数可能是个JSON,怎么办?
可以将JSON转成String然后作为form表单的一个字段;
或者将JSON里的各字段拿出来分别作为form表单的一个字段传输,但若JSON有多层则此法行不通了。
另外须注意:form表单只能用POST方法
从上面可发现,@RequestParam指定的参数并不一定对应的是请求URL中的参数,也可以是请求体中的参数(如form-data请求中的字段、json请求中的字段,这些是在POST方法的body里的)。
- In Spring MVC, "request parameters" map to query parameters, form data, and parts in multipart requests. This is because the Servlet API combines query parameters and form data into a single map called "parameters", and that includes automatic parsing of the request body.
11、@Autoweird、@Resource、@Inject都可以用于依赖注入,其区别:
https://www.sourceallies.com/2011/08/spring-injection-with-resource-and-autowired/#more-2350
‘@Autowired’ and ‘@Inject’ annotation behave identically. Both of these annotations use the ‘AutowiredAnnotationBeanPostProcessor’ to inject dependencies. ‘@Autowired’ and ‘@Inject’ can be used interchangeable to inject Spring beans. However the ‘@Resource’ annotation uses the ‘CommonAnnotationBeanPostProcessor’ to inject dependencies. Even though they use different post processor classes they all behave nearly identically.
12、事务@Transational
Java Throwable分为Error和Exception,Exception分为Unchecked Exception(包括RuntimeException类及其子类)和Checked Exception(包括Exception类自身)。
Spring的AOP即声明式事务管理@Transactional默认是遇到运行异常(RuntimeException)和程序错误(Error)才会回滚,对checked Exception即Exception可try{}捕获的不会回滚。 因此若在API中实现自定义异常则最好继承RuntimeException以在出错时能进行回滚。若想针对Checked Exception进行事务回滚,可在@Transactional 注解里使用 rollbackFor 属性明确指定异常,如: @Transactional(rollbackFor = Exception.class)
@Transactional 可以作用于接口、接口方法、类以及类方法上。当作用于类上时,该类的所有 public 方法将都具有该类型的事务属性,同时,我们也可以在方法级别使用该标注来覆盖类级别的定义。
虽然 @Transactional 注解可以作用于接口、接口方法、类以及类方法上,但是 Spring 建议不要在接口或者接口方法上使用该注解,因为只有在使用基于接口的代理时它才会生效。
@Transactional 注解应该只被应用到 public 方法上,这是由 Spring AOP 的本质决定的。如果你在 protected、private 或者默认可见性的方法上使用 @Transactional 注解,这将被忽略,也不会抛出任何异常。
默认情况下,只有来自外部的方法调用才会被AOP代理捕获,也就是,类内部方法调用本类内部的其他方法并不会引起事务行为,即使被调用方法使用@Transactional注解进行修饰。
可参考:https://www.jianshu.com/p/380a9d980ca5
13、代码中添加静态资源
在 Spring MVC 中,资源的查找、处理使用的是责任链设计模式(Filter Chain):
其思路为如果当前 resolver 找不到资源,则转交给下一个 resolver 处理。 当前 resolver 找到资源则立即返回给上级 resovler(如果存在),此时上级 resolver 又可以选择对资源进一步处理或再次返回给它的上级(如果存在)。
配置方法为重写 WebMvcConfigurerAdapter 类的 addResourceHandlers:
@Configuration @EnableWebMvc public class WebConfig extends WebMvcConfigurerAdapter { @Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler("/webjars/**") .addResourceLocations( "classpath:/META-INF/resources/webjars/"); }
这段代码实际上是添加了一个 PathResourceResolver来完成对资源的查找,该 resolver 的作用是将 url 为 /webjars/** 的请求映射到 classpath:/META-INF/resources/webjars/。
比如请求 http://localhost:8080/webjars/jquery/3.1.0/jquery.js 时, Spring MVC 会查找路径为 classpath:/META-INF/resources/webjars/jquery/3.1.0/jquery.js 的资源文件。
使用了 @EnableWebMvc 注解后 WebMvcAutoConfiguration 提供的默认配置会失效,必须提供全部配置。想要使用默认配置,无需使用 @EnaleWebMvc 注解。
14、过滤器(Filter)与拦截器(Interceptor)
区别:
Filter 接口定义在 javax.servlet 包中;HandlerInterceptor接口定义在org.springframework.web.servlet 包中
Filter在 Servlet 规范中定义,依赖于Servlet容器,被Servlet容器(如Tomcat)调用;Interceptor不依赖于Servlet容器,是Spring框架的一个组件,归Spring IoC容器管理调用。因此可以通过注入等方式来获取其他Bean的实例,使用更方便。
Filter是基于函数回调的,而Interceptor则是基于动态代理(Java反射)的。
Filter只能在请求的前后使用,而Interceptor可以详细到每个方法(即handler)
Filter对几乎所有的请求起作用(包括静态资源、程序中定义的Servlet等)而Interceptor只能对handler请求起作用。Filter方法:(init、doFilter、destroy),Interceptor方法:(preHandle、postHandle、afterCompletion)
Filter不能访问handler上下文、值栈里的对象(因还没到达handler),而Interceptor可以(得益于IoC容器,虽然还没到达handler,但程序启动后就扫描好了handler)。
注:SpringBoot(SpringMVC)里的Handler特指@Controller注解的类里每个处理HTTP请求的public method
执行顺序:过滤前-拦截前-handler执行-拦截后-过滤后
示例:
//这里通过如下两个注解使过滤器生效。也可以不用注解,通过FilterRegistrationBean添加过滤器 @Component @WebFilter(urlPatterns = "/Blogs", filterName = "blosTest") class TestFilter implements Filter { @Override public void init(FilterConfig filterConfig) throws ServletException { System.err.println("filter .. init"); } @Override public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest) servletRequest; HttpServletResponse response = (HttpServletResponse) servletResponse; System.out.println("filter 请求前"); filterChain.doFilter(request, response); System.out.println("filter 请求后"); } @Override public void destroy() { System.err.println("filter .. destroy"); } }
1 import javax.servlet.http.HttpServletRequest; 2 import javax.servlet.http.HttpServletResponse; 3 4 import org.springframework.context.annotation.Configuration; 5 import org.springframework.web.servlet.HandlerInterceptor; 6 import org.springframework.web.servlet.ModelAndView; 7 import org.springframework.web.servlet.config.annotation.InterceptorRegistry; 8 import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; 9 10 @Configuration 11 public class LoginInterceptorConfigurer implements WebMvcConfigurer { 12 @Override 13 public void addInterceptors(InterceptorRegistry registry) { 14 // 多个拦截器组成一个拦截器链 15 // addPathPatterns 用于添加拦截规则 16 // excludePathPatterns 用户排除拦截 17 registry.addInterceptor(new MyInterceptor1()).addPathPatterns("/**"); 18 registry.addInterceptor(new MyInterceptor2()).addPathPatterns("/**"); 19 } 20 } 21 22 class MyInterceptor1 implements HandlerInterceptor { 23 24 @Override 25 public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) 26 throws Exception { 27 System.out.println(">>>MyInterceptor1>>>>>>>在请求处理之前进行调用(Controller方法调用之前)"); 28 29 return true;// 只有返回true才会继续向下执行,返回false取消当前请求 30 } 31 32 @Override 33 public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, 34 ModelAndView modelAndView) throws Exception { 35 System.out.println(">>>MyInterceptor1>>>>>>>请求处理之后进行调用,但是在视图被渲染之前(Controller方法调用之后)"); 36 } 37 38 @Override 39 public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) 40 throws Exception { 41 System.out.println(">>>MyInterceptor1>>>>>>>在整个请求结束之后被调用,也就是在DispatcherServlet 渲染了对应的视图之后执行(主要是用于进行资源清理工作)"); 42 } 43 44 } 45 46 class MyInterceptor2 implements HandlerInterceptor { 47 48 @Override 49 public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) 50 throws Exception { 51 System.out.println(">>>MyInterceptor2>>>>>>>在请求处理之前进行调用(Controller方法调用之前)"); 52 53 return true;// 只有返回true才会继续向下执行,返回false取消当前请求 54 } 55 56 @Override 57 public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, 58 ModelAndView modelAndView) throws Exception { 59 System.out.println(">>>MyInterceptor2>>>>>>>请求处理之后进行调用,但是在视图被渲染之前(Controller方法调用之后)"); 60 } 61 62 @Override 63 public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) 64 throws Exception { 65 System.out.println(">>>MyInterceptor2>>>>>>>在整个请求结束之后被调用,也就是在DispatcherServlet 渲染了对应的视图之后执行(主要是用于进行资源清理工作)"); 66 } 67 68 } 69 70 71 72 //输出如下 73 74 >>>MyInterceptor1>>>>>>>在请求处理之前进行调用(Controller方法调用之前) 75 >>>MyInterceptor2>>>>>>>在请求处理之前进行调用(Controller方法调用之前) 76 2018-12-07 17:43:48.043 [http-nio-8080-exec-1] INFO c.s.s.r.ControllerResponseWrapper - Req from /0:0:0:0:0:0:0:1:34712: { errorCode:1000, errorMsg:ok, path:/api/v1/admin_super/courses, method:GET, timestamp:2018-12-07 17:43:48 } 77 >>>MyInterceptor2>>>>>>>请求处理之后进行调用,但是在视图被渲染之前(Controller方法调用之后) 78 >>>MyInterceptor1>>>>>>>请求处理之后进行调用,但是在视图被渲染之前(Controller方法调用之后) 79 >>>MyInterceptor2>>>>>>>在整个请求结束之后被调用,也就是在DispatcherServlet 渲染了对应的视图之后执行(主要是用于进行资源清理工作) 80 >>>MyInterceptor1>>>>>>>在整个请求结束之后被调用,也就是在DispatcherServlet 渲染了对应的视图之后执行(主要是用于进行资源清理工作)
原理:
过滤器原理:Servlet容器维护过滤器链,依次将过滤器应用于request。
Servlet程序:用户编写的代码,定义了 接受的请求url、接受的请求方法、接收到请求时要执行的业务逻辑等内容。也可以是图片、index.html等文件。
Servlet容器:运行Servlet程序的容器,是Servlet标准的实现者,如Tomcat、Undertow等,是Web容器的一种。会接收外界的http request并转发给Servlet程序。
Filter:由用户定义的内容。Servlet容器和Servlet程序间通信的关卡,会对前者发往后者的request及后者返回给前者的response进行拦截。
Filter中定义了对哪些request进行拦截以及拦截的逻辑,以进行 决定是否继续将请求发往Servlet程序、修改request、修改response 等操作。
多对多关系:一个Filter可作用于多个Servlet程序、也可多个Filter作用于同一Servlet程序。
FilterChain:由Servlet容器维护的内容,作用于同一Servlet程序的多个Filter会在request到来时被Servlet容器组装成一个FilterChain。调用逻辑:由Servlet容器将链中的首个Filter应用到该request,还会把FilterChain对象传给该Filter,之后由当前Filter通过FilterChain对象通知下一Filter处理处理该request,后续同理。
拦截器的原理:Spring IoC容器维护handler的拦截器列表,依次将拦截器应用于handler。
传统的Java Web应用中后端可以有很多个Servlet程序,每个Servlet程序中有若干个handler;而SpringMVC中只有一个Servlet程序——DispatcherServlet(当然用户也可以自己添加Servlet程序),由它将所有的请求根据请求path转发到相应的handler,项目启动时IoC容器会扫描所有的handler及应用到该handler的interceptor,因此DispatcherServlet会在转发请求到handler的前后执行interceptor的preHandl、postHandle、afterCompletion方法。详情可参阅 SpringMVC工作原理-marchon
通过以上对比可发现:
过滤器是 Filter调用FilterChain、FilterChain调用下一Filter 的执行模式,这本质上是函数回调,要命的是函数调用栈会越来越深。
拦截器中,IoC容器在程序启动后就扫描、维护好了各hanlder及其拦截器列表,因此通过遍历一个handler的拦截器列表来执行拦截器,不会有Filter调用层次深的问题。
不过,当一个handler上有多个拦截器时,多个拦截器的执行顺序与多个filter的类似,即 h1 -> h2 -> handler -> h2 -> h1 。
参考资料:
https://segmentfault.com/a/1190000012072060
http://einverne.github.io/post/2017/08/spring-interceptor-vs-filter.html
过滤器触发两次
将Filter定义成一个Bean并通手动注册到SpringSecurity时( .addFilterBefore(myFilter, UsernamePasswordAuthenticationFilter.class) //这里 myFilter通过 @Autowired MyFilter myFilter; 定义 ),此时如果调用一个handler一次则会触发两次该Filter。
原因:SpringBoot对于任何一个bean都会自动注册,加上我们手动注册的,这样该Filter就被注册了两次。解决:
法1:不让Filter成为一个Bean:若该Filter里面没有依赖需要自动注入的Bean,则可以不将该Filter定义为Bean,这样往SpringSecurity注册的filter 通过new Filter()产生而不是Autowired,从而不会注册两次。
法2:禁止SpringBoot自动注册该Bean,可以在该Filter里加上如下代码:
@Bean public FilterRegistrationBean registration(MyFilter filter) {// 本filter将手动注册到SpringSecurity。但SpringBoot会自动注册任何一个bean组件,这样导致注册两次从而每次调用都会触发两次,故通过此来让SpringBoot别自动注册此filter FilterRegistrationBean registration = new FilterRegistrationBean(filter); registration.setEnabled(false); return registration; }
法3:继承OncePerRequestFilter来实现Filter,这样虽然注册了两次,但可以保证只执行一次。implements Filter 和 extends OncePerRequestFilter两种写法的示例分别如下:
1 @Component 2 @WebFilter(urlPatterns = "/Blogs", filterName = "blosTest") 3 class TestFilter1 extends OncePerRequestFilter { 4 5 @Autowired 6 CustomerCourseService customerCourseService; 7 8 @Override 9 protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, 10 FilterChain filterChain) throws ServletException, IOException { 11 // you business 12 // customerCourseService.getByCustomerId(""); 13 } 14 15 @Override 16 public void destroy() { 17 System.err.println("filter .. destroy"); 18 } 19 } 20 21 @Component 22 @WebFilter(urlPatterns = "/Blogs", filterName = "blosTest") 23 class TestFilter2 implements Filter { 24 25 @Autowired 26 CustomerCourseService customerCourseService; 27 28 @Override 29 public void init(FilterConfig filterConfig) throws ServletException { 30 31 } 32 33 @Override 34 public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) 35 throws IOException, ServletException { 36 // you business 37 // customerCourseService.getByCustomerId(""); 38 39 } 40 41 @Override 42 public void destroy() { 43 44 } 45 }
Spring处理HTTP请求的过程:
SpringBoot基础
配置文件语法
配置文件可以用yml或properties, 若是yml,则键与值间的冒号后需要有空格!,如 :
sensestudy:
redis:
host: 172.20.6.88
port: 6379
配置文件优先级
代码中的初始配置 < 配置文件中的配置 < 操作系统环境变量 < 命令行启动参数中的配置
若一个模块依赖另一个模块,则被依赖module里配置文件中的配置项 < 当前项目配置文件中的配置项
Spring Security
hasRole、hasAnyRole
log配置
SpringBoot默认采用logback日志框架,可以配置采用其他框架。
可以直接在application.yml配置(日志级别、输出文件等),如:
logging:
level:
root: info
file: ./logs/log.log #输出位置
#config: classpath:logback-spring.xml
但是此对于定期输出一个文件等较复杂的配置无能为力,可以借助日志配置文件:
如上述配置中指定了配置文件,在项目resources文件夹下。也可以不指定,只要配置文件名按默认风格命名即可。配置示例:
<?xml version="1.0" encoding="UTF-8"?> <configuration debug="true" scan="false" scanPeriod="30 seconds"> <contextName>sensestudy</contextName> <!-- 文件输出路径 --> <springProperty scope="context" name="LOG_FILE_PATH" source="logging.path" /> <!-- <property name="LOG_FILE_PATH" value="/sensestudy_logs/javaserver_logs" /> --> <!-- 日志级别 --> <springProperty scope="context" name="LOG_LEVEL" source="logging.level.root" /> <!-- <property name="LOG_LEVEL" value="info" /> --> <!-- 输出格式 --> <!-- 格式化输出:%date表示日期,%thread表示线程名,%-5level:级别从左显示5个字符宽度 %msg:日志消息,%n是换行符 --> <property name="LOG_PATTERN" value="%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n" /> <appender name="CONSOLE_APPENDER" class="ch.qos.logback.core.ConsoleAppender"> <encoder> <Pattern>${LOG_PATTERN}</Pattern> </encoder> <filter class="ch.qos.logback.classic.filter.ThresholdFilter"> <level>TRACE</level> </filter> </appender> <appender name="FILE_APPENDER" class="ch.qos.logback.core.rolling.RollingFileAppender"> <!-- <filter class="ch.qos.logback.classic.filter.LevelFilter"> <level>ERROR</level> <OnMismatch>DENY</OnMismatch> <OnMatch>ACCEPT</OnMatch> </filter> --> <file>${LOG_FILE_PATH}/sensestudyserver.log</file> <encoder> <pattern>${LOG_PATTERN}</pattern> </encoder> <append>true</append> <!-- <filter class="ch.qos.logback.classic.filter.LevelFilter"> <level>warn</level> <onMatch>ACCEPT</onMatch> <onMismatch>DENY</onMismatch> </filter> --> <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy"> <fileNamePattern>${LOG_FILE_PATH}/sensestudyserver.%d{yyyy-MM-dd}.%i.log.zip </fileNamePattern> <!-- keep 30 days' files --> <!-- <maxHistory>30</maxHistory> --> <timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP"> <maxFileSize>20MB</maxFileSize> </timeBasedFileNamingAndTriggeringPolicy> <!-- 超出删除老文件 --> <totalSizeCap>20GB</totalSizeCap> </rollingPolicy> </appender> <!-- root是默认的logger,输出到console --> <root level="${LOG_LEVEL}"> <appender-ref ref="CONSOLE_APPENDER" /> </root> <!-- 另外专门定义一个logger用来输出埋点信息到特定文件 --> <logger name="fileLogger" additivity="false" level="${LOG_LEVEL}"> <appender-ref ref="FILE_APPENDER" /> <appender-ref ref="CONSOLE_APPENDER" /><!-- 同时也输出到console --> </logger> </configuration>
配置文件中的配置优先级优于application.yml,在配置文件指定日志输出位置后,application.yml中的file(输出位置)就失效了。
更多可参考:SpringBoot logback日志配置
SpringBoot 静态资源
Spring Boot 默认“约定”从资源目录的这些子目录读取静态资源:
- src/main/resources/META-INF/resources
- src/main/resources/static (推荐)
- src/main/resources/public
自定义handler参数转换器
如将 int 请求参数转为对应的枚举值:
// add request parameter converters to current controller's handler @InitBinder public void initBinder(WebDataBinder dataBinder) { // DevelopStateEnum converter dataBinder.registerCustomEditor(DevelopStateEnum.class, new PropertyEditorSupport() { @Override public void setAsText(String text) throws IllegalArgumentException { Integer devState = Integer.parseInt(text); setValue(DevelopStateEnum.myValueOf(devState)); } }); }
上述方法位于一个Controller内,只对当前Controller有效,若要对全局有效,可以借助@ControllerAdvice放在@ControllerAdvice所在的类中。
此外,进行该配置GET或POST中的请求参数均生效。
参考资料:https://stackoverflow.com/questions/4617099/spring-3-0-mvc-binding-enums-case-sensitive
Post方法接受复杂请求体参数
对于一个以上的请求参数(甚至包含嵌套参数,如Course下包含Experiment列表),可直接用java.util.Map接收,然后一层层get,但是此法显然不好,需要自己解析、不直观且易出错。
更好的方法:定义一个Bean,需要注意的是对于数组需要用[] 而不是List ,后者会报错 用List也可。可以的一个示例及对应的Controller方法:
@PreAuthorize("hasRole('DEVELOPER')") @PutMapping("/developer/course/catalog/translation") public void updateCatalogTranslation(@RequestBody CourseCatalogVO reqMap, HttpServletRequest request) {} @ToString(callSuper = true) @Data public static class CourseCatalogVO { private String courseId; private String isUseCatalog; private ChapterVO[] expList; @Data public static class ChapterVO { private String chapter; private ExperimentBriefInfo[] experimentBriefInfoList; } }
获取request对象
(方法之一)
1 public final HttpServletRequest getCurrentRequestInstance() { 2 RequestAttributes requestAttributes = org.springframework.web.context.request.RequestContextHolder.getRequestAttributes(); 3 if (requestAttributes == null) 4 return null; 5 HttpServletRequest request = (HttpServletRequest) requestAttributes 6 .resolveReference(RequestAttributes.REFERENCE_REQUEST); 7 return request; 8 }
/** * get {@link javax.servlet.http.HttpServletRequest } instance.<br> */ public HttpServletRequest getHttpServletRequest() { ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); return null == attributes ? null : attributes.getRequest(); } /** * get {@link javax.servlet.http.HttpServletResponse } instance.<br> */ public HttpServletResponse getHttpServletResponse() { ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); return null == attributes ? null : attributes.getResponse(); }
自动扫描
默认扫描规则:
@SpringBootApplication注解修饰main方法后会自动扫描main方法所在的包及其子包下的所有Bean并注册到Spring容器(因为该注解被@ComponentScan修饰了)。因此通常将main方法放在项目的公共父包下。注:只会自动扫描本项目自身的包,从外部引入的包默认不会被扫描,即使引入的包也是main方法的子包。
指定扫描规则:
当项目中引入模块A且模块A的包路径与当前项目不一样时,@SpringBootApplication并不能自动扫描到A中的Bean。此时若A与当前项目有公共父包则将当前项目被@SpringBootApplication修饰的main方法移动到公共父包下即可解决问题;然而最坏的情况是没有公共父包,此时可以通过如下注解指定扫描路径:
@ComponentScan:通过@ComponentScan的basePackages等指定A的包路径值(最父级的或最详细的均可),一旦详细指定了路径,则当前项目的包路径也得指定否则会找不到当前项目下的Bean。该注解自动扫描的对象包括被@Component、@Controller、@RestController、@Service、@Repository等修饰的类。但是不会扫描Spring JPA Entity、JPA Repository,可通过如下两个注解分别指定对应扫描路径(参阅:https://stackoverflow.com/questions/48935864/spring-boot-scan-whole-packages-without-entityscan-enablejparepositories)。
@EntityScan:扫描被@Entity修饰的类。
@EnableJpaRepositories:扫描被@Repository修饰的类。(使用JPA时为何@ComponentScan扫描不到@Repository?)
注:SpringBoot默认扫描只会扫描本项目自身的包(包括@Component、@Entity、@Repository等修饰的),从外部引入的包即使也是main方法的子包也不会被自动扫描。
handler定义的继承
微服务中Restful服务提供者的一种典型实现:接口中定义handler(api模块)、实现类implements接口以实现具体的handler逻辑(service模块)。示例:
1 //CourseApi.java 2 @RequestMapping("/api/v1") 3 public interface CourseApi { 4 5 // course 6 @GetMapping("/courses") 7 public BaseResp<PageResultVO<List<CourseVO>>> listCourses(@RequestParam("devState") CourseDevelopStateEnum devState, 8 @RequestParam("page") Integer page, @RequestParam("size") Integer size); 9 10 @GetMapping("/course") 11 public BaseResp<CourseVO> getCourse(@RequestParam("courseId") String courseId); 12 13 // experiment and step 14 @GetMapping("/course/experiments") 15 public BaseResp<List<ExperimentWithStepsVO>> listExperiments(@RequestParam("courseId") String courseId); 16 17 @GetMapping("/course/experiment") 18 public BaseResp<ExperimentWithStepsVO> getExperiment(@RequestParam("experimentId") String experimentId); 19 20 // catalog 21 @GetMapping("/course/catalog") 22 public BaseResp<CourseCatalogVO> getCatalog(@RequestParam("courseId") String courseId); 23 24 } 25 26 27 28 29 30 //CourseController.java 31 32 @RestController 33 public class CourseController implements CourseApi { 34 @Autowired 35 private CourseService courseService; 36 37 @Override 38 public BaseResp<PageResultVO<List<CourseVO>>> listCourses(CourseDevelopStateEnum devState, Integer page, 39 Integer size) { 40 PageResultVO<List<CourseVO>> res = courseService.listCourses(page, size, null, Arrays.asList(devState)); 41 42 return BaseResp.generateSuccess(res); 43 } 44 45 @Override 46 public BaseResp<CourseVO> getCourse(String courseId) { 47 List<CourseVO> res = courseService.listCourses(0, Integer.MAX_VALUE, Arrays.asList(courseId), null) 48 .getContent(); 49 50 return BaseResp.generateSuccess(CollectionUtils.isEmpty(res) ? null : res.get(0)); 51 } 52 53 @Override 54 public BaseResp<List<ExperimentWithStepsVO>> listExperiments(String courseId) { 55 List<ExperimentWithStepsVO> res = courseService.listExperiments(courseId, null); 56 57 return BaseResp.generateSuccess(CollectionUtils.isEmpty(res) ? Collections.emptyList() : res); 58 59 } 60 61 @Override 62 public BaseResp<ExperimentWithStepsVO> getExperiment(String experimentId) { 63 List<ExperimentWithStepsVO> res = courseService.listExperiments(null, experimentId); 64 65 return BaseResp.generateSuccess(CollectionUtils.isEmpty(res) ? null : res.get(0)); 66 } 67 68 @Override 69 public BaseResp<CourseCatalogVO> getCatalog(String courseId) { 70 CourseCatalogVO res = courseService.getCatalog(courseId); 71 72 return BaseResp.generateSuccess(res); 73 } 74 75 }
理想很丰满,现实很骨感。在Springboot5.1RC之前,接口中定义@RequestParam并不会生效,此时为了生效不得不在实现类中也重复加@RequestParam声明。更优雅的解决方法:升级SpringBoot版本。相关可参阅:https://stackoverflow.com/questions/8002514/spring-mvc-annotated-controller-interface-with-pathvariable
SpringBoot配置的优先级
bootstrap.properties:位于jar包外的优先级最高
application.properties:配置中心的文件 > 命令行配置 > 本地active指定文件 > 本地default文件
Handler扫描与识别
哪些hanlder会生效:被@Component(@Service、@Controller等)修饰且可实例化的类中定义的handler
SpringBoot/MVC项目启动时会自动扫描@Controller类中被@RequestMapping(及@GetMapping等)修饰的方法(即所谓的handler),维护url与方法的对应关系并注册到Spring容器中,以后收到前端请求时根据对应信息找到请求地址对应的处理method。
相关的代码为HandlerMapping接口及AbstractHandlerMapping、AbstractHandlerMethodMapping等实现类。
例外:抽象类和接口中的则不会被扫描,即使被@Controller、@RequestMapping等修饰,因为抽象类或接口无法创建Bean实例,也就无法由请求路径找到对应的处理方法。
踩坑记:
问题:springcloud netflix openfeign中,通过@FeignClient定义了的client,其他项目引入该client后,启动时会被FeignClientFactory创建一个FeignClient Bean,若该client或其所继承的api中被@RequestMapping修饰了,则其中的handler也会被注册到spring mvc容器中(见Feign Issue 466),从而导致可能与本项目中已有的接口定义冲突,即ambiguous definition。
解决:引用者需要进行配置以不让Spring容器将其当做handler,如下:
@Configuration @ConditionalOnClass({ Feign.class }) public class FeignConfiguration {// 解决@FeignClient中的@RequestMapping也被当前项目识别成Handler的问题 @Bean public WebMvcRegistrations feignWebRegistrations() { return new WebMvcRegistrations() { @Override public RequestMappingHandlerMapping getRequestMappingHandlerMapping() { return new FeignRequestMappingHandlerMapping(); } }; } private static class FeignRequestMappingHandlerMapping extends RequestMappingHandlerMapping { @Override protected boolean isHandler(Class<?> beanType) { return super.isHandler(beanType) && !AnnotatedElementUtils.hasAnnotation(beanType, FeignClient.class); } } }
从feign 3.0.5版本起,openfeign在创建FeignClient实例时会检查是否被@RequestMapping修饰,若有则直接抛错使得启动失败,也就是说从代码要求上禁止了该现象的发生。
handler定义(@GetMapping等)在父子类间的继承:(这里假设父类中有若干hanlder、假设子类被@Controller修饰)
若父类为抽象类或接口,子类可以正常继承父类的handler声明(不管父类有没有被@Controller修饰);此时这些handler在子类中都生效了;
若父类非抽象类或接口,则父类不能被@Controller修饰,否则会报ambiguous mapping,即一个路径映射到多个方法。
故:若需要handler继承,则要么父类为抽象类或接口,要么父类不是Controller(不被@Controller修饰)。
另外,若父类被@Controller修饰而子类没有,则显然子类的handler也不会生效,因为子类没被@Controller修饰。说一大堆,总结就是:只要同一个handler没在多个地方生效就ok,否则会出错。
Handler的参数约束(@RequestParameter等)的继承:
handler方法中的@RequestParameter、@RequestBody等约束是否会被子类继承?当handler定义在接口中时会、定义在类中时则不会。对于后者只能在子类中重复声明参数约束,显然这不算好的做法,故最好将handler定义在接口中。
@EnableXXX的实现
SpringBoot项目中为了让组件生效,可以通过在主类上加@EnablkeXXX注解来达到目的,如@EnableFeignClient,这非常方便。如何实现自定义实现类似功能?可以自定义@EnableXXX注解并在注解上通过@Import加载配置文件即可。示例:
@Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @Documented @Import(AuthInterceptor.class)//或 AuthConfigRegistor.class。在Import的参数指定的类中完成具体的启用逻辑,如加载配置文件。该类为普通类而非Bean public @interface EnableHeaderAuth { } //public class AuthConfigRegistor implements ImportBeanDefinitionRegistrar { // // @Override // public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) { // registry.registerBeanDefinition("myAuthConfig", // BeanDefinitionBuilder.genericBeanDefinition(AuthInterceptor.class).getRawBeanDefinition()); // // } // //}
HTTP响应码整型常量
有多个库中都有http响应码常量,示例:
org.springframework.http.BAD_REQUEST
javax.servlet.http.SC_BAD_REQUEST
根据文件名获取contentType(借助HttpServletRequest的方法,不用自己去分情况解析):
String contentType = request.getServletContext().getMimeType(fileName); // 结果如text/html
response.setContentType( contentType );
属性复制
自造轮子:Bean to Map、Map to Bean、Bean to Bean,通过反射完成。
对于Bean to Bean,只有名字一样的属性才会复制
代码:
/** * Map --> Bean:利用Introspector、PropertyDescriptor实现。Bean中有public set方法的属性才会被赋值。 * * @param <T> * * @param map 待转换者 * @param obj Bean实例 */ public static void transMap2Bean(Map<String, Object> map, Object obj) { try { BeanInfo beanInfo = Introspector.getBeanInfo(obj.getClass()); PropertyDescriptor[] propertyDescriptors = beanInfo.getPropertyDescriptors();// 有public get或public // set方法的属性才会被获取到,此外还一定额外会有个只带有public // get方法的class属性 for (PropertyDescriptor property : propertyDescriptors) { String key = property.getName(); if (!key.equals("class") && map.containsKey(key)) {// 会自动有个只有public get方法的class属性 Method setter = property.getWriteMethod();// 获取属性的public set方法,故属性有public set方法才能获取到,否则为null if (null == setter) { continue; } Object value = map.get(key); setter.setAccessible(true); setter.invoke(obj, value); } } } catch (Exception e) { throw new RuntimeException(e); } } /** * Bean --> Map:利用Introspector、PropertyDescriptor实现。Bean中有public * get方法的属性才会被包装到Map。 * * @param obj 指定的对象实例 * @return */ public static Map<String, Object> transBean2Map(Object obj, String... ignoreFields) { if (obj == null) { return null; } Map<String, Object> map = new HashMap<String, Object>(); try { BeanInfo beanInfo = Introspector.getBeanInfo(obj.getClass()); PropertyDescriptor[] propertyDescriptors = beanInfo.getPropertyDescriptors();// 有public get或public // set方法的属性才会被获取到,此外还一定额外会有个只带有public // get方法的class属性 List<String> ignoreList = Arrays.asList(commonIgnoreFields); if (ignoreFields != null && ignoreFields.length > 0) { ignoreList.addAll(Arrays.asList(ignoreFields)); } for (PropertyDescriptor property : propertyDescriptors) { String key = property.getName(); if (ignoreList.contains(key)) { continue; } if (!key.equals("class")) {// 会自动有个只有public get方法的class属性 Method getter = property.getReadMethod();// 获取属性的public get方法,故属性有public get方法才能获取到,否则为null if (null == getter) { continue; } getter.setAccessible(true); Object value = getter.invoke(obj); map.put(key, value); } } } catch (Exception e) { log.error(e.getMessage(), e); } return map; } /** * 将srcBean中的属性值复制到tarBean的同名属性中 * * @see {@link org.springframework.beans.BeanUtils#copyProperties(Object, Object)} * @param srcBean * @param tarBean */ public static void transBean2Bean(Object srcBean, Object tarBean) { // 等价于CommonUtil.transMap2Bean(CommonUtil.transBean2Map(srcBean), tarBean); BeanUtils.copyProperties(srcBean, tarBean); } /** * Bean --> Map:利用反射将指定对象的指定属性及值包装到Map。不要求属性一定得有get方法。 * * @param obj 指定的对象实例 * @param fields 该对象的属性集合 * @return */ public static Map<String, Object> copyFidddeldsToMap1(Object obj, Field[] fields) { if (null == obj) { return null; } Map<String, Object> desMap = new HashMap<>(); for (Field field : fields) { field.setAccessible(true); try { desMap.put(field.getName(), field.get(obj)); } catch (IllegalArgumentException | IllegalAccessException e) { log.error(e.getMessage(), e); } } return desMap; }
SpringFrameWork的org.springframework.beans.copyProperties:该方法不仅支持Bean copy,还会:识别属性的@JsonIgnore属性以忽略属性复制、也可以在方法参数指定要忽略复制的属性
序列化时属性忽略
给Bean的字段加上@JsonIgnore,则在序列化时(如后端数据返回给前端、后端数据序列化成字符串等)不会序列化该字段。
获取文件绝对路径
通过org.springframework.util.ResourceUtils,示例: ResourceUtils.getURL("classpath:").getPath()
若参数为"classpath:",则该路径是项目jar包所在目录的绝对路径、若是IDE里运行则是项目根目录。
若参数非"classpath:",则将参数当成一个网络地址去并尝试去构建URL
内部实现如下:
1 public static URL getURL(String resourceLocation) throws FileNotFoundException { 2 Assert.notNull(resourceLocation, "Resource location must not be null"); 3 if (resourceLocation.startsWith(CLASSPATH_URL_PREFIX)) { 4 String path = resourceLocation.substring(CLASSPATH_URL_PREFIX.length()); 5 ClassLoader cl = ClassUtils.getDefaultClassLoader(); 6 URL url = (cl != null ? cl.getResource(path) : ClassLoader.getSystemResource(path)); 7 if (url == null) { 8 String description = "class path resource [" + path + "]"; 9 throw new FileNotFoundException(description + 10 " cannot be resolved to URL because it does not exist"); 11 } 12 return url; 13 } 14 try { 15 // try URL 16 return new URL(resourceLocation); 17 } 18 catch (MalformedURLException ex) { 19 // no URL -> treat as file path 20 try { 21 return new File(resourceLocation).toURI().toURL(); 22 } 23 catch (MalformedURLException ex2) { 24 throw new FileNotFoundException("Resource location [" + resourceLocation + 25 "] is neither a URL not a well-formed file path"); 26 } 27 } 28 }
可见,ResourceUtils.getURL内部首先就是尝试通过ClassLoader来读取classPath路径。
文件下载
借助ResponseEntity
1 public ResponseEntity<InputStreamResource> getExperimentIcon(String fileRelativePath) throws IOException {// fileRelativePath指相对于jar包所在父目录的路径 2 // 获取项目jar包所在目录的绝对路径。若是在开发测试模式,则是项目根目录。 3 File parentDirObj = new File(ResourceUtils.getURL("classpath:").getPath()); 4 if (!parentDirObj.exists()) 5 parentDirObj = new File(""); 6 7 FileSystemResource file = new FileSystemResource(parentDirObj.getAbsolutePath() + "/" + fileRelativePath); 8 9 return ResponseEntity.ok().contentLength(file.contentLength()).contentType(MediaType.IMAGE_PNG) 10 .body(new InputStreamResource(new BufferedInputStream(file.getInputStream()))); 11 12 }
SpringBoot对返回的数据序列化(用的是ObjectMapper序列化工具)时报错
No serializer found for class org.hibernate.proxy.pojo.bytebuddy.ByteBuddyInterceptor and no properties discovered to create BeanSerializer (to avoid exception, disable SerializationFeature.FAIL_ON_EMPTY_BEANS)
原因:若待序列化的数据中有些属性违背输出规则或有些属性循环引用就会造成无法输出,这时就会出现此错。
示例:
比如使用JPA时(具体实现为Hibernate),若Entity中采用懒加载(fetch=FetchType.LAZY)的模式加载依赖的外键对象,则该对象会被hibernate动态嵌入一个属性,该属性是个代理对象(类型为 org.hibernate.proxy.pojo.bytebuddy.ByteBuddyInterceptor ),在序列化时无法对该类进行序列化。此外,hibernate延迟加载还好为对象自动加入一些属性,如忽略Hibernate的延迟加载的一些属性"hibernateLazyInitializer", "handler", "fieldHandler"。示例:
解决:
法1(不推荐):一种方法是如错误信息中所示,对于空对象直接忽略而不进行序列化,作如下配置即可。
@Bean("objectMapper") public ObjectMapper myMapper() { return new ObjectMapper().disable(SerializationFeature.FAIL_ON_EMPTY_BEANS); }
此法的缺点是"hibernateLazyInitializer"等hibernate自动生成的属性也序列化到返回给调用者的数据中了,这并不是期望的。
法2:在外键对象类上忽略Hibernate延迟加载所加入的属性,如在ExperimentEntity加如下配置: @JsonIgnoreProperties(value = { "hibernateLazyInitializer", "handler", "fieldHandler" })
参考资料:
https://segmentfault.com/a/1190000011702922、https://blog.csdn.net/jxchallenger/article/details/79307062
@RequestParameter一个参数接收多个值
后端参数声明为List,如 @RequestParam List<String> scope ,前端调用时有两种传法皆可,法1: scope=1&scope=2&scope=3 ,法2: scope=1,2,3 ,显然后者更好
1、Java Web设置页面刷新(两种方法):
response.setHeader("refresh", "0.3," + request.getHeader("Referer"));
response.getWriter().print("<meta http-equiv='Refresh' content='5;url=http://www.baidu.com.cn' />");