Aop的总结:
1.aop是什么?
aop是面向切面编程,和loc是spring的核心。通过预编译方式和动态代理实现程序维护的技术。简而言之,就是将重复的代码集中统一管理。下面例子中:add方法和select方法存在重复程序:
public void add(){
//记录程序执行 开始时间
long begin = System.nanoTime();
//执行业务逻辑
log.info("添加...");
//录程序执行 结束时间
long end = System.nanoTime();
//记录程序执行耗时
log.info("执行耗时{} ns",(end - begin));
}
public String select(){
long begin = System.nanoTime();
log.info("查询...");
long end = System.nanoTime();
log.info("执行耗时{} ns",(end - begin));
return "user";
}
重复程序有:
//记录程序执行 开始时间
long begin = System.nanoTime();
//调用原始操作
//录程序执行 结束时间
long end = System.nanoTime();
//记录程序执行耗时
log.info("执行耗时{} ns",(end - begin));
2.aop的步骤:
2.1.把依赖导入 2.2定义类,定义方法无返回值,抽取公共代码。(例如:记录执行的开始时间、代用原始操作、记录程序结束时间、记录程序耗时多少)
1.导依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
2.定义类抽取公共代码(执行耗时统计操作)
public class TimeAspect {
public void recordTime() {
//记录程序执行 开始时间
long begin = System.nanoTime();
//调用原始操作
//录程序执行 结束时间
long end = System.nanoTime();
//记录程序执行耗时
log.info("执行耗时{} ns",(end - begin));
}
}
3.标识当前类是一个AOP类,并被Spring容器管理(@Aspect` 标识当前类是一个AOP类)
@Slf4j
@Component
@Aspect
public class TimeAspect {
@Around("execution(* com.itheima.service.impl.UserServiceImpl.*(..))")
public Object recodeTime(ProceedingJoinPoint joinPoint) throws Throwable {
//开始执行时间
long begin = System.nanoTime();
//目标对象
Object proceed = joinPoint.proceed();
//结束执行时间
long end = System.nanoTime();
//时间差
log.info("共耗时:{}",(end-begin));
return proceed;
}
}
4.执行目标方法
public class TimeAspect {
@Around("execution(* com.itheima.service.impl.UserServiceImpl.*(..))")
public Object recordTime(ProceedingJoinPoint joinPoint) throws Throwable {
//记录程序执行 开始时间
long begin = System.nanoTime();
//调用原始操作
Object proceed = joinPoint.proceed();
//录程序执行 结束时间
long end = System.nanoTime();
//记录程序执行耗时
log.info("执行耗时{} ns",(end - begin));
return proceed;
}
}
5.测试运行
@SpringBootTest
class AopDemoApplicationTests {
@Autowired
private UserService userService;
@Test
void testAdd(){
userService.add();
}
}
2.3定义一个aop类,而通过@Aspect标识当前类是一个aop类。而@Component是把该类交给spring容器管理
3.通知类型:
而在配置公共代码的方法时会用到的注解有@Around、@Before、@After、@AfterReturning、@AfterThrowing、@Annotation
- @Before - 此注解标注的通知方法在目标方法`前`被执行
- @After - 此注解标注的通知方法在目标方法`后`被执行,`无论是否有异常`
- @AfterReturning - 此注解标注的通知方法在目标方法`后`被执行,`有异常不会执行`
- @AfterThrowing - 此注解标注的通知方法发生`异常后`执行
4.通知顺序:
4.1是按照类的字母顺序进行排序(不推荐)4.2是按照@Order(数字)
4.2.1:前置通知:执行顺序是从小到大,小的先执行。最终执行通知:从大到小:大的先执行
/**
* 通知类型测试
*/
@Aspect
@Component
@Slf4j
public class AdviceType {
// @Before 在目标方法执行前执行
@Before("execution(* com.itheima.service.impl.UserServiceImpl.update(..))")
public void BeforeAdvice(){
log.info("before...");
}
// @After 在目标方法执行后执行,且无论是否出现异常
@After("execution(* com.itheima.service.impl.UserServiceImpl.update(..))")
public void AfterAdvice(){
log.info("After...");
}
// 在目标方法执行后执行,如果出现异常,则不执行
@AfterReturning("execution(* com.itheima.service.impl.UserServiceImpl.update(..))")
public void AfterReturningAdvice(){
log.info("AfterReturning...");
}
// 目标方法出现异常,执行
@AfterThrowing("execution(* com.itheima.service.impl.UserServiceImpl.update(..))")
public void AfterThrowingAdvice(){
log.info("AfterThrowing...");
}
// 环绕通知,目标方法执行前执行-->目标方法执行 --> 目标方法执行后执行
@Around("execution(* com.itheima.service.impl.UserServiceImpl.update(..))")
public Object AroundAdvice(ProceedingJoinPoint joinPoint) throws Throwable {
log.info("Around 前...");
Object proceed = joinPoint.proceed();
log.info("Around 后...");
return proceed;
}
}
5.切点表达式:
就三个关键字:Execution、@Annotation、Args 特殊符号;*:表示:任意的方法名、包名、类名等等 (…):表示任意参数
@Slf4j
@Component
@Aspect
public class PointCutExpress {
@Before("execution(* com.itheima.service.impl.UserServiceImpl.select*(..))")
public void before(){
log.info("before");
}
}
- execution(返回值类型 包名.类名.方法名(参数类型))`
- `*` 可以通配任意返回值类型、包名、类名、方法名、或任意类型的一个参数
- `..` 可以通配任意层级的包、或任意类型、任意个数的参数
- `@annotation()` 根据注解匹配
- `args()` 根据方法参数匹配
6.什么是连接点?
就相当于目标方法,spring使用它来代替方法名和方法参数类型、方法的实际参数
特别是对@Around注解,他是用的是JoinPoint的子类型:ProceedingJoinPoint,其他四种通知类型只能使用父类型
7.cglib和jdk的区别:
首先至今默认的是cglib,用法是在配置文件添加Spring.aop.proxy.target.class=true,接着就true/false而言,true是CGLIB,而false是jdk
在jdk下又分为目标实现有接口,还是没有接口。有接口则会生成jdk动态代理,没有接口默认还是CGLIB
8.案例:就是监控用户对数据进行增删改等方面没有规律的操作使用aop技术统一操作
自定义注解:
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@Documented
public @interface Log {
}
9.aop技术使用:
1.自定义一个注解:具体的忘了不过,先加一个target(),其中括号里面写的是…method.然后其他的使用ctrl看Target底层代码,里面类的注解和target在一起的就是了
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
然后在ControllerServiceImpl中的增删改等能影响数据的操作添加自定义注解,作用是相当于标识的作用
2.数据库创建日志表:字段名就是操作数据用户的相关信息
例如:用户名、访问类名.方法名、请求参数、创建时间、执行时长
CREATE TABLE `sys_log` (
`id` int primary key AUTO_INCREMENT,
`username` varchar(50) COMMENT '用户名',
`method` varchar(200) COMMENT '访问方法名',
`params` varchar(5000) COMMENT '请求参数',
`time` int NOT NULL COMMENT '执行时长(毫秒)',
`create_time` datetime COMMENT '创建时间'
);
3.在创建日志实体类。
@Data
public class SysLog {
private Integer id;
//用户名
private String username;
//访问类名.方法名
private String method;
//请求参数
private String params;
//执行时长(毫秒)
private Integer time;
//创建时间
private LocalDateTime createTime;
}
4.编写日志操作mapper
@Mapper
public interface SysLogMapper {
@Insert("insert into sys_log (username, method, params, time, create_time) values ( #{username},\n" +
" #{method},\n" +
" #{params},\n" +
" #{time},\n" +
" #{createTime})")
void save(SysLog sysLog);
}
5.编写操作日志的接口service以及serviceImpl类,同时serviceImpl实现该接口
接口中的方法和mapper的方法一致,可直接复制。接着在IMPL中重写方法
接口类
public interface SysLogService {
void add(SysLog sysLog);
}
接口的实现类
@Service
public class SysLogServiceImpl implements SysLogService {
@Autowired
private SysLogMapper sysLogMapper;
@Override
public void add(SysLog sysLog) {
sysLogMapper.add(sysLog);
}
}
6.开始编写日志切面类
首先在类上添加可以标志该类是aop类的注解
@Aspect
以及将其交给spring管理的注解
@Component
接者注入接口,这里的注解是@Autowired
接着就编写执行目标方法
操作流程:1.操作人2.执行时长3.执行方法全类名4.执行方法参数5.操作时间6.最后将数据保存到数据库
1.创建日志对象
2.操作时间
3.执行时长
4.开始时间、执行目标方法、结束时间、时长计算
5.执行方法全类名:分别从获取方法名和类名操作=>方法名使用到的方法有:参数名.getSignature().getName()
类名涉及方法:参数名.getTarget().getClass().getName()
最后是用mapper中方法的参数的setMethod方法,将方法名和类名进行拼接
6.执行方法参数:也是用方法和参数两个方面考虑:首先使用参数名中的获取参数方法(getArgs),返回一个数组,然后导入依赖hutool工具,接着在使用json的toJsonString方法
返回json合适的字符串。
然后使用mapper下方法的参数调用设置任意参数的方法setParams。将上面返回的字符串放入其中
6.操作人登陆成功后,数据存储到session,需要从session中获取:
ServletRequestAttributes attr = (ServletRequestAttributes) RequestContextHolder.currentRequestAttributes();
HttpSession session=attr.getRequest().getSession(true);
User user = (User) session.getAttribute(“loginUserSuccess”);
sysLog.setUsername(user.getUsername());
注意点:这里的loginUserSuccess必须和userController中的登陆成功标志(session.setAttribute(“loginUserSuccess”,u))一致
7.调用方法存储数据到数据库:
使用注入的变量名调用mapper下的方法。
例如: sysLogService.add(sysLog);
具体代码如下:
@Component
@Aspect
public class LogAspect {
@Autowired
private SysLogService sysLogService;
@Around("@annotation(com.itheima.anno.Log)")
public Object rol(ProceedingJoinPoint joinPoint) throws Throwable {
//1.操作日志对象
SysLog sysLog = new SysLog();
//2.操作时间
sysLog.setCreateTime(LocalDateTime.now());
//2.1方法执行时长
long begin = System.currentTimeMillis();
//执行目标方法
Object proceed = joinPoint.proceed();
long end = System.currentTimeMillis();
//时间差
sysLog.setTime((int) (end-begin));
//执行类名.方法名
//获取方法名
String name = joinPoint.getSignature().getName();
//获取类名
String className = joinPoint.getTarget().getClass().getName();
sysLog.setMethod(className+"."+name);
//执行方法参数
//获取参数
Object[] args = joinPoint.getArgs();
//将其转为json格式
String jsonString = JSON.toJSONString(args);
//String toString = Arrays.toString(args);
sysLog.setParams(jsonString);
//操作人登陆成功后将用户名,登录用户存储在session
ServletRequestAttributes attr = (ServletRequestAttributes) RequestContextHolder.currentRequestAttributes();
HttpSession session=attr.getRequest().getSession(true);
User user = (User) session.getAttribute("loginUserSuccess");
sysLog.setUsername(user.getUsername());
//保存到数据库
sysLogService.add(sysLog);
return proceed;
}
}
此外在使用session之前建议有登录代码:
第一个:用户登录的controller
@RestController
@RequestMapping("/users")
public class UserController {
@Autowired
private UserService userService;
@PostMapping("/login")
public R login(@RequestBody User user, HttpSession session){
User u = userService.login(user.getUsername(), user.getPassword());
if (u==null){
return R.error("用户名或密码输入错误");
}else{
//登陆成功
//存储登陆成功标记
session.setAttribute("loginUserSuccess",u);
return R.success();
}
}
}
第二个:拦截器代码:
package com.itheima.filter;
import com.alibaba.fastjson.JSON;
import com.itheima.domain.R;
import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;
import java.io.IOException;
/**
* 登录校验
* 1.判断标记是否存在
* 2.从session中取出来
* 2.
* 存在。放行
* 不存在。则拦截。跳转到登录界面
*/
@WebFilter("/*")
public class LoginFilter implements Filter {
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
/**
*判断登录标记是否存在:session=> servletRequest(有的是request),而因为这里的类型是ServletRequest,所以要进行强转为HttpServletRequest
*/
HttpServletRequest hsr = (HttpServletRequest)servletRequest;
HttpSession session = hsr.getSession();
Object loginUserSuccess = session.getAttribute("loginUserSuccess");
//放行对应资源文件:前端资源(html、css、js、image)、登录接口、注册接口、
//定义换一个目标数组,把放心的
//判断请求路径,是否包含request.js
String[] ignoreUrls={".html",".css",".js","/img","/element-ui","/login"};
//建个循环
String url = hsr.getRequestURI();
for (String ignoreUrl : ignoreUrls) {
if (url.contains(ignoreUrl)){
//放行
filterChain.doFilter(servletRequest,servletResponse);
return;
}
}
if (loginUserSuccess!=null){
//登录成功,放行
filterChain.doFilter(servletRequest,servletResponse);
//不想让其继续往下执行,所以使用return,结束改方法
return;
}
//如果用户没有登录成功,就跳转回登录界面
//方法一:response,此方法不推荐,因为不知道跳转界面的地址
/* HttpServletRequest resp=(HttpServletRequest) servletResponse;
resp.senndRedirect("");*/
//方法二:返回R对象
R r = R.error("NO LOGIN");
//只能通过手动将R转为json,返回
//使用json工具的toJSONString方法,将R对象转为JSON字符串
String jsonString = JSON.toJSONString(r);
//返回数据:responce有一个方法:getWriter=》writer
servletResponse.getWriter().write(jsonString);
}
}