本系列文章:
Spring(一)IOC、DI、@Autowired、@Resource、作用域
Spring(二)IOC容器的初始化流程
Spring(三)IOC容器的依赖注入流程
Spring(四)IOC容器的高级特性
Spring(五)AOP、事务
Spring(六)Spring MVC
Spring(七)SpringBoot
Spring(八)Spring Cloud
Model(数据模型),提供要展示的数据
,因此包含数据和行为,现在一般都分离开来:数据DAO和服务层(行为Service)。也就是模型提供了模型数据查询和模型数据的状态更新等功能,包括数据和业务。可简单理解为和数据库打交道的行为和交互的数据。View(视图),负责进行模型的展示,一般就是我们见到的用户界面
,客户想看到的东西。Controller(控制器),接收用户请求,委托给模型进行处理
,处理完毕后把返回的模型数据返回给视图,由视图负责展示。 也就是说控制器做了个调度员的工作。前端控制器DispatcherServlet
;HandlerMapping处理器映射器
,请求获取Handler;HandlerAdapter处理器适配器
;SpringMVC中的Servlet一共有三个层次,分别是HttpServletBean、FrameworkServlet和 DispatcherServlet:
- HttpServletBean直接继承自Java的HttpServlet,其作用是将Servlet中配置的参数设置到相应的属性;
- FrameworkServlet初始化了WebApplicationContext;
- DispatcherServlet初始化了自身的9个组件。
Handler,也就是处理器。它直接应对着MVC中的C也就是Controller层,它的具体表现形式有很多,可以是类,也可以是方法。在Controller层中@RequestMapping标注的所有方法都可以看成是一个Handler,只要可以实际处理请求就可以是Handler。
最常见的主要组件有六个:
public interface HandlerMapping
{
HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception;
}
HandlerMapping接口中只定义了一个方法,就是通过request找到HandlerExecutionChain,而HandlerExecutionChain包装了一个Handler和一组Interceptors。
public interface HandlerAdapter {
//判断是否支持传入的handler
boolean supports(Object handler);
//使用给定的handler处理请求
ModelAndView handle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception;
//返回上次修改时间,可以返回-1表示不支持
long getLastModified(HttpServletRequest request, Object handler);
}
从名字上看,它就是一个适配器。因为SpringMVC中的Handler可以是任意的形式,只要能处理请求就ok,但是Servlet需要的处理方法的结构却是固定的,都是以request和response为参数的方法。如何让固定的Servlet处理方法调用灵活的Handler来进行处理呢?这就是HandlerAdapter要做的事情。任意形式的Handler通过使用适配器,可以“转换”成固定形式,然后交给Servlet来处理。每种Handler都要有对应的HandlerAdapter才能处理请求。
public interface ViewResolver {
View resolveViewName(String viewName, Locale locale) throws Exception;
}
ViewResolver用来将String类型的视图名和Locale解析为View类型的视图。View是用来渲染页面的,也就是将程序返回的参数填入模板里,生成html(也可能是其它类型)文件。这里就有两个关键问题:使用哪个模板?用什么技术(规则)填入参数?这其实是ViewResolver主要要做的工作,ViewResolver需要找到渲染所用的模板和所用的技术(也就是视图的类型)进行渲染,具体的渲染过程则交由不同的视图自己完成。
总结:处理器Handler(也就是平常说的Controller控制器)以及视图层View ,都是需要自行开发的。其他的一些组件,如:前端控制器DispatcherServlet、处理器映射器HandlerMapping、处理器适配器HandlerAdapter等都是由框架提供。
除了上面的这些,还有一些相对不那么常用的组件(了解即可):
对异常情况进行处理
,在SpringMVC中就是HandlerExceptionResolver。具体来说,此组件的作用是根据异常设置ModelAndView,之后再交给render方法进行渲染。HandlerExceptionResolver的接口定义:public interface HandlerExceptionResolver {
ModelAndView resolveException(
HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex);
}
从上面的分析中我们可以知道HandlerExceptionResolver只能处理页面渲染之前的异常,页面渲染过程中的异常,它是不能处理的,这时可以让容器跳转到指定的错误页面来处理异常。
用于解析主题
。SpringMVC中一个主题对应一个properties文件,里面存放着跟当前主题相关的所有资源、如图片、css样式等。SpringMVC的主题也支持国际化,同一个主题不同区域也可以显示不同的风格。SpringMVC中跟主题相关的类有 ThemeResolver、ThemeSource和Theme。主题是通过一系列资源来具体体现的,要得到一个主题的资源,首先要得到资源的名称,这是ThemeResolver的工作。然后通过主题名称找到对应的主题(可以理解为一个配置)文件,这是ThemeSource的工作。最后从主题中获取资源就可以了。用于处理上传请求
。处理方法是将普通的request包装成MultipartHttpServletRequest,后者可以直接调用getFile方法获取File,如果上传多个文件,还可以调用getFileMap得到FileName->File结构的Map。此组件中一共有三个方法,作用分别是判断是不是上传请求,将request包装成MultipartHttpServletRequest、处理完后清理上传过程中产生的临时资源。 SpringMVC的核心就是DispatcherServlet,DispatcherServlet实质也是一个HttpServlet
。DispatcherSevlet负责将请求分发,所有的请求都有经过它来统一分发。大致看下SpringMVC请求处理的流程:
用户向服务器发送请求,请求会到DispatcherServlet,DispatcherServlet 对请求URL进行解析,得到请求资源标识符(URI),然后根据该URI,调用HandlerMapping获得该Handler配置的所有相关的对象(包括一个Handler处理器对象、多个HandlerInterceptor拦截器对象),最后以HandlerExecutionChain对象的形式返回。
DispatcherServlet 根据获得的Handler,选择一个合适的HandlerAdapter。提取Request中的模型数据,填充Handler入参,开始执行Handler(Controller)。
在填充Handler的入参过程中,根据你的配置,Spring将帮你做一些额外的工作:
HttpMessageConveter: 将请求消息(如Json、xml等数据)转换成一个对象,将对象转换为指定的响应信息
数据转换:对请求消息进行数据转换。如String转换成Integer、Double等
数据格式化:对请求消息进行数据格式化。 如将字符串转换成格式化数字或格式化日期等
数据验证: 验证数据的有效性(长度、格式等),验证结果存储到BindingResult或Error中
Handler执行完成后,向DispatcherServlet返回一个ModelAndView对象;根据返回的ModelAndView,选择一个适合的ViewResolver返回给DispatcherServlet;ViewResolver 结合Model和View,来渲染视图,最后将渲染结果返回给客户端。
注解属于Java语言的特性,是在Java5.0引入的新特性
。
注解是用于给Java代码附加元数据,可在编译时或运行时解析并处理这些元数据
。Java代码可以是包名、类、方法、成员变量、参数等,且附加的元数据不会影响源代码的执行。
可以这样理解Java注解:想像Java代码如包名、类、方法、成员变量、参数等都是具有生命,注解就是给代码中某些元素贴上去的一张标签。通俗点来讲,注解如同一张标签
。
[@Target]
[@Retention]
[@Documented]
[@Inherited]
public @interface [名称] {
// 元素
}
形式跟接口很类似,不过前面多了一个@符号。
定义注解时可给注解添加属性,也叫注解的成员变量。注解只有成员变量,没有方法。注解的成员变量的以“无形参的方法”形式来声明。
@Target({ElementType.FIELD, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface Test {
String value();
}
还可给注解的属性设定默认值:
@Target({ElementType.FIELD, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface Test {
String value() default "hello";
}
注解的属性可支持数据类型有如下:
所有基本数据类型(int,float,boolean,byte,double,char,long,short)
String类型
Class类型
enum类型
Annotation类型
以上所有类型的数组
- @Override:表示当前的方法定义将覆盖父类中的方法;
- @Deprecated:表示代码被弃用,如果使用了被@Deprecated注解的代码则编译器将发出警告;
- @SuppressWarnings:表示关闭编译器警告信息。
元注解 | 作用 | 备注 |
---|---|---|
@Target | 指定了注解运用的地方 | 你可以这样理解,当一个注解被 @Target 注解时,这个注解就被限定了运用的场景。类比到标签,原本标签是你想张贴到哪个地方就到哪个地方,但是因为 @Target 的存在,它张贴的地方就非常具体了,比如只能张贴到方法上、类上、方法参数上等等。 |
@Retention | 说明注解的存活时间 | 我们可以这样的方式来加深理解,@Retention 去给一张标签解释的时候,它指定了这张标签张贴的时间。@Retention 相当于给一张标签上面盖了一张时间戳,时间戳指明了标签张贴的时间周期。 |
@Document | 说明注解是否能被文档化 | 不常用 |
@Inhrited | 说明注解能否被继承 | 不常用 |
可以在两个时期对注解进行处理:编译时和运行时。
- 通过反射会影响运行效率;
- 如果注解无法保存到运行时的话,是无法使用运行时处理的。
public class CustomProcessor extends AbstractProcessor {
//初始化注解处理器
//@param processingEnv APT框架的环境对象,通过该对象可以获取到很多工具类
@Override
public synchronized void init(ProcessingEnvironment processingEnv) {
super.init(processingEnv);
}
//配置该注解处理器需要处理的注解类型
@Override
public Set<String> getSupportedAnnotationTypes() {
return super.getSupportedAnnotationTypes();
}
//配置该注解处理器支持的最新版本
@Override
public SourceVersion getSupportedSourceVersion() {
return super.getSupportedSourceVersion();
}
//用于处理注解,并生成.java文件
@Override
public boolean process(Set<? extends TypeElement> annotations,
RoundEnvironment roundEnv) {
return false;
}
}
编译时处理的优点: 不在运行时进行操作,所以对程序的性能不会有什么影响。
编译时处理的缺点:
- 无法对原来的.java文件进行修改;
- 生成额外的.java文件;
- 因为是在编译期进行处理注解,所以会对编译速度有影响。
@RequestParam与@PathVariable为Spring的注解,都可以用于在Controller层接收前端传递的数据,不过两者的应用场景不同。
@PathVariable主要用于接收http://host:port/path/{参数值}
类型的数据。@RequestParam主要用于接收http://host:port/path?参数名=参数值
类型的数据,这里后面也可以不跟参数值。
PathVariable只能用于接收url路径上的参数,而RequestParam只能用于接收请求带的params
,示例: //使用@PathVariable接收参数,参数值需要在url进行占位, 前端传参
//的URL:url = ${ctx}/main/mm/am/edit/${Id}/${name}
@RequestMapping("/edit/{id}/{name}")
public String edit(Model model, @PathVariable long id,@PathVariable String name){
return page("edit");
}
//在url中输入:localhost:8080/**/?userName=zhangsan
//请求中包含username参数(如/requestparam1?userName=zhang),则自动传入。
public String queryUserName(@RequestParam String userName)
使用@PathVariable时,需要注意一个问题,如果想要url中占位符中的id值直接赋值到参数id中,需要保证url中的参数和方法接收参数一致,否则就无法接收。如果不一致的话,其实也可以解决,需要用@PathVariable中的value属性来指定对应关系。示例:
@RequestMapping("/user/{idd}")
public String testPathVariable(@PathVariable(value = "idd") Integer id) {
System.out.println("获取到的 id 为:" + id);
return "success";
}
不少应用为了实现RestFul的风格,采用@PathVariable这种方式。
RequestMapping是一个用来处理请求地址映射的注解,可用于类或方法上。用于类上,表示类中的所有响应请求的方法都是以该地址作为父路径。
RequestMapping注解有六个属性,下面分成三类进行说明:
value: 指定请求的实际地址
;method: 指定请求的method类型, GET、POST、PUT、DELETE等,默认为GET
; @RequestMapping(value = "/hello02", method = RequestMethod.POST)
public String hello02(){
return "success";
}
@RequestMapping(value = "/test", produces="application/json;charset=UTF-8")
@RequestMapping( value = "/hello",params = {"username"})
@RequestMapping( value = "/hello",headers = {"User-Agent=Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.79 Safari/537.36"})
@Controller("accountController")
@RequestMapping("/account")
public class AccountController {
}
@RequestMapping(value="/removeAccount",params= {"accountName","money>100"})
public String removeAccount() {
System.out.println("删除了账户");
return "success";
}
注解 @ResponseBody,使用在控制层的方法上。
作用:@responseBody注解的作用是将controller的方法返回的对象通过适当的转换器转换为指定的格式之后,写入到response对象的body区,通常用来返回JSON数据
,需要注意的是,在使用此注解之后不会再走视图处理器,而是直接将数据写入到输入流中,他的效果等同于通过response对象输出指定格式的数据。
@ResponseBody:表示该方法的返回结果直接写入HTTPresponsebody中,一般在异步获取数据时使用,用于构建RESTFUL的API。在使用@RequestMapping后,返回值通常解析为跳转路径,加上@esponsebody后返回结果不会被解析为跳转路径,而是直接写入HTTP responsebody中。比如异步获取json数据,加@Responsebody后,会直接返回json数据。该注解一般会配合@RequestMapping一起使用。
如果返回对象,按utf-8编码。如果返回String,默认按iso8859-1编码,页面可能出现乱码。因此在注解中我们可以手动修改编码格式,例如@RequestMapping(value=“/cat/query”,produces=“text/html;charset=utf-8”),前面是请求的路径,后面是编码格式。
示例:
@RequestMapping("/login")
@ResponseBody
public User login(User user){
return user;
}
假设User字段:userName pwd,那么在前台接收到的数据为:
'{"userName":"xxx","pwd":"xxx"}'
效果等同于如下代码:
@RequestMapping("/login")
public void login(User user, HttpServletResponse response){
response.getWriter.write(JSONObject.fromObject(user).toString());
}
原理:控制层方法的返回值是如何转化为json格式的字符串的?其实是通过HttpMessageConverter中的方法实现的,它本是一个接口,在其实现类完成转换。如果是bean对象,会调用对象的getXXX()方法获取属性值并且以键值对的形式进行封装,进而转化为json串。如果是map集合,采用get(key)方式获取value值,然后进行封装。
@ResponseBody注解是将返回的数据结构转换为Json格式。所以 @RestController可以看作是@Controller和@ResponseBody的结合体,使用@RestController之后就不用再使用@Controller了。但是需要注意一个问题:如果是前后端分离,不用模板渲染的话,比如Thymeleaf,这种情况下是可以直接使用@RestController将数据以json格式传给前端,前端拿到之后解析;但如果不是前后端分离,需要使用模板来渲染的话,一般Controller中都会返回到具体的页面,那么此时就不能使用@RestController 了。示例:
public String getUser() {
return "user";
}
其实是需要返回到user.html页面的,如果使用@RestController的话,会将user作为字符串返回的,所以这时候我们需要使用@Controller注解。
@RequestBody主要用来接收前端传递给后端的json字符串中的数据的(请求体中的数据的)
;
GET方式无请求体,所以使用@RequestBody接收数据时,前端不能使用GET方式提交数据,而是用POST方式进行提交。
示例:
@RequestMapping("/testRequestBody")
public String testRequestBody(@RequestBody String body){
System.out.println(body);
return "success";
}
@RequestMapping("/testRequestJson")
public String testRequestJson(@RequestBody User user){
System.out.println(user);
return "success";
}
在Spring2.5版本中,引入了更多的Spring类注解:
@Component、@Service、@Controller。
@Component是一个通用的Spring容器管理的单例bean组件注解。而@Repository, @Service, @Controller就是针对不同的使用场景所采取的特定功能化的注解组件。
总结:
@Component是通用注解,理论上可以在任意的类上进行添加,在扫描的时候都会完成Bean的注册其他三个注解是这个注解的拓展
,并且具有了特定的功能 。@Repository注解在持久层中,对数据库进行操作
。@Controller是控制层的注解,具有将请求进行转发,重定向的功能,包括调用Service层的方法
。@Service是业务逻辑层注解,这个注解只是标注该类处于业务逻辑层
。用这些注解对应用进行分层之后,就能将请求处理,义务逻辑处理,数据库操作处理分离出来,为代码解耦,也方便了项目的维护和开发。
常用的缓存注解有3个:@Cacheable、@CachePut和@CacheEvict。
参数 | 解释 | 示例 |
---|---|---|
value | 缓存的名称,在 spring 配置文件中定义,必须指定至少一个 | @Cacheable(value=”mycache”) @Cacheable(value={”cache1”,”cache2”} |
key | 缓存的 key,可以为空,如果指定要按照 SpEL 表达式编写,如果不指定,则缺省按照方法的所有参数进行组合 | @Cacheable(value=”testcache”,key=”#userName”) |
condition | 缓存的条件,可以为空,使用 SpEL 编写,返回 true 或者 false,只有为 true 才进行缓存 | @Cacheable(value=”testcache”,condition=”#userName.length()>2”) |
使用示例:@Cacheable(value=”accountCache”)
。这个注释的意思是,当调用这个方法的时候,会从一个名叫accountCache的缓存中查询,如果没有,则执行实际的方法(即查询数据库),并将执行的结果存入缓存中,否则返回缓存中的对象。这里的缓存中的key就是参数userName,value就是Account对象。“accountCache”缓存是在 spring*.xml中定义的名称。较完整示例:
@Cacheable(value="accountCache")// 使用了一个缓存名叫 accountCache
public Account getAccountByName(String userName) {
// 方法内部实现不考虑缓存逻辑,直接实现业务
System.out.println("real query account."+userName);
return getFromDB(userName);
}
参数 | 解释 | 例子 |
---|---|---|
value | 缓存的名称,在 spring 配置文件中定义,必须指定至少一个 | @CachePut(value=”my cache”) |
key | 缓存的 key,可以为空,如果指定要按照 SpEL 表达式编写,如果不指定,则缺省按照方法的所有参数进行组合 | @CachePut(value=”testcache”,key=”#userName”) |
condition | 缓存的条件,可以为空,使用 SpEL 编写,返回 true 或者 false,只有为 true 才进行缓存 | @CachePut(value=”testcache”,condition=”#userName.length()>2”) |
@CachePut注释,这个注释可以确保方法被执行,同时方法的返回值也被记录到缓存中,实现缓存与数据库的同步更新。示例代码:
@CachePut(value="accountCache",key="#account.getName()")// 更新accountCache 缓存
public Account updateAccount(Account account) {
return updateDB(account);
}
参数 | 解释 | 例子 |
---|---|---|
value | 缓存的名称,在 spring 配置文件中定义,必须指定至少一个 | @CacheEvict(value=”my cache”) |
key | 缓存的 key,可以为空,如果指定要按照 SpEL 表达式编写,如果不指定,则缺省按照方法的所有参数进行组合 | @CacheEvict(value=”testcache”,key=”#userName”) |
condition | 缓存的条件,可以为空,使用 SpEL 编写,返回 true 或者 false,只有为 true 才进行缓存 | @CacheEvict(value=”testcache”,condition=”#userName.length()>2”) |
allEntries | 是否清空所有缓存内容,缺省为 false,如果指定为 true,则方法调用后将立即清空所有缓存 | @CachEvict(value=”testcache”,allEntries=true) |
beforeInvocation | 是否在方法执行前就清空,缺省为 false,如果指定为 true,则在方法还没有执行的时候就清空缓存,缺省情况下,如果方法执行抛出异常,则不会清空缓存 | @CachEvict(value=”testcache”,beforeInvocation=true) |
示例:
@CacheEvict(value="accountCache",key="#account.getName()")// 清空accountCache 缓存
public void updateAccount(Account account) {
updateDB(account);
}
@CacheEvict(value="accountCache",allEntries=true)// 清空accountCache 缓存
public void reload() {
reloadAll()
}
@Cacheable(value="accountCache",condition="#userName.length() <=4")// 缓存名叫 accountCache
public Account getAccountByName(String userName) {
// 方法内部实现不考虑缓存逻辑,直接实现业务
return getFromDB(userName);
}
Lombok的@Data注解会帮我们实现equals和hashcode方法,但是有继承关系时,Lombok自动生成的方法可能就不是我们期望的了。示例:定义一个Person类型,包含姓名和身份证两个字段:
@Data
class Person {
private String name;
private String identity;
public Person(String name, String identity) {
this.name = name;
this.identity = identity;
}
}
对于身份证相同、姓名不同的两个Person对象:
Person person1 = new Person("zhuye","001");
Person person2 = new Person("Joseph","001");
log.info("person1.equals(person2) ? {}", person1.equals(person2));
使用equals判等会得到false。如果希望只要身份证一致就认为是同一个人的话,可以使用 @EqualsAndHashCode.Exclude
注解来修饰name字段,从equals和hashCode的实现中排除name字段:
如果类型之间有继承,Lombok会怎么处理子类的equals和hashCode呢?
@EqualsAndHashCode默认实现没有使用父类属性。为解决这个问题,我们可以手动设置callSuper开关为true,来覆盖这种默认行为:@EqualsAndHashCode(callSuper = true)
。
控制器是单例模式
,所以在多线程访问的时候有线程安全问题,不建议用同步,会影响性能。
为什么设计成单例设计模式?
- 性能(不用每次请求都创建对象)。
- 不需要多例(不要在控制器中定义成员变量)。
控制器最佳实践:
不要在Controller中定义成员变量
。万一必须要定义一个非静态成员变量时候,则通过注解@Scope("prototype"),将其设置为多例模式
。
在返回值前面加"forward:"
,譬如"forward:user.do?name=method4"。 @RequestMapping("/forward")
public String forward(){
System.out.println("forward");
return "forward:/index.jsp";
}
@RequestMapping("/forward2")
public String forward2(){
System.out.println("forward2");
return "forward:/forward";
}
在返回值前面加"redirect:"
,譬如"redirect:http://www.baidu.com"。
- 地址栏的请求不会发生变化,显示的还是第一次请求的地址;
请求的次数,有且仅有一次请求
;请求域中的数据不会丢失
。
- 地址栏的地址发生变化,显示最新发送请求的地址;
请求次数:2次
;请求域中的数据会丢失
,因为是不同的请求。
区别 | 转发forward() | 重定向sendRedirect() |
---|---|---|
根目录 | 包含项目访问地址 | 没有项目访问地址 |
地址栏 | 不会发生变化 | 会发生变化 |
哪里跳转 | 服务器端进行的跳转 | 浏览器端进行的跳转 |
请求域中数据 | 不会丢失 | 会丢失 |
post
:必须要分别设置request和response的编码格式。在web.xml中配置一个CharacterEncodingFilter过滤器(此过滤器为Spring MVC提供的),设置成utf-8
,示例:<filter>
<filter-name>CharacterEncodingFilterfilter-name>
<filter-class>org.springframework.web.filter.CharacterEncodingFilterfilter-class>
<init-param>
<param-name>encodingparam-name>
<param-value>utf-8param-value>
init-param>
filter>
<filter-mapping>
<filter-name>CharacterEncodingFilterfilter-name>
<url-pattern>/*url-pattern>
filter-mapping>
如果在一个应用程序中可能会包含N多个(自定义)过滤器,这N多个过滤器一般是没有顺序的要求的。但是如果设置了编码过滤器,那么要求必须要将编码过滤器设置到最上面,保证编码过滤器优先执行。
get
请求中文参数出现乱码解决方法有两个:改tomcat配置文件(server.xml)添加编码,与工程编码一致
,该方式修改文件少,较常用,如下:
2)另外一种方法:对参数进行重新编码
:
String userName = new String(request.getParamter(“userName”)
.getBytes(“ISO8859-1”),“utf-8”);
ISO8859-1是tomcat默认编码,需要将tomcat编码后的内容按utf-8编码。
Spring MVC处理异常有3种方式:
该方法仅能获取到异常信息
,若在出现异常时,对需要获取除异常以外的数据的情况不适用。public class CustomExceptionHandler implements HandlerExceptionResolver {
@Override
public ModelAndView resolveException(HttpServletRequest request,
HttpServletResponse response, Object object, Exception exception) {
if(exception instanceof IOException){
return new ModelAndView("ioexp");
}else if(exception instanceof SQLException){
return new ModelAndView("sqlexp");
}
return null;
}
}
这个类必须声明到Spring配置文件中,或者使用@Component标签,让Spring管理它。
/*当使用ExceptionHandler进行处理的时候,默认会先走小范围,然后再寻找大范围
* 当在某一个类中定义的ExceptionHandler只能处理当前类的异常信息,如果其他类
* 包含的话,无法进行处理
*/
@ExceptionHandler(value = {ArithmeticException.class,NullPointerException.class})
public ModelAndView handlerException(Exception exception){
System.out.println("exception1");
ModelAndView mv = new ModelAndView();
mv.setViewName("error");
mv.addObject("exce",exception);
return mv;
}
@ExceptionHandler(value = {Exception.class})
public ModelAndView handlerException2(Exception exception){
System.out.println("exception2");
ModelAndView mv = new ModelAndView();
mv.setViewName("error");
mv.addObject("exce",exception);
return mv;
}
全局异常处理
示例(在类上使用@ControllerAdvice注解):
@ControllerAdvice
public class MyGlobalExceptionHandler {
@ExceptionHandler(value = {ArithmeticException.class,NullPointerException.class})
public ModelAndView handlerException(Exception exception){
System.out.println("global-------exception1");
ModelAndView mv = new ModelAndView();
mv.setViewName("error");
mv.addObject("exce",exception);
return mv;
}
@ExceptionHandler(value = {Exception.class})
public ModelAndView handlerException2(Exception exception){
System.out.println("global-------exception2");
ModelAndView mv = new ModelAndView();
mv.setViewName("error");
mv.addObject("exce",exception);
return mv;
}
}
当局部注解@ExceptionHandler和全局注解@ControllerAdvice共存时,每次进行异常处理的时候,先在本类查找,然后去查找全局配置
。
Spring MVC集成异常处理3种方式都可以达到统一异常处理的目标。从3种方式的优缺点比较:
直接在形参里面声明这个参数就可以,但必须名字和传过来的参数一样。示例:
返回值可以有很多类型,有String、ModelAndView等。ModelAndView类把视图和数据都合并的一起的,但一般用String比较好。
整型:Integer、int
字符串:String
单精度:Float、float
双精度:Double、double
布尔型:Boolean、boolean
@SessionAttributes只能使用在类定义上,Spring MVC会将存放在model中对应的数据暂存到HttpSession中
。
@Controller
@SessionAttributes("linkList")
public class RoomControl {
REST,翻译过来叫做表现层状态转化,是一种软件架构风格、设计风格,而不是标准,只是提供了一组设计原则和约束条件。它主要用于客户端和服务器交互类的软件。基于这个风格设计的软件可以更简洁,更有层次,更易于实现缓存等机制。
我们在获取资源的时候就是进行增删改查的操作,如果是常规的架构风格,需要发送四个请求,分别是:
查询:localhost:8080/query?id=1
增加:localhost:8080/insert
删除:localhost:8080/delete?id=1
更新:localhost:8080/update?id=1
按照此方式发送请求的时候比较麻烦,需要定义多种请求,所以产生了能让不同的请求方式表示不同的请求类型
,即Restful风格:
GET:获取资源 /book/1
POST:新建资源 /book
PUT:更新资源 /book/1
DELETE:删除资源 /book/1
示例:
package com.test.controller;
import org.springframework.stereotype.Controller;
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.servlet.view.InternalResourceViewResolver;
@Controller
public class RestController {
@RequestMapping(value = "/user",method = RequestMethod.POST)
public String add(){
System.out.println("添加");
return "success";
}
@RequestMapping(value = "/user/{id}",method = RequestMethod.DELETE)
public String delete(@PathVariable("id") Integer id){
System.out.println("删除:"+id);
return "success";
}
@RequestMapping(value = "/user/{id}",method = RequestMethod.PUT)
public String update(@PathVariable("id") Integer id){
System.out.println("更新:"+id);
return "success";
}
@RequestMapping(value = "/user/{id}",method = RequestMethod.GET)
public String query(@PathVariable("id") Integer id){
System.out.println("查询:"+id);
return "success";
}
}
此时浏览器form表单只支持GET与POST请求,而DELETE、PUT等method并不支持。对于此问题,Spring3.0添加了一个HiddenHttpMethodFilter过滤器,可以将这些请求转换为标准的http方法,使得支持GET、POST、PUT与DELETE请求。配置示例:
<filter>
<filter-name>hiddenfilter-name>
<filter-class>org.springframework.web.filter.HiddenHttpMethodFilterfilter-class>
filter>
<filter-mapping>
<filter-name>hiddenfilter-name>
<url-pattern>/*url-pattern>
filter-mapping>
SpringMVC提供了拦截器机制,允许运行目标方法之前进行一些拦截工作或者目标方法运行之后进行一下其他相关的处理。自定义的拦截器必须实现HandlerInterceptor接口。
拦截器方法的执行时机:
业务处理器处理请求之前被调用
,在该方法中对用户请求request进行处理。如果程序员决定该拦截器对请求进行拦截处理后还要调用其他的拦截器,或者是业务处理器去进行处理,则返回true;如果程序员决定不需要再调用其他的组件去处理请求,则返回false。业务处理器处理完请求后,但是DispatcherServlet 向客户端返回响应前
被调用,在该方法中对用户请求request进行处理。在DispatcherServlet完全处理完请求后被调用
,可以在该方法中进行一些资源清理的操作。 有两种写法,一种是实现org.springframework.web.servlet.HandlerInterceptor接口
;另外一种是继承适配器org.springframework.web.servlet.handler.HandlerInterceptorAdapter
类,接着在接口方法当中,实现处理逻辑;最后在Spring MVC的配置文件中配置拦截器即可:
<mvc:interceptors>
<bean id="myInterceptor" class="com.zwp.action.MyHandlerInterceptor">bean>
<mvc:interceptor>
<mvc:mapping path="/modelMap.do" />
<bean class="com.zwp.action.MyHandlerInterceptorAdapter" />
mvc:interceptor>
mvc:interceptors>
先看实现接口的方式:
public class MyInterceptor implements HandlerInterceptor {
/**
* 在处理器处理具体的方法之前开始执行
* @return 注意返回值,如果返回值是false表示请求处理到此为止,如果是true,才会接着向下执行
*/
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
System.out.println(this.getClass().getName()+"----preHandle");
return true;
}
/**
* 在处理器完成方法的处理之后执行
*/
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
System.out.println(this.getClass().getName()+"----postHandle");
}
/**
* 整个servlet处理完成之后才会执行,主要做资源清理的工作
*/
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
System.out.println(this.getClass().getName()+"----afterCompletion");
}
}
拦截器的执行顺序如下:
拦截器的preHandle方法----》执行目标方法----》执行拦截器的postHandle方法----》执行页面跳转----》执行拦截器的afterCompletion方法
在配置拦截器的时候有两个需要注意的点:
定义了多个拦截器时,谁先执行取决于配置的顺序
。
Spring Boot 2.0废弃了WebMvcConfigurerAdapter,但是WebMvcConfigurationSupport又会导致默认的静态资源被拦截,这就需要我们手动将静态资源放开。此时有两种方法解决。
/**
* 用来指定静态资源不被拦截,否则继承 WebMvcConfigurationSupport 这种方式
会导致静态资源无法直接访问
* @param registry
*/@Override
protected void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/**").addResourceLocations("classpath:/static/");
super.addResourceHandlers(registry);
}
@Configuration
public class MyInterceptorConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
// 实现 WebMvcConfigurer 不会导致静态资源被拦截
registry.addInterceptor(new MyInterceptor()).addPathPatterns("/**");
}
}
通常情况下业务代码是分层的,数据经过无状态的Controller、Service、Repository流转到数据库,没必要使用synchronized。
滥用synchronized可能会极大地降低性能。使用Spring框架时,默认情况下 Controller、Service、Repository是单例的,加上synchronized会导致整个程序几乎就只能支持单线程,造成极大的性能问题。
- 对于自定义的业务异常,以Warn级别的日志记录异常以及当前URL、执行方法等信息后,提取异常中的错误码和消息等信息,转换为合适的API包装体返回给API调用方;
- 对于无法处理的系统异常,以Error级别的日志记录异常和上下文信息(比如URL、参数、用户ID)后,转换为普适的“服务器忙,请稍后再试”异常信息,同样以API包装体返回给调用方。
示例:
@RestControllerAdvice
@Slf4j
public class RestControllerExceptionHandler {
private static int GENERIC_SERVER_ERROR_CODE = 2000;
private static String GENERIC_SERVER_ERROR_MESSAGE = "服务器忙,请稍后再试";
@ExceptionHandler
public APIResponse handle(HttpServletRequest req, HandlerMethod method, Exception ex) {
if (ex instanceof BusinessException) {
BusinessException exception = (BusinessException) ex;
log.warn(String.format("访问 %s -> %s 出现业务异常!", req.getRequestURI(), method.toString()), ex);
return new APIResponse(false, null, exception.getCode(), exception.getMessage());
} else {
log.error(String.format("访问 %s -> %s 出现系统异常!", req.getRequestURI(), method.toString()), ex);
return new APIResponse(false, null, GENERIC_SERVER_ERROR_CODE, GENERIC_SERVER_ERROR_MESSAGE);
}
}
}
@GetMapping("wrong1")
public void wrong1(){
try {
readFile();
} catch (IOException e) {
//原始异常信息丢失
throw new RuntimeException("系统忙请稍后再试");
}
}
或者是这样,只记录了异常消息,却丢失了异常的类型、栈等重要信息:
catch (IOException e) {
//只保留了异常消息,栈没有记录
log.error("文件读取错误, {}", e.getMessage());
throw new RuntimeException("系统忙请稍后再试");
}
这两种处理方式都不太合理,可以改为如下方式:
catch (IOException e) {
log.error("文件读取错误", e);
throw new RuntimeException("系统忙请稍后再试");
}
或者,把原始异常作为转换后新异常的cause,原始异常信息同样不会丢:
catch (IOException e) {
throw new RuntimeException("系统忙请稍后再试", e);
}
throw new RuntimeException();
如果捕获了异常打算处理的话,除了通过日志正确记录异常原始信息外,通常还有三种处理模式:
- 转换,即转换新的异常抛出。对于新抛出的异常,最好具有特定的分类和明确的异常消息,而不是随便抛一个无关或没有任何信息的异常,并最好通过cause关联老异常。
- 重试,即重试之前的操作。比如远程调用服务端过载超时的情况,盲目重试会让问题更严重,需要考虑当前情况是否适合重试。
- 恢复,即尝试进行降级处理,或使用默认值来替代原始数据。
使用SpringMVC时,全局性质的跨域配置示例:
@Configuration
@EnableWebMvc
public class WebConfig extends WebMvcConfigurerAdapter {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**");
}
}
使用SpringBoot时,可以改成:
@Configuration
public class MyConfiguration {
@Bean
public WebMvcConfigurer corsConfigurer() {
return new WebMvcConfigurerAdapter() {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**");
}
};
}
}
作为上述其他方法的替代,Spring框架还提供了CorsFilter,示例:
@Configuration
public class MyConfiguration {
@Bean
public FilterRegistrationBean corsFilter() {
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
CorsConfiguration config = new CorsConfiguration();
config.setAllowCredentials(true);
config.addAllowedOrigin("http://domain1.com");
config.addAllowedHeader("*");
config.addAllowedMethod("*");
source.registerCorsConfiguration("/**", config);
FilterRegistrationBean bean = new FilterRegistrationBean(new CorsFilter(source));
bean.setOrder(0);
return bean;
}
}
@CrossOrigin不起作用的原因:
@CrossOrigin
@RestController
public class person{
@RequestMapping(method = RequestMethod.GET)
public String add() {
// 若干代码
}
}
Spring的事件通知机制是一项很有用的功能,使用事件机制可以将相互耦合的代码解耦
,从而方便功能的修改与添加。
举个例子,假设有一个添加评论的方法,在评论添加成功之后需要进行修改Redis缓存、给用户添加积分等等操作。在以前的代码中,可以使用观察者模式来解决这个问题。Spring中已经存在了一个升级版观察者模式的机制,这就是监听者模式
。通过该机制,我们就可以发送接收任意的事件并处理。
监听者模式包含了一个监听者Listener与之对应的事件Event,还有一个事件发布者EventPublish,过程就是EventPublish发布一个事件,被监听者捕获到,然后执行事件相应的方法。
Spring 提供了以下5种标准的事件:
Stop()
方法停止容器时触发该事件。close()
方法)时触发该事件。容器被关闭时,其管理的所有单例Bean都被销毁。request
)结束触发该事件。 如果一个Bean实现了ApplicationListener接口,当一个ApplicationEvent 被发布以后,Bean会自动被通知,其实就是接下来要说的自定义事件。
除了上面介绍的五种事件以外,还可以通过扩展ApplicationEvent 类来开发自定义的事件。示例:
//自定义事件对象
public class CustomApplicationEvent extends ApplicationEvent{
public CustomApplicationEvent ( Object source, final String msg ){
super(source);
System.out.println("Created a Custom event");
}
}
//事件对应的监听器
public class CustomEventListener implements ApplicationListener < CustomApplicationEvent >{
@Override
public void onApplicationEvent(CustomApplicationEvent applicationEvent) {
//handle event
}
}
//通过applicationContext接口的publishEvent()方法来发布自定义事件
CustomApplicationEvent customEvent =
new CustomApplicationEvent(applicationContext, "Test message");
applicationContext.publishEvent(customEvent);