东拼西凑学java

前言

随着大环境的影响,互联网寒冬降临,程序员的日子越来越难,搞不好哪天就被噶了,多学点东西也没啥坏处,国内市场java如日中天,出门在外不会写两行java代码,都不好意思说自己是程序员,伪装成一个萌新运维,混迹于各大java群,偷师学艺,略有所获,水一篇博客以记之
本博客仅仅代表作者个人看法,以.Net视角来对比,不存在语言好坏之分,不足之处,欢迎拍砖,以免误人子弟,java大佬有兴趣可以带我一jio,探讨学习,懂的都懂
特殊用语 --> 懂的都懂 形容一些心照不宣的事情,可自行百度谷歌....

环境准备

JDK 1.8
IDEA
Maven 需配置阿里的源

工程结构

.net里工程结构大致如下:
|---解决方案
    |---项目A
    |---项目B
    |---项目C

java里工程结构
|---项目
    |---模块A
    |---模块B
    |---模块C
  • 我们先创建一个空模板项目 文件->新建项目-->Empty Project 指定项目名称以及项目路径即可
  • 在该项目路径下,创建对应的模块,比较常用的是Spring,Spring Initializr Maven,这个跟.net类似

starter 中间件

starter 是springboot里提出的一个概念,场景启动器,把一些常用的依赖聚合打包,方便使用者直接在项目中使用,简化了开发,在.net里就是中间件了,一个意思

官方解释
Starters are a set of convenient dependency descriptors that you can include in your application.
You get a one-stop shop for all the Spring and related technologies that you need without having to hunt through sample code and copy-paste loads of dependency descriptors. 
For example, if you want to get started using Spring and JPA for database access, include the spring-boot-starter-data-jpa dependency in your project.
spring生态是毋庸置疑的,开发常用的中间件,spring都整理好了,可以去官网直接查阅,懂的都懂

创建一个自定义的starter

  • 新建一个模块,选择Maven模块,给模块取个名字 hello-spring-boot-starter, 取名字要遵循starter的规范,望文知意

  • 再创建一个模块,选择Spring Initializr模块,给模块取个名字 hello-spring-boot-starter-autoconfigure,用于给starter编写装配信息,这样spring就能根据约定,自动装配,hello-spring-boot-starter 依赖于 hello-spring-boot-starter-autoconfigure,当然了如果嫌麻烦,直接在 hello-spring-boot-starter 里写装配信息也可以,这个跟.net里类似,懂的都懂

  • java项目起手式,在src/main/java,创建包路径,通常为公司域名,com.xxx.xxx,我这里定义为com.liang.hello

1.在com.liang.hello下,定义三个包

    autoConfig  用于编写装配信息,生成对象,spring将这些对象添加到IOC容器
    bean        用于映射配置文件,将application.yaml里的配置映射为实体类(javabean)
    service     用于编写中间件的业务代码,需要使用到配置信息的实体类

2.在bean包下,创建HelloProperties 文件,我定义了两个属性,和一个Student对象

    package com.liang.hello.bean;

    import org.springframework.boot.context.properties.ConfigurationProperties;

    /**
    * @ConfigurationProperties("hello")是springboot提供读取配置文件的一个注解
    *  1)让当前类的属性和配置文件中以 hello开头的配置进行绑定
    *  2)以 hello为前缀在配置文件中读取/修改当前类中的属性值
    */
    @ConfigurationProperties("hello")
    public class HelloProperties {

        private String prefix;
        private String suffix;
        private Student student;

        public String getPrefix() {
            return prefix;
        }

        public void setPrefix(String prefix) {
            this.prefix = prefix;
        }

        public String getSuffix() {
            return suffix;
        }

        public void setSuffix(String suffix) {
            this.suffix = suffix;
        }

        public Student getStudent() {
            return student;
        }

        public void setStudent(Student student) {
            this.student = student;
        }
    }

    package com.liang.hello.bean;

    public class Student {
        private String name;
        private String age;

        public String getName() {
            return name;
        }

        public void setName(String name) {
            this.name = name;
        }

        public String getAge() {
            return age;
        }

        public void setAge(String age) {
            this.age = age;
        }
    }

3.在service包下,定义一个接口,跟一个实现类,简单的输出配置文件的信息

    package com.liang.hello.service;
    public interface BaseService {
        String sayMsg(String msg);
    }

    package com.liang.hello.service;

    import com.liang.hello.bean.HelloProperties;
    import org.springframework.beans.factory.annotation.Autowired;

    public class HelloService implements BaseService{

        @Autowired
        private HelloProperties helloProperties;


        public String sayMsg(String msg)
        {
            return helloProperties.getPrefix()+": "+msg+">> "+helloProperties.getSuffix() + helloProperties.getStudent().getName() + helloProperties.getStudent().getAge();
        }
    }

4.在autoConfig包下,产生一个bean对象,丢给spring ioc,要标记这个类为一个配置类

    package com.liang.hello.autoConfig;

    import com.liang.hello.bean.HelloProperties;
    import com.liang.hello.service.BaseService;
    import com.liang.hello.service.HelloService;
    import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
    import org.springframework.boot.context.properties.EnableConfigurationProperties;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;

    @Configuration //标识配置类
    @EnableConfigurationProperties(HelloProperties.class)//开启属性绑定功能+默认将HelloProperties放在容器中
    public class HelloAutoConfiguration {

        /**
        * @Bean注解用于告诉方法,产生一个Bean对象,然后这个Bean对象交给Spring管理。
        * 产生这个Bean对象的方法Spring只会调用一次,随后这个Spring将会将这个Bean对象放在自己的IOC容器中;
        *
        * @ConditionalOnMissingBean(HelloService.class)
        * 条件装配:容器中没有HelloService这个类时标注的方法才生效 / 创建一个HelloService类
        */
        @Bean
        @ConditionalOnMissingBean(HelloService.class)
        public BaseService helloService()
        {
            BaseService helloService = new HelloService();
            return helloService;
        }
    }
  • 前面都非常简单,就是自己生成了一个对象,然后交给spring ioc管理,接下来就是告诉spring 如何寻找到HelloAutoConfiguration

5.在resources/META-INF 下,创建spring.factories 文件,告诉你配置类的位置,srping会扫描包里这个文件,然后执行装配

    # Auto Configure
    org.springframework.boot.autoconfigure.EnableAutoConfiguration=com.liang.hello.autoConfig.HelloAutoConfiguration

这样一个简单的starter就写好了,使用maven构建一下,并推送到本地仓库,maven类似于nuget,懂的都懂,现在去创建一个测试项目,来测试一下

6.新建模块,选择Spring Initializr模块,给模块取个名字 hello-spring-boot-starter-test,在pom.xml里添加依赖

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>com.liang</groupId>
            <artifactId>hello-spring-boot-starter</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>

7.java项目起手式,在src/main/java,创建包路径,定义为com.liang.hello
在resources下,定义application.yaml配置文件

    hello:
      prefix: 你好!
      suffix: 666 and 888
      student:
        name: 雪佬
        age: 18

    server:
      port: 8080
      servlet:
        context-path: /

8.在com.liang.hello下,定义controller包,用于webapi的控制器
定义一个HelloController类,编写一个简单的webapi,来测试自定义的starter,DataResponse是我自定义的一个统一返回类

        @Autowired
        BaseService helloService;

        @ResponseBody
        @GetMapping("/hello") //处理get请求方式的/hello请求路径
        public DataResponse sayHello()   //处理方法
        {
            String s = helloService.sayMsg("test666777888");
            return  DataResponse.Success(s,"");
        }
  • java语法懂的都懂,由于函数没有可选参数,所以需要写很多重载方法
    package com.liang.hello.common;

    import lombok.Builder;
    import lombok.ToString;

    @Builder
    @ToString
    public class DataResponse {
        /**
        * 响应码
        */
        public String Code;
        /**
        * 返回的数据
        */
        public Object Data;

        /**
        * 消息
        */
        public String Message;


        public DataResponse(String code,Object data,String message){
            Code = code;
            Data = data;
            Message = message;
        }


        public static DataResponse Error() {
            return DataResponse.builder().Code("-1").build();
        }
        public static DataResponse Error(Object data) {
            return DataResponse.builder().Code("-1").Data(data).build();
        }
        public static DataResponse Error(String message) {
            return DataResponse.builder().Code("-1").Message(message).build();
        }
        public static DataResponse Error(Object data,String message) {
            return DataResponse.builder().Code("-1").Data(data).Message(message).build();
        }


        public static DataResponse Success() {
            return DataResponse.builder().Code("0").build();
        }
        public static DataResponse Success(Object data) {
            return DataResponse.builder().Code("0").Data(data).build();
        }
        public static DataResponse Success(String message) {
            return DataResponse.builder().Code("0").Message(message).build();
        }
        public static DataResponse Success(Object data,String message) {
            return DataResponse.builder().Code("0").Data(data).Message(message).build();
        }

    }
弄到这里starter就结束了嘛,显然事情没有这么简单,既然用到了spring的自动装配,那我们不妨往深处再挖一挖,没准有意外收获哦

前面我们已经创建了HelloService,那再创建一个TestService,同样继承BaseService,然后HelloAutoConfiguration类下,在写一个testService的bean,测试一下一个接口多个实现,如何获取指定的实例

非常神奇,spring会自动匹配,根据变量名称,自动匹配bean,点击左侧spring的绿色小图标(类似于断点图标),还能自动跳转到bean的实现,不要问,问就是牛逼,懂的都懂

现在我们已经在starter里创建了2个bean,如果有N个bean,每个bean都要去HelloAutoConfiguration类下写装配,真是太麻烦了,这个时候,就可以使用到spring的自动装配注解,只用在testService类上,加一个@Service的注解,就搞定了,简单方便,连spring.factories都不用写,在.net里的DI框架目前还没有统一,有内置的,用的比较多的是autofac,还有自研的DI框架,都大同小异

项目结构
image

springboot

springboot现在已经是java web开发的主流了,通常我们用.net core来之对标,他们诞生的初衷完全不一样,springboot是整合自身的生态,化繁为简,starter就是非常具有代表性的特性之一,.net core是一套跨平台方案,诞生之初就是为了跨平台,本身就非常简洁,易用性也非常高,开发者往里面添加所需的中间件即可,它的关注点始终围绕框架的简洁与性能

选择springboot脚手架项目,会自动创建一个启动文件HelloSpringBootStarterTestApplication 里面有一个@SpringBootApplication的组合注解,想了解的可以去翻阅java八股文,这里我加了一个@EntityScan("com.liang.hello")注解,用于自动扫描该包下的bean,并完成装配

控制器类上,要加@RestController 注解,这也是一个组合注解,然后在方法上加@ResponseBody注解,用于返回json类型,指定方法映射的路由,就可以了,如果想做mvc项目,还需要下载模板引擎的依赖,修改返回类型,指向一个视图,略微麻烦些

image

aop

  • 创建完springboot webapi模块,我们需要添加一个切面,用于记录请求的信息
    java里分为过滤器与拦截器,过滤器依赖与servlet容器,拦截器是Spring容器的功能,本质上都是aop思想的实现
    .net core里内置了各种过滤器,方便我们直接使用,拦截器则使用的比较少

1.老步骤,添加maven依赖

        <dependency>
            <groupId>org.aspectj</groupId>
            <artifactId>aspectjweaver</artifactId>
            <version>1.9.9.1</version>
        </dependency>
  • 定义一个SpringBootAspect的类,用于AOP拦截,先定义一个切入点,再定义切面处理逻辑,这里主要定义一个控制器全局异常处理
        @Aspect
        @Component
        public class SpringBootAspect {

            /**
            * 定义一个切入点
            */
            @Pointcut(value="execution(* com.liang.hello.controller.*.*(..))")
            public void aop(){}

            @Around("aop()")
            public Object around(ProceedingJoinPoint invocation) throws Throwable{
                Object res = null;
                System.out.println("SpringBootAspect..环绕通知 Before");
                try {

                    res = invocation.proceed();
                }catch (Throwable throwable){
                    //修改内容
                    System.out.println("页面执行错误,懂的都懂");
                    res = new DataResponse("500",null,"页面执行错误");
                }
                System.out.println("SpringBootAspect..环绕通知 After");
                return res;
            }

        }

image

ide提示异常,java规定,结束语句后面,不允许有代码,他们认为编译器不执行的代码是垃圾代码,呔,java语法懂的都懂,略施小计,成功的骗过了ide
image

  • 执行结果
    image

定时任务

  • 定时任务是工作中使用非常频繁的部分,也有很多框架,但是一些简单的内置任务,使用框架就有点杀鸡用牛刀了,.net里我们通常用HostedService来实现,springboot内置了定时任务

1.创建一个ScheduledTasks类,使用注解开启异步,HelloSpringBootStarterTestApplication类也要开启哦,代码如下

    @EnableAsync
    @Component
    public class ScheduledTasks {
        private static final SimpleDateFormat dateFormat = new SimpleDateFormat("HH:mm:ss");

        /**
        * 任务调度,每隔1秒执行一次
        */
        @Async
        @Scheduled(fixedRate = 1000)
        public void reportCurrentTime() {

            runThreadTest(1);

        }

        public void runThreadTest(int i) {
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("线程"+Thread.currentThread().getName()+"执行异步任务"+i + "现在时间:" + dateFormat.format(new Date()));

        }
    }

image

  • runThreadTest方法,堵塞3秒,模拟业务执行耗时,发现定开启了异步,但是它依旧是同步执行,需要等上一个任务执行完毕,才会再执行下一个任务,网上翻了下答案,需要配置线程池,代码如下
    package com.liang.hello.config;


    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.scheduling.annotation.AsyncConfigurer;
    import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;

    import java.util.concurrent.Executor;

    @Configuration
    public class ExecutorConfig implements AsyncConfigurer {

        // ThredPoolTaskExcutor的处理流程
        // 当池子大小小于corePoolSize,就新建线程,并处理请求
        // 当池子大小等于corePoolSize,把请求放入workQueue中,池子里的空闲线程就去workQueue中取任务并处理
        // 当workQueue放不下任务时,就新建线程入池,并处理请求,
        // 如果池子大小撑到了maximumPoolSize,就用RejectedExecutionHandler来做拒绝处理
        // 当池子的线程数大于corePoolSize时,多余的线程会等待keepAliveTime长时间,如果无请求可处理就自行销毁
        //getAsyncExecutor:自定义线程池,若不重写会使用默认的线程池。
        @Override
        @Bean
        public Executor getAsyncExecutor() {
            ThreadPoolTaskExecutor threadPool = new ThreadPoolTaskExecutor();
            //设置核心线程数
            threadPool.setCorePoolSize(10);
            //设置最大线程数
            threadPool.setMaxPoolSize(20);
            //线程池所使用的缓冲队列
            threadPool.setQueueCapacity(10);
            //等待任务在关机时完成--表明等待所有线程执行完
            threadPool.setWaitForTasksToCompleteOnShutdown(true);
            // 等待时间 (默认为0,此时立即停止),并没等待xx秒后强制停止
            threadPool.setAwaitTerminationSeconds(60);
            // 线程名称前缀
            threadPool.setThreadNamePrefix("ThreadPoolTaskExecutor-");

            // 初始化线程
            threadPool.initialize();
            return threadPool;
        }
    }

执行结果
image

mybatis plus

  • 目前java主流的ORM框架,应该是mybatis了,我是不怎么喜欢在xml里组织sql的,麻烦的一批,但是也避免了萌新为图方便,sql写的到处都是,维护起来懂的都懂,网上随便翻个答案,直接往项目里整合

1.老样子先添加依赖

        <!--mybatis-plus的springboot支持-->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.4.3.1</version>
        </dependency>
        <!--mysql驱动-->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-core</artifactId>
            <version>3.4.3.1</version>
        </dependency>

2.然后在yaml文件里添加mysql与mybatis plus的配置

    spring:
        datasource:
            driver-class-name: com.mysql.cj.jdbc.Driver
            url: jdbc:mysql://************:3306/test_mybatis?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull&useSSL=false&allowPublicKeyRetrieval=true
            username: root
            password: ******
        jackson:
            date-format: yyyy-MM-dd HH:mm:ss
            time-zone: GMT+8
            serialization:
            write-dates-as-timestamps: false

        mybatis-plus:
        configuration:
            map-underscore-to-camel-case: false
            auto-mapping-behavior: full
            log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
        mapper-locations: classpath:mapper/*.xml
        global-config:
            # 逻辑删除配置
            db-config:
            # 删除前
            logic-not-delete-value: 1
            # 删除后
            logic-delete-value: 0

3.再整一个mybatis plus的配置类,添加mybatis plus的拦截器,反正也是网上抄的,我猜测大致是这个意思

    package com.liang.hello.config;

    import com.baomidou.mybatisplus.annotation.DbType;
    import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
    import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;

    @Configuration
    public class MybatisPlusConfig {
        @Bean
        public MybatisPlusInterceptor mybatisPlusInterceptor() {
            MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
            interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
            return interceptor;
        }
    }

现在开始添加创建mybatis的相关目录,网上都有,直接跟着抄就可以了,也非常好理解

4.定义一个mapper的包,在包下编写mybatis的接口,我这里用的是mybatis plus,已经默认实现了CRUD,我们简单的写几个接口,用来测试

    package com.liang.hello.mapper;

    import com.baomidou.mybatisplus.core.conditions.Wrapper;
    import com.baomidou.mybatisplus.core.mapper.BaseMapper;
    import com.baomidou.mybatisplus.core.toolkit.Constants;
    import com.liang.hello.dto.OrderInfoResponse;
    import com.liang.hello.entity.UserInfo;
    import org.apache.ibatis.annotations.Mapper;
    import org.apache.ibatis.annotations.Param;
    import org.apache.ibatis.annotations.Select;
    import org.springframework.stereotype.Repository;
    import java.util.List;

    @Repository
    @Mapper
    //表明这是一个Mapper,也可以在启动类上加上包扫描
    //Mapper 继承该接口后,无需编写 mapper.xml 文件,即可获得CRUD功能
    public interface UserInfoMapper extends BaseMapper<UserInfo> {

        @Select("select u.*,o.id as orderId,o.price from user_info u left join order_info o on u.id = o.userId ${ew.customSqlSegment}")
        List<OrderInfoResponse> getAll(@Param(Constants.WRAPPER) Wrapper wrapper);

        List<UserInfo> selectByName(@Param("UserName") String userName);

        void updateUserInfo(@Param("UserName") String userName,@Param("Age") int age);
    }
  • 简单的sql,mybatis plus也支持直接使用注解的方式来执行,简单方便,参数是通过queryWrapper条件构造器来完成的,喜欢的同学可以重点了解一下,.net里有linq,用过的同学懂的都懂

    另外一个方式,就是通过制定mapper.xml来编写sql,xml文件路径在配置文件里制定,我们按照约定即可,在resources/mapper下,创建UserInfo.xml,名称空间指向接口路径,id对应接口的名称,返回类型指向对应的实体

    <?xml version="1.0" encoding="UTF-8"?>
    <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "<http://mybatis.org/dtd/mybatis-3-mapper.dtd>">
    <mapper namespace="com.com.liang.hello.mapper.UserInfoMapper">

        <select id="selectByName" resultType="com.liang.hello.entity.UserInfo">
            select * from user_info
            <where>
                <if test="UserName != null and UserName != ''">
                    UserName like CONCAT('%',#{UserName},'%');
                </if>
            </where>
        </select>

        <select id="updateUserInfo" resultType="com.liang.hello.entity.UserInfo">
            update user_info set Age=#{Age}
            <where>
                <if test="UserName != null and UserName != ''">
                    UserName = #{UserName};
                </if>
            </where>
        </select>
    </mapper>

mybatis xml语法可以去学习下,也不困难,简单的看一遍就差不多了,复杂的部分用到的时候再去翻阅

5.定义一个service的包,在包下创建UserInfoServiceImpl,很熟悉的味道,经典的三层架构,在service层编写业务逻辑,调用mapper接口的增删改查方法,这里重点说下事务

spring提供了事务的注解@Transactional,使用起来也非常方便,原理应该是借助AOP来实现,使用这个注解前需要事先了解事务失效的场景,老八股文了,懂的都懂,在.net里使用手动提交事务比较多,特意去了解搜了下手动提交事务,感觉差不多

    //修改年龄
    @Transactional
    public void update(UserInfo entity){
    //        TransactionStatus txStatus = transactionManager.getTransaction(new DefaultTransactionDefinition());
    //
    //        try {
    //            userInfoMapper.updateUserInfo(entity.getUserName(),entity.getAge());
    //            if(true) {
    //                throw new Exception("xx");
    //            }
    //            userInfoMapper.updateUserInfo(entity.getUserName(),entity.getAge()+1);
    //
    //        } catch (Exception e) {
    //            transactionManager.rollback(txStatus);
    //            e.printStackTrace();
    //        }finally {
    //            transactionManager.commit(txStatus);
    //        }



        //执行第一条sql
        userInfoMapper.updateUserInfo(entity.getUserName(),entity.getAge());
        if(true) throw new RuntimeException();
        //执行第二条sql
        userInfoMapper.updateUserInfo(entity.getUserName(),entity.getAge()+1);
    }

jwt

  • 现在前后端分离已经成为主流,jwt是首选方案,话不多说,直接往里面怼

1.老规矩,先添加jwt的依赖

        <dependency>
            <groupId>com.auth0</groupId>
            <artifactId>java-jwt</artifactId>
            <version>3.4.0</version>
        </dependency>

2.先定义一个工具类JwtUtils,用于jwt的一些常规操作,当看到verifyToken这个方法的时候,我就发现事情没有那么简单

    package com.liang.hello.common;

    import com.auth0.jwt.JWT;
    import com.auth0.jwt.JWTVerifier;
    import com.auth0.jwt.algorithms.Algorithm;
    import com.auth0.jwt.exceptions.JWTDecodeException;
    import com.auth0.jwt.exceptions.TokenExpiredException;
    import com.auth0.jwt.interfaces.Claim;
    import com.auth0.jwt.interfaces.DecodedJWT;
    import org.springframework.util.StringUtils;

    import javax.servlet.http.HttpServletRequest;
    import java.io.UnsupportedEncodingException;
    import java.util.Date;
    import java.util.Enumeration;
    import java.util.HashMap;
    import java.util.Map;


    public class JwtUtils {
        // 过期时间 24 小时  60 * 24 * 60 * 1000
        private static final long EXPIRE_TIME = 60 * 60 * 1000;//60分钟
        // 密钥
        private static final String SECRET = "uxzc5ADbRigUDaY6pZFfWus2jZWLPH1";
        private static  String json="";

        /**
        * 生成 token
        */
        public static String createToken(String userId) {
            try {
                // 设置过期时间
                Date date = new Date(System.currentTimeMillis() + EXPIRE_TIME);
                // 私钥和加密算法
                Algorithm algorithm = Algorithm.HMAC256(SECRET);
                // 设置头部信息
                Map<String, Object> header = new HashMap<>(2);
                header.put("Type", "Jwt");
                header.put("alg", "HS256");

                // 返回token字符串 附带userId信息
                return JWT.create()
                        .withHeader(header)
                        .withClaim("userId", userId)
                        //到期时间
                        .withExpiresAt(date)
                        //创建一个新的JWT,并使用给定的算法进行标记
                        .sign(algorithm);

            } catch (Exception e) {
                return null;
            }
        }

        /**
        * 校验 token 是否正确
        */
        public static Map<String, Claim> verifyToken(String token){
            token = token.replace("Bearer ","");
            DecodedJWT jwt = null;
            try {
                JWTVerifier verifier = JWT.require(Algorithm.HMAC256(SECRET)).build();
                jwt = verifier.verify(token);

            } catch (TokenExpiredException e) {
                //效验失败
                //这里抛出的异常是我自定义的一个异常,你也可以写成别的
                throw new TokenExpiredException("token校验失败");
            }
            return jwt.getClaims();
        }

        /**
        * 获得token中的信息
        */
        public static String getUserId(String token) {
            Map<String, Claim> claims = verifyToken(token);
            Claim user_id_claim = claims.get("userId");
            if (null == user_id_claim || StringUtils.isEmpty(user_id_claim.asString())) {
                return null;
            }
            return  user_id_claim.asString();
        }

    }
  • 校验token正确,从token中获取信息,在.net里框架帮忙做了,使用起来非常简单,emmmmm.....我觉得spring提供一个spring-boot-starter-jwt 很有必要

.net里实现如下

            //认证参数
            services.AddAuthentication("Bearer")
                .AddJwtBearer(o =>
                {
                    o.TokenValidationParameters = new TokenValidationParameters
                    {
                        ValidateIssuerSigningKey = true,//是否验证签名,不验证的话可以篡改数据,不安全
                        IssuerSigningKey = signingKey,//解密的密钥
                        ValidateIssuer = true,//是否验证发行人,就是验证载荷中的Iss是否对应ValidIssuer参数
                        ValidIssuer = jwtOptions.Iss,//发行人
                        ValidateAudience = true,//是否验证订阅人,就是验证载荷中的Aud是否对应ValidAudience参数
                        ValidAudience = jwtOptions.Aud,//订阅人
                        ValidateLifetime = true,//是否验证过期时间,过期了就拒绝访问
                        ClockSkew = TimeSpan.Zero,//这个是缓冲过期时间,也就是说,即使我们配置了过期时间,这里也要考虑进去,过期时间+缓冲,默认好像是7分钟,你可以直接设置为0
                        RequireExpirationTime = true,
                    };
                    o.Events = new JwtBearerEvents
                    {
                        //权限验证失败后执行
                        OnChallenge = context =>
                        {
                            //终止默认的返回结果(必须有)
                            context.HandleResponse();
                            var result = JsonConvert.SerializeObject(new { code = "401", message = "验证失败" });
                            context.Response.ContentType = "application/json";
                            //验证失败返回401
                            context.Response.StatusCode = StatusCodes.Status200OK;
                            context.Response.WriteAsync(result);
                            return Task.FromResult(0);
                        }
                    };
                });
  • 思路应该比较简单,弄个拦截器,校验一波jwt,完成认证,再通过jwt里的userId校验用户是否拥有访问权限,开干

3.先整一个自定义的注解AllowAnonymousAttribute,允许匿名访问,标识这个注解可以作用于类和方法上

    package com.liang.hello.attribute;

    import java.lang.annotation.ElementType;
    import java.lang.annotation.Retention;
    import java.lang.annotation.RetentionPolicy;
    import java.lang.annotation.Target;

    @Target({ElementType.METHOD, ElementType.TYPE})
    @Retention(RetentionPolicy.RUNTIME)
    public @interface AllowAnonymousAttribute {
        boolean required() default true;
    }
  • 编写自定义拦截器,用于jwt的校验,校验通过,获取用户信息并授权,这里主要是获取类跟方法有没有使用自定义注解,HandlerInterceptorAdapter也提示已过期,不知道有没有替代方案
    package com.liang.hello.filters;

    import com.auth0.jwt.interfaces.Claim;
    import com.liang.hello.attribute.AllowAnonymousAttribute;
    import com.liang.hello.common.JwtUtils;
    import com.liang.hello.entity.UserInfo;
    import com.liang.hello.service.UserInfoServiceImpl;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Component;
    import org.springframework.web.method.HandlerMethod;
    import org.springframework.web.servlet.HandlerInterceptor;
    import org.springframework.web.servlet.ModelAndView;
    import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;

    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    import java.lang.annotation.Annotation;
    import java.lang.reflect.Method;
    import java.security.SignatureException;
    import java.util.Map;

    @Component
    public class JwtFilter extends HandlerInterceptorAdapter {
        @Autowired
        UserInfoServiceImpl userInfoService;

        @Override
        public boolean preHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object object) throws SignatureException {
            // 如果不是映射到方法直接通过
            if (!(object instanceof HandlerMethod)) {
                return true;
            }
            HandlerMethod handlerMethod = (HandlerMethod) object;
            AllowAnonymousAttribute actionAttribute= handlerMethod.getMethod().getDeclaredAnnotation(AllowAnonymousAttribute.class);
            AllowAnonymousAttribute controllerAttribute = handlerMethod.getBeanType().getDeclaredAnnotation(AllowAnonymousAttribute.class);

            if (actionAttribute!=null || controllerAttribute!=null) return true;
            //默认全部检查
            System.out.println("被jwt拦截需要验证");
            // 从请求头中取出 token  这里需要和前端约定好把jwt放到请求头一个叫Authorization的地方,**<font color=red size=3>懂的都懂</font>**
            String token = httpServletRequest.getHeader("Authorization");
            // 执行认证
            if (token == null) {
                //这里其实是登录失效,没token了   这个错误也是我自定义的,读者需要自己修改
                throw new SignatureException("自定义错误");
            }
            // 获取 token 中的 user Id
            String userId = JwtUtils.getUserId(token);

            //找找看是否有这个user   因为我们需要检查用户是否存在,读者可以自行修改逻辑
            UserInfo user = userInfoService.getUserInfoById(userId);

            if (user == null) {
                //这个错误也是我自定义的
                throw new SignatureException("自定义错误");
            }
            //放入attribute以便后面调用
            httpServletRequest.setAttribute("userName", user.getUserName());
            httpServletRequest.setAttribute("id", user.getId());
            httpServletRequest.setAttribute("age", user.getAge());
            return true;
        }

    }

4.注册自定义拦截器,让spring调用这个拦截器

    package com.liang.hello.config;

    import com.liang.hello.filters.JwtFilter;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
    import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

    import javax.annotation.Resource;

    @Configuration
    public class WebConfig implements WebMvcConfigurer {
        @Resource
        private JwtFilter jwtFilter ;
        @Override
        public void addInterceptors(InterceptorRegistry registry) {
            registry.addInterceptor(jwtFilter).addPathPatterns("/**");
        }
    }

异常处理

前面我们有使用过框架自定义的一些异常,TokenExpiredException,SignatureException,我们可以在SpringBootAspect里处理这些异常,并给出友好提示

    @ExceptionHandler(value = {TokenExpiredException.class})
    public DataResponse tokenExpiredException(TokenExpiredException e){
        return new DataResponse("401",null,"权限不足token失效");
    }

    @ExceptionHandler(value = {SignatureException.class})
    public DataResponse authorizationException(SignatureException e){
        return new DataResponse("401",null,"权限不足");
    }
    //全局异常,兜底方案
    @ExceptionHandler(value = {Exception.class})
    public DataResponse exception(Exception e){
        return new DataResponse("500",null,"系统错误");
    }
  • 未登录访问需要授权接口
    image

  • 登录,使用错误的用户名
    image

  • 登录,使用正确的用户名
    image

  • 使用token,访问需要授权接口
    主动抛出异常
    image
    正常执行
    image

  • token过期,访问需要授权接口
    image

  • 使用错误token,访问需要授权接口,因为没有主动捕获该异常,被全局异常统一处理
    image

posted @ 2022-12-16 15:55  提伯斯  阅读(572)  评论(0编辑  收藏  举报