Spring-----AOP面向切面编程---实现不同框架下的登录日志

一.Spring的两大核心

 spring的两大核心概念

IOC:另外一种说法叫DI(Dependency Injection),即依赖注入。IOC将这些相互依赖对象的创建、协调工作交给Spring容器去处理

AOP:即面向切面编程

1.AOP的概念

在软件业,AOP为Aspect Oriented Programming的缩写,意为:面向切面编程,通过预编译方式和运行期间动态代理实现程序功能的统一维护的一种技术。AOP是OOP的延续,是软件开发中的一个热点,也是Spring框架中的一个重要内容,是函数式编程的一种衍生范型。利用AOP可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率。

AOP:将核心业务代码和非核心业务代码分离,如果我们写的核心业务代码需要非核心业务功能时,在不影响核心业务代码的前提下,建立切面进行功能添加 

基础代码: 

service接口 

package com.wt.before;

/**
 * @Author wt
 * @Date 2022/7/6 16:23
 * @PackageName:com.wt.proxy
 * @ClassName: MathService
 * @Description: 
 * @Version 1.0
 */
public interface MathService {
    /**
     *加法运算
     * @param a
     * @param b
     * @return
     */
    public double add(double a,double b);

    /**
     * 减法运算
     * @param a
     * @param b
     * @return
     */
    public double sub(double a,double b);

    /**
     * 惩罚运算
     * @param a
     * @param b
     * @return
     */
    public double mul(double a,double b);

    /**
     * 除法运算
     * @param a
     * @param b
     * @return
     */
    public double div(double a,double b);
}

service的实现类 

package com.wt.before;

/**
 * @Author wt
 * @Date 2022/7/6 16:26
 * @PackageName:com.wt.proxy
 * @ClassName: MathServiceImpl
 * @Description: TODO
 * @Version 1.0
 * 思考: 随着业务的不断扩展:
 *    ① 日志功能。如果日志代码修改: 以AAA开始 感觉:如果有1w方法都需要添加日志,修改1W方法的代码
 *    ② 校验功能:如果1w个方法需要校验,则需修改1w个方法的源代码。
 *  
 *   有没有好的解决方案:
 *     (1)抽取个方法。缺点还需要再1w方法的地方调用。
 *     (2)动态代理: 实现方式有两种: [1]JDK原生动态代理 [2]cglib动态代理
 *     (3)AOP面向切面编程:AOP的底层实现就是基于动态代理。
 */
public class MathServiceImpl implements MathService {
    @Override
    public double add(double a, double b) {
        //功能运行前输出
        System.out.println("before++++++++++++");
        double result = a+b;
        //功能运行后输出
        System.out.println("after-----------");
        return result;
    }

    @Override
    public double sub(double a, double b) {
        System.out.println("before++++++++++++");
        double result = a+b;
        System.out.println("after-----------");
        return result;
    }

    @Override
    public double mul(double a, double b) {
        System.out.println("before++++++++++++");
        double result = a*b;
        System.out.println("after-----------");
        return result;
    }

    @Override
    public double div(double a, double b) {
        System.out.println("before++++++++++++");
        double result = a/b;
        System.out.println("after-----------");
        return result;
    }
}

思考: 随着业务的不断扩展:
    ① 日志功能。如果日志代码修改: 以before开始 感觉:如果有1w方法都需要添加日志,修改1W方法的代码
    ② 校验功能:如果1w个方法需要校验,则需修改1w个方法的源代码。
  
   有没有好的解决方案:
       (1)抽取个方法。缺点还需要再1w方法的地方调用。
       (2)动态代理: 实现方式有两种: [1]JDK原生动态代理 [2]cglib动态代理
       (3)AOP面向切面编程:AOP的底层实现就是基于动态代理

2.切面工厂的使用

(1) jdk原生动态代理 

Spring-----AOP面向切面编程---实现不同框架下的登录日志_第1张图片

MathServie接口

package com.wt.jdk;

/**
 * @Author wt
 * @Date 2022/7/6 16:23
 * @PackageName:com.wt.jdk
 * @ClassName: MathService
 * @Description: TODO
 * @Version 1.0
 */
public interface MathService {
    /**
     *加法运算
     * @param a
     * @param b
     * @return
     */
    public double add(double a,double b);

    /**
     * 减法运算
     * @param a
     * @param b
     * @return
     */
    public double sub(double a,double b);

    /**
     * 惩罚运算
     * @param a
     * @param b
     * @return
     */
    public double mul(double a,double b);

    /**
     * 除法运算
     * @param a
     * @param b
     * @return
     */
    public double div(double a,double b);
}

MathServiceImpl接口实现类 

package com.wt.proxy;

/**
 * @Author wt
 * @Date 2022/7/6 16:26
 * @PackageName:com.wt.proxy
 * @ClassName: MathServiceImpl
 * @Description: TODO
 * @Version 1.0
 */
public class MathServiceImpl implements MathService{

    public double add(double a, double b) {
        double result = a+b;
        return result;
    }


    public double sub(double a, double b) {
        double result = a-b;
        return result;
    }


    public double mul(double a, double b) {
        double result = a*b;
        return result;
    }


    public double div(double a, double b) {
        double result = a/b;
        return result;
    }
}

建立代理工厂ProxyFactory实现切面编程 

method:表示代理对象要代理的方法
invoke:回调该方法
args:方法需要的参数

类加载器

Spring-----AOP面向切面编程---实现不同框架下的登录日志_第2张图片

package com.wt.jdk;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.Arrays;

/**
 * @Author wt
 * @Date 2022/7/6 16:28
 * @PackageName:com.wt.jdk
 * @ClassName: ProxyFactory
 * @Description: 代理实现类
 * @Version 1.0
 */
public class ProxyFactory {
    //被代理对象
    private Object target;

    //生成构造函数
    public ProxyFactory(Object target) {
        this.target = target;
    }

    //获取代理对象
    public Object getProxy(){
        /**
         * ClassLoader loader, 被代理对象的类加载器
         * Class[] interfaces, 被代理对象实现的接口
         * InvocationHandler h: 当代理对象执行被代理的方法时,会触发该对象中的invoke功能
         */
        ClassLoader loader = target.getClass().getClassLoader();
        Class[] interfaces = target.getClass().getInterfaces();
//        方法一
//        InvocationHandler h = (proxy, method, args) -> {
//            System.out.println("befor===============");
//            Object result = method.invoke(target, args);
//            System.out.println("after");
//            return result;
//        };

//        方法二
        InvocationHandler h = new InvocationHandler() {
            @Override
            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                System.out.println("befor======"+ method.getName()+"========="+Arrays.asList(args));
                //System.out.println(method);
                //System.out.println(proxy);
                //System.out.println(Arrays.toString(args));
                Object result = method.invoke(target, args);
                System.out.println("after:"+result);
                return result;
            }
        };
        Object o = Proxy.newProxyInstance(loader, interfaces, h);
        return o;

    }
}

建立测试类

package com.wt.jdk;

/**
 * @Author wt
 * @Date 2022/7/6 18:06
 * @PackageName:com.wt.jdk
 * @ClassName: Test
 * @Description: TODO
 * @Version 1.0
 */
public class Test {
    public static void main(String[] args) {
        
       //被代理对象
        MathServiceImpl mathService = new MathServiceImpl();
        //创建代理工厂对象
        ProxyFactory proxyFactory = new ProxyFactory(mathService);
        //获取代理对象
        MathService proxy = (MathService) proxyFactory.getProxy();
        //执行相应地业务代码(调用不同的方法)
        double add = proxy.div(15, 5);
        System.out.println(add);
    }
}

 Spring-----AOP面向切面编程---实现不同框架下的登录日志_第3张图片

JDK原生代理的缺点 : 必须基于接口实现

 (2) cglib

(1)引入cglib的jar包,由于Spring的核心包中已经包括了Cglib功能,所以也可以直接引入spring-core-3.2.5.jar
 (2)创建一个代理类工厂并实现接口MethodInterceptor
(3)目标类不能为final

a.引入jar包

        
            cglib
            cglib
            3.2.5
        

MathServiceImpl类

package com.wt.cglib;

/**
 * @Author wt
 * @Date 2022/7/6 16:26
 * @PackageName:com.wt.proxy
 * @ClassName: MathServiceImpl
 * @Description: TODO
 * @Version 1.0
 */
public class MathServiceImpl {

    public double add(double a, double b) {
        double result = a+b;
        return result;
    }


    public double sub(double a, double b) {
        double result = a-b;
        return result;
    }


    public double mul(double a, double b) {
        double result = a*b;
        return result;
    }


    public double div(double a, double b) {
        double result = a/b;
        return result;
    }
}

b.创建代理工厂类并实现 MethodInterceptor接口 

package com.wt.cglib;

//import net.sf.cglib.proxy.Enhancer;
//import net.sf.cglib.proxy.MethodInterceptor;
//import net.sf.cglib.proxy.MethodProxy;


import org.springframework.cglib.proxy.Enhancer;
import org.springframework.cglib.proxy.MethodInterceptor;
import org.springframework.cglib.proxy.MethodProxy;

import java.lang.reflect.Method;
import java.util.Arrays;

/**
 * @Author wt
 * @Date 2022/7/7 10:17
 * @PackageName:com.wt.cglib
 * @ClassName: ProxyFactory
 * @Description: TODO
 * @Version 1.0
 */
public class ProxyFactory implements MethodInterceptor {
    private Object target;

    public ProxyFactory(Object target) {
        this.target = target;
    }

    //获取代理对象,给目标对象创建一个代理对象
    public Object getProxy(){
//        ClassLoader classLoader = target.getClass().getClassLoader();
//        Class[] interfaces = target.getClass().getInterfaces();
        //1.工具类
        Enhancer enhancer = new Enhancer();
        //2.指定被代理对象的父类
        enhancer.setSuperclass(target.getClass());
        //3.指定回调类(回调函数)
        enhancer.setCallback(this);
        //4.创建子类(代理对象)
        return enhancer.create();

    }


    //当代理对象执行处理方法时触发的方法
    @Override
    public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {

        System.out.println("befor======"+ method.getName()+"========="+ Arrays.asList(args));
        Object result = method.invoke(target, args);
        System.out.println("after:"+result);
        return result;
    }
}

创建测试类 

package com.wt.cglib;

/**
 * @Author wt
 * @Date 2022/7/7 9:23
 * @PackageName:com.wt.cglib
 * @ClassName: Test
 * @Description: TODO
 * @Version 1.0
 */
public class Test {
    public static void main(String[] args) {
        //被代理对象
        MathServiceImpl mathService = new MathServiceImpl();
        //创建代理工厂对象
        ProxyFactory proxyFactory = new ProxyFactory(mathService);
        //获取代理对象
        MathServiceImpl proxy = (MathServiceImpl) proxyFactory.getProxy();
        //执行相应地业务代码
        double mul = proxy.div(2, 3);
        System.out.println(mul);
    }
}

cglib可以不基于接口实现 

二.AOP的实现 

应用: 日志:----增加删除修改操作---->记录到日志表中.

JDK动态代理和cglib动态代理代码比较麻烦,----->springaop就是在动态代理的基础上进行了封装,就可以使用一些注解完成。

1.引入依赖


        
            org.springframework
            spring-webmvc
            5.2.15.RELEASE
        


        
            org.springframework
            spring-aspects
            5.2.9.RELEASE
        

 2.配置spring.xml文件




    

    

3.切面实现

通过spring实现切面编程,可通过接口实现,也可直接使用实体类进行实现(使用接口则要在实体类上实现接口)

(1) 定义MathserviceImpl类 

package com.wt.aop;

import org.springframework.stereotype.Service;

/**
 * @Author wt
 * @Date 2022/7/6 16:26
 * @PackageName:com.wt.proxy
 * @ClassName: MathServiceImpl
 * @Description: TODO
 * @Version 1.0
 */

//创建Service注解,当包扫描到该注解时,spring会创建该类的对象
@Service("ms")
public class MathServiceImpl {

    public double add(double a, double b) {
        double result = a+b;
        return result;
    }


    public double sub(double a, double b) {
        double result = a-b;
        int d = 10/0;
        return result;
    }


    public double mul(double a, double b) {
        double result = a*b;
        return result;
    }


    public double div(double a, double b) {
        double result = a/b;
        return result;
    }
}

(2) 创建切面类 

@Aspect、 @Before、等注解Spring默认不识别  需要在spring.xml中开启注解:

    
   

 四种注解:

@Before:方法执行前

@After:不管有没有异常都会执行,相当于finally

@AfterReturning:碰到return关键字执行

@AfterThrowing:碰到异常执行

package com.wt.aop;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;


/**
 * @Author wt
 * @Date 2022/7/7 17:16
 * @PackageName:com.wt.aop
 * @ClassName: LogAspect
 * @Description: TODO
 * @Version 1.0
 */

@Component//等价于Service Controller
@Aspect //表示该类为切面类
public class LogAspect {

    //*表示任意方法   ..表示任意个参数()

    //第一个*表示:修改符合返回值类型
    //第二个*表示:所有类
    //第三个*表示:所有方法
    @Before("execution(* com.wt.aop.MathServiceImpl.*(..))")
    public void before(JoinPoint joinPoint){
        String name = joinPoint.getSignature().getName();
        System.out.println("方法执行前输出"+name+"方法");
    }

    @After("execution(public double com.wt.aop.MathServiceImpl.add(double,double))")
    public void after(){
        System.out.println("方法执行后输出");
    }

    //遇到return执行
    @AfterReturning("execution(* com.wt.aop.*.*(..))")
    public void afterreturning(){
        System.out.println("遇到return执行");
    }

    @AfterThrowing("execution(* com.wt.aop.*.*(..))")
    public void afterthrowing(JoinPoint joinPoint){
        String name = joinPoint.getSignature().getName();
        System.out.println("遇到异常时执行"+name+"方法执行时异常");
    }
}

路径可以使用实际的指定路径,也可用通配符 ‘*’ 代替 

Spring-----AOP面向切面编程---实现不同框架下的登录日志_第4张图片

其他的注解

Spring-----AOP面向切面编程---实现不同框架下的登录日志_第5张图片 (3) 创建测试类

package com.wt.aop;

import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

/**
 * @Author wt
 * @Date 2022/7/7 17:24
 * @PackageName:com.wt.aop
 * @ClassName: Test
 * @Description: TODO
 * @Version 1.0
 */
public class Test {
    public static void main(String[] args) {
        //获取spring配置文件
        ApplicationContext app = new ClassPathXmlApplicationContext("spring.xml");
        MathServiceImpl mathService = (MathServiceImpl) app.getBean("ms");
        double sub = mathService.sub(3, 5);
        System.out.println(sub);


    }
}

aop实际就是在JDK动态代理和cglib动态代理的基础上进行了封装,它会根据类是否实现接口调用不同的动态代理 

4.使用AOP完成ssm项目的登录日志 

 (1) 在数据库中创建登录日志表

create table logincn(
	id int primary key auto_increment comment '日志编号',
	username varchar(255) comment '登录账户',
	userip varchar(255) comment '登录者ip',
	userstate int comment '登录返回状态码',
	usermsg varchar(255) comment '登录返回信息',
	userdate date comment '登录时间'
)

(2) 创建切面类

在不改变当前代码的前提下:

        引入所需依赖,进行spring.xml文件的相关配置,导入切面路径,即可实现登录日志

package com.wt.aop;

import com.wt.entity.LoginCN;
import com.wt.entity.User;
import com.wt.service.UserService;
import com.wt.utils.CommentResult;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.http.HttpServletRequest;
import java.util.Date;

/**
 * @Author wt
 * @Date 2022/7/11 10:02
 * @PackageName:com.wt.aop
 * @ClassName: LoginAop
 * @Description: TODO
 * @Version 1.0
 */
@Component
@Aspect
public class LoginAop {

    @Autowired
    private UserService userService;


    @AfterReturning(value = "execution(* com.wt.controller.UserController.login(..))",returning = "r")
    public void LoginAOP(JoinPoint joinPoint,Object r){
        Object[] args = joinPoint.getArgs();
        System.out.println("登录者的账号:"+args[0]);
        System.out.println(args[0]);
        String arg = (String) args[0];
        System.out.println(arg);

        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = attributes.getRequest();

        String remoteAddr = request.getRemoteAddr();
        System.out.println("登录者的ip:"+remoteAddr);

        CommentResult result = (CommentResult) r;

        System.out.println("登录的状态:"+result.getCode());
        System.out.println("登录的提示信息:"+result.getMsg());

        LoginCN loginCN = new LoginCN(null,arg,remoteAddr, result.getCode(), result.getMsg(),new Date());

        userService.save(loginCN);



    }
}

Spring-----AOP面向切面编程---实现不同框架下的登录日志_第6张图片

5. 完成springcloud/springbooot项目的登录日志

 (1) 在数据库中创建登录日志表

 create table logincn(
    id int primary key auto_increment comment '日志编号',
    username varchar(255) comment '登录账户',
    userip varchar(255) comment '登录者ip',
    userstate int comment '登录返回状态码',
    usermsg varchar(255) comment '登录返回信息',
    userdate date comment '登录时间'
)

(2) 创建切面类

1.和ssm项目不同的是,我们把切面地址单独封装了一个方法,并且获取登录者的ip方法因为springboot项目的路由转发等原因我们封装了一个解析真实ip的方法,获取登录者的真实ip;

2.returning里的值必须和下面方法里的参数名一致,参数的数据类型,若切面路径直接指定方法则类型定义为该方法的返回类型即可,若不指定方法则可以使用Object,后面使用可以根据实际需求进行强制转换为自己需要的数据类型

Spring-----AOP面向切面编程---实现不同框架下的登录日志_第7张图片

package com.senven.common.pojo.aop;

import com.senven.common.entity.Logincn;
import com.senven.common.entity.User;
import com.senven.common.pojo.controller.LoginController;
import com.senven.common.pojo.feign.UserFeign;
import com.senven.common.pojo.vo.CommonResult;
import com.senven.common.pojo.vo.LoginVo;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.bouncycastle.asn1.ocsp.ResponseData;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.http.HttpServletRequest;
import java.time.LocalDateTime;
import java.util.Date;
import java.util.Map;

@Component
@Aspect
public class LoginAop {
    @Autowired
    private UserFeign userFeign;

    @Pointcut("execution(* com.senven.common.pojo.controller.LoginController.login(..))")
    public void loginSucessPointCut(){};

    @AfterReturning(pointcut = "loginSucessPointCut()",returning = "result")
    public void loginSuccess(JoinPoint joinPoint, CommonResult result){
        Object[] args = joinPoint.getArgs();
        System.out.println("登录者的账号:"+args[0]);
        LoginVo arg = (LoginVo) args[0];
        System.out.println(arg);
        String username = arg.getUsername();
        Object data = result.getData();
        Integer code = result.getCode();
        String msg = result.getMsg();


        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = attributes.getRequest();

        //调用封装好的获取登陆者ip地址的方法
        String remoteAddr = getIpddr(request);
        System.out.println("登录者的ip:"+remoteAddr);


//
        System.out.println("登录的状态:"+result.getCode());
        System.out.println("登录的提示信息:"+result.getMsg());
//
        Logincn loginCN = new Logincn(null,username,remoteAddr, result.getCode(), result.getMsg(), LocalDateTime.now());
        userFeign.insertLogincn(loginCN);
    }

        /**
     * 获取IP地址
     *
     */
    public static String getIpAddr(HttpServletRequest request)
    {
        String ip = request.getHeader("X-Real-IP");
        if(!StringUtils.isBlank(ip) && !"unknown".equalsIgnoreCase(ip))
            return ip;
        ip = request.getHeader("X-Forwarded-For");
        if(!StringUtils.isBlank(ip) && !"unknown".equalsIgnoreCase(ip))
        {
            int index = ip.indexOf(',');
            if(index != -1)
                return ip.substring(0, index);
            else
                return ip;
        }
        if(ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip))
            ip = request.getHeader("Proxy-Client-IP");
        if(ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip))
            ip = request.getHeader("WL-Proxy-Client-IP");
        if(ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip))
            ip = request.getHeader("HTTP_CLIENT_IP");
        if(ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip))
            ip = request.getHeader("HTTP_X_FORWARDED_FOR");
        if(ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip))
            ip = request.getRemoteAddr();
        return ip;
    }


}

  • 相关请求头解释
String ip = request.getHeader("X-Real-IP");

X-Real-IP是Nginx用来获取用户的真实ip地址,但是有时候可能获取不到返回是UNKONWN。所以当获取不到使用下面方式进行获取:

String ip = request.getHeader("X-Forwarded-For");

X-Forwarded-For 有时候可能获取不到,因为服务器不一定使用了反向代理,所以可能返回的也是UNKONWN,获取不到可以使用以下方式进行获取:

String ip = request.getHeader("Proxy-Client-IP");

Proxy-Client-IP 是apache服务器定制一个请求头,这个属性只在apache服务器才可以看到。所以当服务器不是apache服务器,也会返回UNKONWN

String ip = request.getHeader("WL-Proxy-Client-IP");

WL- Proxy-Client-IP 其中WL是weblogic的缩写,这通常用户weblogic服务器环境中,也可能返回UNKONWN

String ip = request.getHeader("HTTP_CLIENT_IP");

 HTTP_CLIENT_IP 有些代理服务器会加上此配置

String ip = request.getHeader("HTTP_X_FORWARD_FOR");

HTTP_X_FORWARD_FOR 可以在nginx配置此项,一般为 HTTP_X_FORWARD_FOR $remote_addr;
看了上面的相关头解释和获取IP的方法,则可以将其集成到我们的项目中

你可能感兴趣的:(Sping,spring,java,数据库,后端)