Spring AOP
概念
AOP全称为Aspect Oriented Programming,表示面向切面编程。切面指的是将那些与业务无关,但业务模块都需要使用的功能封装起来的技术。
AOP基本术语
**连接点(Joinpoint):**连接点就是被拦截到的程序执行点,因为Spring只支持方法类型的连接点,所以在Spring中连接点就是被拦截到的方法。连接点由两个信息确定:
- 方法( 表示程序执行点,即在哪个目标方法)
- 相对点(表示方位,即目标方法的什么位置,比如调用前,后等)
切入点(Pointcut): 一般认为,所有的方法都可以认为是连接点,但是我们并不希望在所有的方法上都添加通知,而切入点的作用就是提供一组规则来匹配连接点,给满足规则的连接点添加通知。
**通知、增强(Advice) : **可以为切入点添加额外功能,分为:前置通知、后置通知、异常通知、环绕通知、最终通知等。
**目标对象(Target)**目标对象指将要被增强的对象,即包含主业务逻辑的类对象。或者说是被一个或者多个切面所通知的对象。
**织入(Weaving):**织入是将切面和业务逻辑对象连接起来, 并创建通知代理的过程。织入可以在编译时,类加载时和运行时完成。在编译时进行织入就是静态代理,而在运行时进行织入则是动态代理。
**代理(Proxy):**被AOP织入通知后,产生的结果类。
**切面(Aspect):*切面是一个横切关注点的模块化,一个切面能够包含同一个类型的不同增强方法,比如说事务处理和日志处理可以理解为两个切面。切面由切入点和通知组成。Spring AOP就是负责实施切面的框架,它将切面所定义的横切逻辑织入到切面所指定的连接点中。
应用
配置pom文件:
<dependency> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> <version>5.3.11</version> </dependency> <!-- 切面相关的包 --> <dependency> <groupId>org.aspectj</groupId> <artifactId>aspectjweaver</artifactId> <version>1.8.9</version> </dependency>
编写业务层:
接口:
public interface UserService { int saveUser(Map<String,Object> params); }
实现类:
public class UserServiceImpl implements UserService{ @Override public int saveUser(Map<String, Object> params) { System.out.println("保存用户信息" + params); return 0; } }
配置业务层:
spring-aop.xml:
<!--业务层对象--> <bean id="userService" class="com.qf.aop.service.UserServiceImpl"/>
编写通知类:
通知分为前置通知、后置通知、异常抛出通知、环绕通知、最终通知(没什么用这里不实现)。前置通知:
接口为MethodBeforeAdvice,其底层实现如下:
public interface MethodBeforeAdvice extends BeforeAdvice { /** * Callback before a given method is invoked. */ void before(Method method, Object[] args, @Nullable Object target) throws Throwable; }
使用前置通知需要实现这个接口:
public class BeforeAdvice implements MethodBeforeAdvice{ @Override public void before(Method method, Object[] args, Object target) throws Throwable { String methodName = method.getName(); String className = method.getDeclaringClass().getName(); System.out.println("准备执行方法:" + className + "." + methodName + "参数:" + Arrays.toString(args)); } }
写完通知类后需要配置通知:
spring-aop.xml:
<!--业务层对象--> <bean id="userService" class="com.qf.aop.service.UserServiceImpl"/> <!--配置通知对象--> <bean id="before" class="com.qf.aop.advice.BeforeAdvice"/>
当通知对象和业务层对象都纳入IOC容器管理之后,需要将通知对象作用在业务层对象上。Spring提供了aop标签来完成这一功能。
<!--aop配置--> <aop:config> <!-- pointcut表示切点,也就是通知会在哪些位置触发 expression表示切点表达式,切点表达式必须是execution(), execution()方法中的参数必须配置到方法上 比如 * com.qf.spring.aop.service..*(..) 第一个 * 表示任意访问修饰符 com.qf.spring.aop.service.. 最后的两个..表示service包下面的所有子包中的类 *(..) 表示任意方法, 如果()中没有..,则表示不带参数的方法;有,就表示带任意参数 --> <!--切入点配置--> <aop:pointcut id="pc" expression="execution(* com.qf.aop.service..*(..))"/> <!--通知配置--> <aop:advisor advice-ref="before" pointcut-ref="pc"/> </aop:config> </beans>
测试:
public class AopTest { @Test public void saveUserTest(){ ApplicationContext context = new ClassPathXmlApplicationContext("spring-aop.xml"); UserService userService = context.getBean("userService", UserService.class); HashMap<String, Object> map = new HashMap<>(); map.put("name","爱德华"); map.put("sex","男"); int i = userService.saveUser(map); } }
注:利用ClassPathXmlApplicationContext拿到配置文件上下文对象,进而拿到bean对象。
后置通知接口:AfterReturningAdvice.
剩下的流程和前置接口相同,编写通知类,配置通知类对象,配置通知。
<!--配置通知对象--> <bean id="before" class="com.qf.aop.advice.BeforeAdvice"/> <bean id="after" class="com.qf.aop.advice.AfterAdvice"/> <!--aop配置--> <aop:config> <!--切入点配置--> <aop:pointcut id="pc" expression="execution(* com.qf.aop.service..*(..))"/> <!--通知配置--> <aop:advisor advice-ref="before" pointcut-ref="pc"/> <aop:advisor advice-ref="after" pointcut-ref="pc"/> </aop:config> </beans>
异常抛出通知
异常抛出接口为ThrowsAdvice。
注意:异常通知类接口没有要重写的方法,而是自定义。
public class ExceptionAdvice implements ThrowsAdvice { public void afterThrowing(Method method, Object[] args, Object target, Exception ex){ String methodName = method.getName(); String className = method.getDeclaringClass().getName(); System.out.println("执行方法时:" + className + "." + methodName + ",参数:" + Arrays.toString(args) + ",发生了异常:" + ex.getMessage()); } }
配置和前面相同:
<bean id="exception" class="com.qf.aop.advice.ExceptionAdvice" /> <aop:advisor advice-ref="exception" pointcut-ref="pc"/>
环绕通知
接口:MethodInterceptor
注意:1.这里重写的方法参数为MethodInvocation invocation,可以通过invocation.getMethod();//获取被拦截的方法。
public class AroundAdvice implements MethodInterceptor { @Override public Object invoke(MethodInvocation invocation) throws Throwable { Method method = invocation.getMethod();//获取被拦截的方法对象 Object[] args = invocation.getArguments();//获取方法的参数 Object target = invocation.getThis();//获取代理对象 String methodName = method.getName(); String className = method.getDeclaringClass().getName(); System.out.println("准备执行方法:" + className + "." + methodName + ",参数:" + Arrays.toString(args)); Object returnVal = method.invoke(target, args); System.out.println("执行完方法:" + className + "." + methodName + ",参数:" + Arrays.toString(args) + ",得到返回值:" + returnVal); return returnVal; } }
环绕通知可以实现前置通知、后置通知、异常抛出通知的功能,所以配置文件中只需要配置环绕通知即可。
<!--业务层对象--> <bean id="userService" class="com.qf.aop.service.UserServiceImpl"/> <!--配置通知对象--> <bean id="around" class="com.qf.aop.advice.AroundAdvice"/> <!--aop配置--> <aop:config> <!--切入点配置--> <aop:pointcut id="pc" expression="execution(* com.qf.aop.service..*(..))"/> <!--通知配置--> <aop:advisor advice-ref="around" pointcut-ref="pc"/> </aop:config>
AspectJ
简介:
AspectJ是一个面向切面的框架,它扩展了Java语言,定义了AOP 语法,能够在编译期提供代码的织入。Spring通过集成AspectJ实现了以注解的方式定义增强类,大大减少了配置文件中的工作量
注解:
- @Aspect 切面标识
- @Pointcut 切入点
- @Before 前置通知
- @AfterReturning 后置通知
- @Around 环绕通知
- @AfterThrowing 异常抛出通知
通知类编写:
package com.qf.aop.advice; import org.aspectj.lang.JoinPoint; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.Signature; import org.aspectj.lang.annotation.*; import org.aspectj.lang.reflect.MethodSignature; import java.lang.reflect.Method; import java.util.Arrays; @Aspect public class AspectJAdvice { @Before(value = "execution(* com.qf.aop.service..*(..))") public void before(JoinPoint jp){ Signature signature = jp.getSignature();//获取签名 Object[] args = jp.getArgs();//获取方法参数 if(signature instanceof MethodSignature){ //如果签名是方法签名 Method method = ((MethodSignature) signature).getMethod();//获取方法 String methodName = method.getName(); String className = method.getDeclaringClass().getName(); System.out.println("准备执行方法:" + className + "." + methodName + ",参数:" + Arrays.toString(args)); } } @AfterReturning(value = "execution(* com.qf.aop.service..*(..))", returning = "returnValue") public void after(JoinPoint jp, Object returnValue){ Object[] args = jp.getArgs(); //获取方法参数 Signature signature = jp.getSignature(); //获取签名 if(signature instanceof MethodSignature){ //如果签名是方法签名 Method method = ((MethodSignature) signature).getMethod(); //获取方法 String methodName = method.getName(); String className = method.getDeclaringClass().getName(); System.out.println("执行完方法:" + className + "." + methodName + ",参数:" + Arrays.toString(args) + ",得到返回值:" + returnValue); } } @AfterThrowing(value = "execution(* com.qf.aop.service..*(..))", throwing = "t") public void exception(JoinPoint jp, Throwable t){ Object[] args = jp.getArgs(); //获取方法参数 Signature signature = jp.getSignature(); //获取签名 if(signature instanceof MethodSignature){ //如果签名是方法签名 Method method = ((MethodSignature) signature).getMethod(); //获取方法 String methodName = method.getName(); String className = method.getDeclaringClass().getName(); System.out.println("执行方法时:" + className + "." + methodName + ",参数:" + Arrays.toString(args) + ",发生了异常:" + t.getMessage()); } } @Around("execution(* com.qf.aop.service..*(..))") public Object around(ProceedingJoinPoint pjp) throws Throwable { Object[] args = pjp.getArgs();//获取方法的参数 Object target = pjp.getTarget(); //获取代理对象 Signature signature = pjp.getSignature(); //获取签名 if(signature instanceof MethodSignature) { //如果签名是方法签名 Method method = ((MethodSignature) signature).getMethod(); //获取被拦截的方法对象 String methodName = method.getName(); String className = method.getDeclaringClass().getName(); try { System.out.println("准备执行方法:" + className + "." + methodName + ",参数:" + Arrays.toString(args)); Object returnValue = method.invoke(target, args); System.out.println("执行完方法:" + className + "." + methodName + ",参数:" + Arrays.toString(args) + ",得到返回值:" + returnValue); return returnValue; } catch (Throwable t){ System.out.println("执行方法时:" + className + "." + methodName + ",参数:" + Arrays.toString(args) + ",发生了异常:" + t.getMessage()); throw t; } } return null; } }
启用注解支持:
<!--配置通知对象--> <bean class="com.qf.aop.advice.AspectJAdvice"/> <!--启动AspectJ注解 自动为类生成代理--> <aop:aspectj-autoproxy proxy-target-class="true"/>
SpringMVC
简介
1. Spring MVC
SpringMVC是一个Java 开源框架, 是Spring Framework生态中的一个独立模块,它基于 Spring 实现了Web MVC(数据、业务与展现)设计模式的请求驱动类型的轻量级Web框架,为简化日常开发,提供了很大便利。
2. Spring MVC 核心组件
DispatcherServlet 前置控制器
负责接收请求、分发请求
Handler 处理器
处理器包括了拦截器、控制器中的方法等,主要负责处理请求
HandlerMapping 处理器映射器
解析配置文件、扫描注解,将请求与处理器进行匹配
HandlerAdpter 处理器适配器
根据请求来找到匹配的处理器,这个过程称为适配
ViewResolver 视图解析器
处理器执行后得到的结果可能是一个视图,但这个视图属于逻辑视图(页面中存在逻辑代码,比如循环、判断),需要使用视图解器行处理,这个过程称为渲染视图
Spring MVC工作原理
mvc工作原理
前端发送的请求由DispatcherServlet接收到,然后根据提供的HandlerMapping来分发过去,在分发请求过程中使用到HandlerAdapter来适配处理器(因为处理的类型无法确定),找到对应的处理器适配器之后就会执行这个处理器,执行会得到一个ModelAndView,然后交给ViewResolver进行解析得到视图位置,然后对视图进行渲染,渲染完成后将渲染好的视图交给DispatcherServlet,然后传回前端展示。
Spring MVC发展演变
1.Bean的名字或ID匹配URL请求
由于版本更新,使用新版本会无法完成过时的功能,但是为了更好地理解演变的过程,这里使用低版本:
<!--低版本--> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> <version>4.3.9.RELEASE</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-webmvc</artifactId> <version>4.3.9.RELEASE</version> </dependency> <dependency> <groupId>javax.servlet</groupId> <artifactId>javax.servlet-api</artifactId> <version>4.0.1</version> <scope>provided</scope> </dependency>
首先需要再web.xml配置文件中配置DispatcherServlet,包括初始化参数(全局上下文,自己项目的配置文件路径以及使得项目启动时就创建servlet的初始化参数)
<servlet> <servlet-name>dispatcherServlet</servlet-name> <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class> <init-param> <param-name>contextConfigLocation</param-name> <param-value>classpath:spring-mvc.xml</param-value> </init-param> <load-on-startup>1</load-on-startup> </servlet> <servlet-mapping> <servlet-name>dispatcherServlet</servlet-name> <url-pattern>/</url-pattern> </servlet-mapping>
然后是自己写的spring-mvc.xml配置,这里需要写视图解析器、处理器映射器(处理器适配器采用默认的,在底层mvc框架中会根据处理器类型寻找合适的处理器适配器)。具体的逻辑为前端发送请求->处理器映射方式、配置控制器找到控制器->控制器返回modelandview->视图解析器解析路径找到jsp文件:
<!--视图解析器--> <bean class="org.springframework.web.servlet.view.InternalResourceViewResolver"> <property name="prefix" value="/"/> <property name="suffix" value=".jsp"/> </bean> <!--处理器映射的方式:使用bean的名字或者id的值来与请求匹配--> <bean class="org.springframework.web.servlet.handler.BeanNameUrlHandlerMapping"/> <!--通过id值匹配请求的URL--> <bean id="/user" class="com.qf.controller.UserController"/>
处理器映射器给出映射的方式:使用bean的名字或者id,然后DispatcherServlet找到处理器适配器,处理器适配器提供id和处理器的路径。然后底层会根据路径找到处理器。
处理器:
public class UserController extends AbstractController { @Override protected ModelAndView handleRequestInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse) throws Exception { return new ModelAndView("user"); } }
处理器返回ModelAndView给适配器,DispatcherServlet根据id拿到返回的ModelAndView,并交给视图解析器进行处理,最终将处理好的路径进行渲染。
但是这样做有一个问题,每一个请求都需要一个控制器与之对应,如果有很多请求,那么就要写很多个控制器。开发效率极为低下,而Spring提供了方法名匹配请求来解决这个问题。
2.Bean方法名匹配请求
方法名解析器:InternalPathMethodNameResolver,将方法名作为匹配URL请求的依据,与控制器关联起来。
这样一来请求就可以直接与控制器中的方法关联,那么控制器中的方法就应该有多个。
多操作控制器:
MultiActionController控制器类,供其他控制器类继承,在其子类中可以编写多个处理请求的方法,然后使用方法名解析器去匹配请求。
控制器:
public class UserMultiController extends MultiActionController { //这个方法匹配/login请求 public ModelAndView login(HttpServletRequest request, HttpServletResponse response){ return new ModelAndView("login"); } //这个方法匹配/register请求 public ModelAndView register(HttpServletRequest request,HttpServletResponse response){ return new ModelAndView("register"); } }
编写完控制器后需要写相应的控制器映射器(视图解析器不变):
spring-mvc.xml:
<!--方法名解析器,处理映射的方式:使用方法名--> <bean id="methodNameResolver" class="org.springframework.web.servlet.mvc.multiaction.InternalPathMethodNameResolver"/> <!--/login 请求使用该bean对象处理--> <bean id="/login" class="com.qf.controller.UserMultiController"> <property name="methodNameResolver" ref="methodNameResolver"/> </bean> <!--/register 请求使用该bean对象处理--> <bean id="/register" class="com.qf.controller.UserMultiController"> <property name="methodNameResolver" ref="methodNameResolver"/> </bean>
按照这种匹配请求的方式,如果一个控制器要处理多个请求,就会导致此配置文件无限扩展,变得冗杂,后期难以维护,这时如何解决?
Spring提供了SimpleUrlHandlerMapping映射器,该映射器支持一个控制器与多个请求匹配的同时也解决了配置信息繁多的问题。
3.简单URL处理器映射
在Bean方法名匹配请求方式的控制器不变的基础上,只需要改动控制器映射器即可:
spring-mvc.xml:
<!--使用简单URL处理器映射--> <bean class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping"> <property name="mappings"> <props> <prop key="/view">userController</prop> <prop key="/user/*">userMultiController</prop> </props> </property> </bean> <bean id="userController" class="com.qf.controller.UserController"/> <bean id="userMultiController" class="com.qf.controller.UserMultiController"/>
随着业务的增加,控制器的数量也为增加,请求的匹配也会增多,xml文件里虽然减少了冗余,但每次增加方法也会增加代码量,如何解决?
-Spring提供了DefaultAnnotationHandlerMapping映射器,支持使用注解来匹配请求,这样就解决了请求匹配导致配置信息繁多的问题,同时还提升了开发效率。
注解匹配请求
控制器中通过@Controller注解说明这是一个处理器,方法中通过@RequestMapping注解注明映射信息。
controller:
@Controller public class UserAnnotationController { @RequestMapping(value = "/login",method = RequestMethod.GET) public String login(){ return "login"; } @RequestMapping(value = "register",method = RequestMethod.GET) public String register(){ return "register"; } }
注意:这次的controller不需要实现接口或者继承抽象类了,也就意味着可以自定义方法,只需要在方法上加注解就可以达到映射的效果。
写好处理器后需要配置(视图解析器还是不用变,只需要改变处理器映射器就行):
spring-mvc.xml:<!--类上的注解处理器--> <bean class="org.springframework.web.servlet.mvc.annotation.DefaultAnnotationHandlerMapping"/> <!--方法上的注解处理器--> <bean class="org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter"> <!--扫描包,使得该包下类以及类中定义的方法上所使用的注解生效--> <context:component-scan base-package="com.qf.controller" />
新的版本配置
<!--较新的版本使用该标签启动注解支持--> <mvc:annotation-driven/> <!--扫描包,使类和类方法注解生效--> <context:component-scan base-package="com.qf.controller"/>
相当于使用一句话代替了原来对类和方法上注解处理器的声明。
Spring MVC常用注解
@Controller
控制器的标识
@Controller public class UserController{ }
@RequestMapping
该注解用于匹配请求(注明URI)
@Controller @RequestMapping("/user") public class UserController{ @RequestMapping(value="/login", method=RequestMethod.POST) public int login(){ return 1; } }
@RequestBody
该方法只能用在方法的参数上,用于从请求体中获取数据并注入参数中,并且获取的数据只能是JSON格式的数据。
@Controller @RequestMapping("/user") public class UserController{ @RequestMapping(value="/login", method=RequestMethod.POST) public int login(@RequestBody User user){ return 1; } }
@ResponseBody
该注解用于向页面传递数据,如果没有该注解,那么controller方法中返回的任何数据都会被认为是一个页面字符串。
@Controller @RequestMapping("/user") public class UserController{ @RequestMapping(value="/login", method=RequestMethod.POST) @ResponseBody public int login(@RequestBody User user){ return 1; } }
@RequestParam
该注解只能用在方法的参数上, 用于从 URL 查询字符串或表单参数中提取参数。
@Controller @RequestMapping("/user") public class UserController{ @RequestMapping(value="/search", method=RequestMethod.GET) @ResponseBody public List<User> searchUsers(@RequestParam(value="name") String name){ return new ArrayList<>(); } }
注意:@RequestParam和@PathVariable的区别:
@PathVariable:
- 用于从 URL 路径中提取参数。
- 例如:提取
http://example.com/user/john
中的john
。- 用于 RESTful 风格的 URL。
@RequestParam:
- 用于从 URL 查询字符串或表单参数中提取参数。
- 例如:提取
http://example.com/user/search?name=john
中的name
,或提取表单提交的数据。- 适用于查询字符串参数和表单参数。
@PathVariable
该注解只能应用在方法的参数上,用于从请求路径中获取数据并注入至参数中
@Controller @RequestMapping("/user") public class UserController{ // /user/admin @RequestMapping(value="/{username}", method=RequestMethod.GET) @ResponseBody public User queryUser(@PathVariable("username") String username){ return new User(); } }
注意: 花括号
{}
用于定义路径变量,表示 URL 中的动态部分,这些部分将被提取并传递给控制器方法的参数。 前端在发送请求时,必须用具体的username
替换路径变量 。
@RequestHeader
该注解只能应用在方法的参数上,用于从请求头中获取数据
@RequestMapping("/find") public void findUsers(@RequestHeader("Content-Type") String contentType) {//从请求头中获取Content-Type的值 }
@CookieValue
该注解只能应用在方法的参数上,用于从请求中获取cookie的值
@RequestMapping("/find") public void findUsers(@CookieValue("JSESSIONID") String jsessionId) {//从请cookie中获取jsessionId的值 }
@ControllerAdvice
该注解只能应用在类上,表示这个类就是处理异常的控制器
/** * 异常处理的控制器 */ @ControllerAdvice //这个注解就是spring mvc提供出来做全局异常统一处理的 public class ExceptionController { }
@ExceptionHandler
该注解只能应用在@ControllerAdvice或者@RestControllerAdvice标识的类的方法上用来处理异常
/** * 异常处理的控制器 */ @ControllerAdvice //这个注解就是spring mvc提供出来做全局异常统一处理的 public class ExceptionController { @ExceptionHandler //异常处理器 @ResponseBody //响应至页面 public String handleException(Exception e){ return e.getMessage(); } }
Spring 对 RESTFUL的支持
@RestController
相当于@Controller 和 @ResponseBody 注解的组合。表示该类中的所有方法执行完成后所返回的结果直接向页面输出。
@GetMapping
@PostMapping
@PutMapping
@DeleteMapping
静态资源处理
静态资源无法访问的原因
静态资源包含html、js、css、图片、字体文件等。静态文件没有url-pattern,所以默认是无法访问的。之所以可以访问,是因为tomcat中有一个全局的servlet:org.apache.catalina.servlets.DefaultServlet,它的url-pattern是 “/”, 所以项目中不能匹配的静态资源请求,都由这个Servlet来处理。但在SpringMVC中DispatcherServlet也采用了"/" 作为url-pattern, 那么项目中不会再使用全局的Serlvet,这样就造成了静态资源不能完成访问。
处理方案
方案一:修改DispatcherServlet对应的url-pattern修改为"/"以外的其他匹配样式。
方案二(建议):将所有的静态资源放进一个static包中,如果需要访问,则将defaultServlet的url-pattern的url-mapping改为/static/*
<!-- web.xml --> <servlet-mapping> <servlet-name>default</servlet-name> <url-pattern>/static/*</url-pattern> </servlet-mapping>
方案三:利用default-servlet-handler 将处理静态资源的请求转发给容器的默认 Servlet ,而不是给DispatcherServlet。
<!-- spring-mvc.xml --> <!-- 这个handler就是处理静态资源的,它的处理方式就是将请求转会到tomcat中名为default的Servlet --> <mvc:default-servlet-handler/> <!-- mapping是访问路径,location是静态资源存放的路径 --> <mvc:resources mapping="/static/**" location="/static/" />
中文乱码处理
在web.xml中配置字符编码过滤器CharacterEncodingFilter
<filter> <filter-name>encodingFilter</filter-name> <!--字符编码过滤器--> <filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class> <init-param> <!--编码格式--> <param-name>encoding</param-name> <param-value>UTF-8</param-value> </init-param> <init-param> <!--强制编码--> <param-name>forceEncoding</param-name> <param-value>true</param-value> </init-param> </filter> <filter-mapping> <filter-name>encodingFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping>