东拼西凑学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框架,都大同小异 

项目结构
东拼西凑学java

springboot

springboot现在已经是java web开发的主流了,通常我们用.net core来之对标,他们诞生的初衷完全不一样,springboot是整合自身的生态,化繁为简,starter就是非常具有代表性的特性之一,.net core是一套跨平台方案,诞生之初就是为了跨平台,本身就非常简洁,易用性也非常高,开发者往里面添加所需的中间件即可,它的关注点始终围绕框架的简洁与性能  选择springboot脚手架项目,会自动创建一个启动文件HelloSpringBootStarterTestApplication 里面有一个@SpringBootApplication的组合注解,想了解的可以去翻阅java八股文,这里我加了一个@EntityScan("com.liang.hello")注解,用于自动扫描该包下的bean,并完成装配  控制器类上,要加@RestController 注解,这也是一个组合注解,然后在方法上加@ResponseBody注解,用于返回json类型,指定方法映射的路由,就可以了,如果想做mvc项目,还需要下载模板引擎的依赖,修改返回类型,指向一个视图,略微麻烦些 

东拼西凑学java

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;             }          } 

东拼西凑学java

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

  • 执行结果
    东拼西凑学java

定时任务

  • 定时任务是工作中使用非常频繁的部分,也有很多框架,但是一些简单的内置任务,使用框架就有点杀鸡用牛刀了,.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()));          }     } 

东拼西凑学java

  • 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;         }     } 

执行结果
东拼西凑学java

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,"系统错误");     } 
  • 未登录访问需要授权接口
    东拼西凑学java

  • 登录,使用错误的用户名
    东拼西凑学java

  • 登录,使用正确的用户名
    东拼西凑学java

  • 使用token,访问需要授权接口
    主动抛出异常
    东拼西凑学java
    正常执行
    东拼西凑学java

  • token过期,访问需要授权接口
    东拼西凑学java

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

发表评论

相关文章